memgineering 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +252 -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 +2735 -780
- 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
|
}
|
|
@@ -4122,15 +5153,16 @@ var init_setup_web_page = __esm({
|
|
|
4122
5153
|
var setup_web_exports = {};
|
|
4123
5154
|
__export(setup_web_exports, {
|
|
4124
5155
|
browserOpener: () => browserOpener,
|
|
5156
|
+
disclosure: () => disclosure,
|
|
4125
5157
|
runSetupWeb: () => runSetupWeb,
|
|
4126
5158
|
startSetupWebServer: () => startSetupWebServer
|
|
4127
5159
|
});
|
|
4128
|
-
import
|
|
5160
|
+
import "child_process";
|
|
4129
5161
|
import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
4130
5162
|
import { readdir as readdir6, stat as stat3 } from "fs/promises";
|
|
4131
5163
|
import { createServer } from "http";
|
|
4132
5164
|
import { homedir as homedir3 } from "os";
|
|
4133
|
-
import { basename, isAbsolute as
|
|
5165
|
+
import { basename, isAbsolute as isAbsolute3, join as join17, relative as relative2, resolve as resolve8 } from "path";
|
|
4134
5166
|
async function startSetupWebServer(opts) {
|
|
4135
5167
|
const token = randomBytes2(32).toString("hex");
|
|
4136
5168
|
const idleMs = opts.idleMs ?? IDLE_TIMEOUT_MS;
|
|
@@ -4142,11 +5174,11 @@ async function startSetupWebServer(opts) {
|
|
|
4142
5174
|
sockets.add(socket);
|
|
4143
5175
|
socket.on("close", () => sockets.delete(socket));
|
|
4144
5176
|
});
|
|
4145
|
-
await new Promise((
|
|
5177
|
+
await new Promise((resolve11, reject) => {
|
|
4146
5178
|
server.once("error", reject);
|
|
4147
5179
|
server.listen(0, "127.0.0.1", () => {
|
|
4148
5180
|
server.off("error", reject);
|
|
4149
|
-
|
|
5181
|
+
resolve11();
|
|
4150
5182
|
});
|
|
4151
5183
|
});
|
|
4152
5184
|
const address = server.address();
|
|
@@ -4154,8 +5186,8 @@ async function startSetupWebServer(opts) {
|
|
|
4154
5186
|
const origin = `http://127.0.0.1:${port}`;
|
|
4155
5187
|
const ownOrigins = /* @__PURE__ */ new Set([origin, `http://localhost:${port}`]);
|
|
4156
5188
|
let resolveDone;
|
|
4157
|
-
const done = new Promise((
|
|
4158
|
-
resolveDone =
|
|
5189
|
+
const done = new Promise((resolve11) => {
|
|
5190
|
+
resolveDone = resolve11;
|
|
4159
5191
|
});
|
|
4160
5192
|
let stopped = false;
|
|
4161
5193
|
let idleTimer;
|
|
@@ -4165,8 +5197,8 @@ async function startSetupWebServer(opts) {
|
|
|
4165
5197
|
if (idleTimer) clearTimeout(idleTimer);
|
|
4166
5198
|
clearTimeout(maxTimer);
|
|
4167
5199
|
process.off("SIGINT", onInterrupt);
|
|
4168
|
-
await new Promise((
|
|
4169
|
-
server.close(() =>
|
|
5200
|
+
await new Promise((resolve11) => {
|
|
5201
|
+
server.close(() => resolve11());
|
|
4170
5202
|
for (const socket of sockets) socket.destroy();
|
|
4171
5203
|
});
|
|
4172
5204
|
resolveDone(reason);
|
|
@@ -4183,7 +5215,13 @@ async function startSetupWebServer(opts) {
|
|
|
4183
5215
|
void stop("interrupted");
|
|
4184
5216
|
}
|
|
4185
5217
|
process.once("SIGINT", onInterrupt);
|
|
4186
|
-
const state = {
|
|
5218
|
+
const state = {
|
|
5219
|
+
detected: opts.detected,
|
|
5220
|
+
token,
|
|
5221
|
+
ownOrigins,
|
|
5222
|
+
applied: false,
|
|
5223
|
+
...opts.preselectAccount === void 0 ? {} : { preselectAccount: opts.preselectAccount }
|
|
5224
|
+
};
|
|
4187
5225
|
server.on("request", (req, res) => {
|
|
4188
5226
|
void handle(req, res, state, origin, touch).then((outcome) => {
|
|
4189
5227
|
if (outcome === "applied") setTimeout(() => void stop("applied"), lingerMs).unref();
|
|
@@ -4209,21 +5247,40 @@ async function startSetupWebServer(opts) {
|
|
|
4209
5247
|
};
|
|
4210
5248
|
}
|
|
4211
5249
|
async function runSetupWeb(opts) {
|
|
4212
|
-
const server = await startSetupWebServer({
|
|
5250
|
+
const server = await startSetupWebServer({
|
|
5251
|
+
detected: opts.detected,
|
|
5252
|
+
...opts.preselectAccount === void 0 ? {} : { preselectAccount: opts.preselectAccount }
|
|
5253
|
+
});
|
|
4213
5254
|
const opened = opts.printUrl ? false : await openBrowser(server.url);
|
|
4214
5255
|
printDual({
|
|
4215
|
-
|
|
5256
|
+
// The URL carries the key that authorizes reading folders and writing into
|
|
5257
|
+
// `~/.claude`, and it was printed on every run — in `--json` too, so it
|
|
5258
|
+
// landed in the transcript of the agent that ran the command, which may be
|
|
5259
|
+
// stored, synced or sent somewhere. That is a different exposure from the
|
|
5260
|
+
// one everybody thinks of with a local token: not a process on this machine
|
|
5261
|
+
// that could read those files anyway, but a copy of the key leaving it.
|
|
5262
|
+
//
|
|
5263
|
+
// So it is emitted only when it is the way in — the browser did not open,
|
|
5264
|
+
// or `--print-url` asked for it. When the browser DID open, the person is
|
|
5265
|
+
// already looking at the screen and nobody needs the string.
|
|
5266
|
+
json: {
|
|
5267
|
+
...disclosure(server.url, server.port, opened)
|
|
5268
|
+
},
|
|
4216
5269
|
human: () => {
|
|
4217
5270
|
if (opened) {
|
|
4218
5271
|
printHuman("Opened setup in your browser.\n");
|
|
4219
|
-
printHuman(
|
|
5272
|
+
printHuman(
|
|
5273
|
+
c.gray(
|
|
5274
|
+
" Nothing appeared? Run `memgineering setup --web --print-url` and open\n the link yourself \u2014 it is not printed here on purpose, because it\n carries the key that authorizes the install."
|
|
5275
|
+
)
|
|
5276
|
+
);
|
|
4220
5277
|
} else {
|
|
4221
5278
|
printHuman("Open this in a browser to finish setting up:\n");
|
|
4222
5279
|
printHuman(` ${server.url}`);
|
|
4223
5280
|
}
|
|
4224
5281
|
printHuman(
|
|
4225
5282
|
c.gray(
|
|
4226
|
-
"\n The link only
|
|
5283
|
+
"\n The link works only on this machine, and only until this screen\n closes \u2014 applying the setup, closing the tab, or an hour, whichever\n comes first. Ctrl-C to stop waiting."
|
|
4227
5284
|
)
|
|
4228
5285
|
);
|
|
4229
5286
|
}
|
|
@@ -4232,7 +5289,7 @@ async function runSetupWeb(opts) {
|
|
|
4232
5289
|
const ending = {
|
|
4233
5290
|
applied: "Setup applied. Restart your agent session for it to take effect.",
|
|
4234
5291
|
cancelled: "Closed without applying \u2014 nothing was changed.",
|
|
4235
|
-
idle: "Nothing happened for
|
|
5292
|
+
idle: "Nothing happened for half an hour, so the setup screen was closed. Nothing was changed \u2014 run `memgineering setup --web` again when you are ready.",
|
|
4236
5293
|
interrupted: "Stopped \u2014 nothing was changed.",
|
|
4237
5294
|
closed: "Setup screen closed."
|
|
4238
5295
|
};
|
|
@@ -4240,6 +5297,9 @@ async function runSetupWeb(opts) {
|
|
|
4240
5297
|
else printHuman(`
|
|
4241
5298
|
${reason === "applied" ? c.green(ending[reason]) : c.gray(ending[reason])}`);
|
|
4242
5299
|
}
|
|
5300
|
+
function disclosure(url, port, opened) {
|
|
5301
|
+
return opened ? { port, opened } : { port, opened, url };
|
|
5302
|
+
}
|
|
4243
5303
|
async function handle(req, res, state, origin, touch = () => {
|
|
4244
5304
|
}) {
|
|
4245
5305
|
const url = new URL(req.url ?? "/", origin);
|
|
@@ -4330,7 +5390,11 @@ async function stateDocument(state) {
|
|
|
4330
5390
|
hook: state.detected.some((t) => t.hooks)
|
|
4331
5391
|
},
|
|
4332
5392
|
brains: cfg.brain.brains.map((b) => b.root),
|
|
4333
|
-
applied: state.applied
|
|
5393
|
+
applied: state.applied,
|
|
5394
|
+
/** Null unless a flag already answered it. The screen selects it, never submits it. */
|
|
5395
|
+
preselect_account: state.preselectAccount ?? null,
|
|
5396
|
+
/** Already connected, so the screen can say so instead of offering a second sign-in. */
|
|
5397
|
+
signed_in: (await signInSummary()).signedIn
|
|
4334
5398
|
};
|
|
4335
5399
|
}
|
|
4336
5400
|
async function candidateFolders(linked) {
|
|
@@ -4341,16 +5405,17 @@ async function candidateFolders(linked) {
|
|
|
4341
5405
|
if (out.length >= MAX_CANDIDATES || seen.has(path)) return;
|
|
4342
5406
|
seen.add(path);
|
|
4343
5407
|
if (!await isDirectory(path)) return;
|
|
4344
|
-
|
|
4345
|
-
|
|
5408
|
+
const notes = await countNotes(path);
|
|
5409
|
+
if (notes === 0) return;
|
|
5410
|
+
out.push({ path, label: basename(path), linked: linked.includes(path), notes });
|
|
4346
5411
|
};
|
|
4347
|
-
for (const entry of await entriesOf(
|
|
4348
|
-
await consider(
|
|
5412
|
+
for (const entry of await entriesOf(join17(home, "Documents"))) {
|
|
5413
|
+
await consider(join17(home, "Documents", entry));
|
|
4349
5414
|
}
|
|
4350
5415
|
for (const entry of await entriesOf(home)) {
|
|
4351
|
-
if (/^brain/i.test(entry)) await consider(
|
|
5416
|
+
if (/^brain/i.test(entry)) await consider(join17(home, entry));
|
|
4352
5417
|
}
|
|
4353
|
-
for (const name of ["notes", "Notes"]) await consider(
|
|
5418
|
+
for (const name of ["notes", "Notes"]) await consider(join17(home, name));
|
|
4354
5419
|
return out;
|
|
4355
5420
|
}
|
|
4356
5421
|
async function entriesOf(dir) {
|
|
@@ -4359,21 +5424,24 @@ async function entriesOf(dir) {
|
|
|
4359
5424
|
async function isDirectory(path) {
|
|
4360
5425
|
return stat3(path).then((s) => s.isDirectory()).catch(() => false);
|
|
4361
5426
|
}
|
|
4362
|
-
async function
|
|
5427
|
+
async function countNotes(dir) {
|
|
5428
|
+
const isNote = (n) => /\.(md|markdown)$/i.test(n);
|
|
4363
5429
|
const entries = await entriesOf(dir);
|
|
4364
|
-
|
|
5430
|
+
let total = entries.filter(isNote).length;
|
|
5431
|
+
if (total >= COUNT_CAP) return COUNT_CAP;
|
|
4365
5432
|
for (const entry of entries.slice(0, 40)) {
|
|
4366
|
-
const child =
|
|
5433
|
+
const child = join17(dir, entry);
|
|
4367
5434
|
if (!await isDirectory(child)) continue;
|
|
4368
|
-
|
|
5435
|
+
total += (await entriesOf(child)).filter(isNote).length;
|
|
5436
|
+
if (total >= COUNT_CAP) return COUNT_CAP;
|
|
4369
5437
|
}
|
|
4370
|
-
return
|
|
5438
|
+
return total;
|
|
4371
5439
|
}
|
|
4372
5440
|
function isSystemPath(path) {
|
|
4373
5441
|
return SYSTEM_ROOTS.some((root) => {
|
|
4374
5442
|
if (root === "/") return path === "/";
|
|
4375
5443
|
const rel = relative2(root, path);
|
|
4376
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
5444
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
4377
5445
|
});
|
|
4378
5446
|
}
|
|
4379
5447
|
async function inspectFromScreen(req, res, state) {
|
|
@@ -4400,7 +5468,7 @@ async function inspectFromScreen(req, res, state) {
|
|
|
4400
5468
|
});
|
|
4401
5469
|
return "served";
|
|
4402
5470
|
}
|
|
4403
|
-
const path =
|
|
5471
|
+
const path = resolve8(expandHome(asked.trim()));
|
|
4404
5472
|
if (isSystemPath(path)) {
|
|
4405
5473
|
sendJson(res, 400, {
|
|
4406
5474
|
error: {
|
|
@@ -4492,14 +5560,41 @@ async function applyFromScreen(req, res, state) {
|
|
|
4492
5560
|
const changes = await applySetup(choice.value, false);
|
|
4493
5561
|
const brain = await connectBrain(choice.brain);
|
|
4494
5562
|
const cfg = await loadConfig();
|
|
5563
|
+
const signIn = choice.account === "cloud" ? await runSignInStep({ login: true, interactive: false, browser: true, dryRun: false }) : void 0;
|
|
5564
|
+
if (signIn) printSignInStep(signIn);
|
|
4495
5565
|
sendJson(res, 200, {
|
|
4496
5566
|
applied: true,
|
|
5567
|
+
/**
|
|
5568
|
+
* The answer, and the sign-in it started, under DIFFERENT keys.
|
|
5569
|
+
*
|
|
5570
|
+
* `signInStepJson` returns `{ account: … }`, so spreading it beside a field
|
|
5571
|
+
* also called `account` silently replaced the string with an object and the
|
|
5572
|
+
* screen's `result.account === 'cloud'` stopped being true. Named apart so
|
|
5573
|
+
* the collision cannot come back.
|
|
5574
|
+
*/
|
|
5575
|
+
account: choice.account,
|
|
5576
|
+
...signIn ? { account_info: signInStepJson(signIn)["account"] } : {},
|
|
4497
5577
|
tools: choice.value.tools.map((t) => t.id),
|
|
4498
5578
|
auto_update: choice.value.autoUpdate,
|
|
4499
5579
|
hook: choice.value.hook,
|
|
4500
5580
|
changes,
|
|
4501
5581
|
brain,
|
|
4502
5582
|
brains_linked: cfg.brain.brains.length,
|
|
5583
|
+
/*
|
|
5584
|
+
* The half the screen cannot do, addressed to the thing that can.
|
|
5585
|
+
*
|
|
5586
|
+
* Choosing "keep it in an account" signs in and nothing more — no hosted
|
|
5587
|
+
* brain is created and no note is uploaded. The comparison table on the
|
|
5588
|
+
* first screen ticks "the same memory on another machine" for that column,
|
|
5589
|
+
* so leaving this unsaid ships a promise the flow does not keep.
|
|
5590
|
+
*
|
|
5591
|
+
* Not done automatically here on purpose: sending somebody's notes to a
|
|
5592
|
+
* server is a thing to ask about, not a side effect of an installer. The
|
|
5593
|
+
* agent is who asks, so the agent is who gets told.
|
|
5594
|
+
*/
|
|
5595
|
+
...choice.account === "cloud" && cfg.brain.brains.length > 0 ? {
|
|
5596
|
+
next: "Signed in, but the notes are still only on this machine \u2014 nothing was uploaded. `memgineering push` carries this brain up to the account, which is what makes it readable from another machine. ASK the user before running it: it sends their notes to a server."
|
|
5597
|
+
} : {},
|
|
4503
5598
|
...cfg.brain.brains.length === 0 ? {
|
|
4504
5599
|
// Same sentence the CLI ends on. Which folder holds someone's notes
|
|
4505
5600
|
// is the one decision neither surface makes for them, and `link` is
|
|
@@ -4539,6 +5634,15 @@ function readChoice(parsed, detected) {
|
|
|
4539
5634
|
const body = parsed ?? {};
|
|
4540
5635
|
const ids = Array.isArray(body.tools) ? body.tools.map(String) : [];
|
|
4541
5636
|
const known = detected.map((t) => t.id).join(", ");
|
|
5637
|
+
if (body.account !== "cloud" && body.account !== "local") {
|
|
5638
|
+
return {
|
|
5639
|
+
error: {
|
|
5640
|
+
code: "invalid_input",
|
|
5641
|
+
message: "account must be 'cloud' or 'local'",
|
|
5642
|
+
hint: `Received: ${JSON.stringify(body.account)}. This is the first question on the screen \u2014 whether the memory follows you across machines, or stays on this one.`
|
|
5643
|
+
}
|
|
5644
|
+
};
|
|
5645
|
+
}
|
|
4542
5646
|
if (ids.length === 0) {
|
|
4543
5647
|
return {
|
|
4544
5648
|
error: {
|
|
@@ -4582,7 +5686,8 @@ function readChoice(parsed, detected) {
|
|
|
4582
5686
|
// is not an error, it simply has nowhere to go — same rule as `--hook`.
|
|
4583
5687
|
hook: body.hook === true && tools.some((t) => t.hooks)
|
|
4584
5688
|
},
|
|
4585
|
-
brain: brain.value
|
|
5689
|
+
brain: brain.value,
|
|
5690
|
+
account: body.account
|
|
4586
5691
|
};
|
|
4587
5692
|
}
|
|
4588
5693
|
function readBrainChoice(raw) {
|
|
@@ -4619,8 +5724,8 @@ function readBrainChoice(raw) {
|
|
|
4619
5724
|
}
|
|
4620
5725
|
function expandHome(input) {
|
|
4621
5726
|
if (input === "~") return homedir3();
|
|
4622
|
-
if (input.startsWith("~/")) return
|
|
4623
|
-
return
|
|
5727
|
+
if (input.startsWith("~/")) return join17(homedir3(), input.slice(2));
|
|
5728
|
+
return isAbsolute3(input) ? input : resolve8(input);
|
|
4624
5729
|
}
|
|
4625
5730
|
function errorEnvelope(err) {
|
|
4626
5731
|
if (err instanceof MemgError) return { code: err.code, message: err.message, hint: err.hint };
|
|
@@ -4657,7 +5762,7 @@ async function readJsonBody(req, res) {
|
|
|
4657
5762
|
}
|
|
4658
5763
|
}
|
|
4659
5764
|
async function readBody(req) {
|
|
4660
|
-
return new Promise((
|
|
5765
|
+
return new Promise((resolve11, reject) => {
|
|
4661
5766
|
let size = 0;
|
|
4662
5767
|
let refused = false;
|
|
4663
5768
|
let chunks = [];
|
|
@@ -4667,14 +5772,14 @@ async function readBody(req) {
|
|
|
4667
5772
|
if (!refused) {
|
|
4668
5773
|
refused = true;
|
|
4669
5774
|
chunks = [];
|
|
4670
|
-
|
|
5775
|
+
resolve11(null);
|
|
4671
5776
|
}
|
|
4672
5777
|
return;
|
|
4673
5778
|
}
|
|
4674
5779
|
chunks.push(chunk);
|
|
4675
5780
|
});
|
|
4676
5781
|
req.once("end", () => {
|
|
4677
|
-
if (!refused)
|
|
5782
|
+
if (!refused) resolve11(Buffer.concat(chunks).toString("utf8"));
|
|
4678
5783
|
});
|
|
4679
5784
|
req.once("error", reject);
|
|
4680
5785
|
});
|
|
@@ -4719,45 +5824,26 @@ function sendHtml(res, html) {
|
|
|
4719
5824
|
});
|
|
4720
5825
|
res.end(html);
|
|
4721
5826
|
}
|
|
4722
|
-
|
|
4723
|
-
const { file, args } = browserOpener(url);
|
|
4724
|
-
return new Promise((resolve10) => {
|
|
4725
|
-
let settled = false;
|
|
4726
|
-
const finish = (opened) => {
|
|
4727
|
-
if (settled) return;
|
|
4728
|
-
settled = true;
|
|
4729
|
-
resolve10(opened);
|
|
4730
|
-
};
|
|
4731
|
-
const child = spawn(file, args, { detached: true, stdio: "ignore" });
|
|
4732
|
-
child.on("error", () => finish(false));
|
|
4733
|
-
child.on("exit", (code) => {
|
|
4734
|
-
if (code !== null && code !== 0) finish(false);
|
|
4735
|
-
});
|
|
4736
|
-
child.unref();
|
|
4737
|
-
setTimeout(() => finish(true), 300).unref();
|
|
4738
|
-
});
|
|
4739
|
-
}
|
|
4740
|
-
function browserOpener(url) {
|
|
4741
|
-
if (process.platform === "darwin") return { file: "open", args: [url] };
|
|
4742
|
-
if (process.platform === "win32") return { file: "cmd", args: ["/c", "start", "", url] };
|
|
4743
|
-
return { file: "xdg-open", args: [url] };
|
|
4744
|
-
}
|
|
4745
|
-
var IDLE_TIMEOUT_MS, MAX_LIFETIME_MS, APPLY_LINGER_MS, MAX_BODY_BYTES, MAX_CANDIDATES, PREVIEW_CAP, SYSTEM_ROOTS;
|
|
5827
|
+
var IDLE_TIMEOUT_MS, MAX_LIFETIME_MS, APPLY_LINGER_MS, MAX_BODY_BYTES, MAX_CANDIDATES, COUNT_CAP, PREVIEW_CAP, SYSTEM_ROOTS;
|
|
4746
5828
|
var init_setup_web = __esm({
|
|
4747
5829
|
"src/commands/setup-web.ts"() {
|
|
4748
5830
|
"use strict";
|
|
4749
5831
|
init_errors();
|
|
5832
|
+
init_open_browser();
|
|
5833
|
+
init_setup_signin();
|
|
5834
|
+
init_login();
|
|
4750
5835
|
init_config();
|
|
4751
5836
|
init_ui();
|
|
4752
5837
|
init_init();
|
|
4753
5838
|
init_link();
|
|
4754
5839
|
init_setup();
|
|
4755
5840
|
init_setup_web_page();
|
|
4756
|
-
IDLE_TIMEOUT_MS =
|
|
5841
|
+
IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
4757
5842
|
MAX_LIFETIME_MS = 60 * 60 * 1e3;
|
|
4758
5843
|
APPLY_LINGER_MS = 1e3;
|
|
4759
5844
|
MAX_BODY_BYTES = 64 * 1024;
|
|
4760
5845
|
MAX_CANDIDATES = 8;
|
|
5846
|
+
COUNT_CAP = 40;
|
|
4761
5847
|
PREVIEW_CAP = 5e3;
|
|
4762
5848
|
SYSTEM_ROOTS = [
|
|
4763
5849
|
"/",
|
|
@@ -4776,14 +5862,17 @@ var init_setup_web = __esm({
|
|
|
4776
5862
|
});
|
|
4777
5863
|
|
|
4778
5864
|
// src/commands/setup.ts
|
|
4779
|
-
import { access, mkdir as
|
|
5865
|
+
import { access, mkdir as mkdir12, readFile as readFile18 } from "fs/promises";
|
|
4780
5866
|
import { homedir as homedir4 } from "os";
|
|
4781
|
-
import { dirname as
|
|
5867
|
+
import { dirname as dirname4, join as join18 } from "path";
|
|
4782
5868
|
import { fileURLToPath } from "url";
|
|
4783
|
-
import { Command as
|
|
4784
|
-
import
|
|
5869
|
+
import { Command as Command18 } from "commander";
|
|
5870
|
+
import prompts3 from "prompts";
|
|
4785
5871
|
function setupCommand() {
|
|
4786
|
-
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) => {
|
|
4787
5876
|
const detected = await detectTools();
|
|
4788
5877
|
if (detected.length === 0) {
|
|
4789
5878
|
throw memgError(
|
|
@@ -4794,9 +5883,23 @@ function setupCommand() {
|
|
|
4794
5883
|
}
|
|
4795
5884
|
if (opts.web) {
|
|
4796
5885
|
const { runSetupWeb: runSetupWeb2 } = await Promise.resolve().then(() => (init_setup_web(), setup_web_exports));
|
|
4797
|
-
await runSetupWeb2({
|
|
5886
|
+
await runSetupWeb2({
|
|
5887
|
+
detected,
|
|
5888
|
+
printUrl: opts.printUrl === true,
|
|
5889
|
+
...opts.login === void 0 ? {} : { preselectAccount: opts.login ? "cloud" : "local" }
|
|
5890
|
+
});
|
|
4798
5891
|
return;
|
|
4799
5892
|
}
|
|
5893
|
+
const signIn = await runSignInStep({
|
|
5894
|
+
...opts.login === void 0 ? {} : { login: opts.login },
|
|
5895
|
+
// A real terminal, not the `--human` flag. Asking for `--human` says
|
|
5896
|
+
// which mode you want; it does not conjure a screen to draw a prompt
|
|
5897
|
+
// on, and prompting without one hangs until something kills it. The
|
|
5898
|
+
// refusal for that combination lives in `resolveMode`, below.
|
|
5899
|
+
interactive: isInteractive() && opts.agent !== true,
|
|
5900
|
+
browser: opts.browser !== false,
|
|
5901
|
+
dryRun: opts.dryRun === true
|
|
5902
|
+
});
|
|
4800
5903
|
const mode = await resolveMode(opts);
|
|
4801
5904
|
const choice = mode.kind === "human" ? await askHuman(detected, opts) : fromFlags(detected, opts);
|
|
4802
5905
|
const defaultsTaken = !mode.chosen ? [
|
|
@@ -4813,6 +5916,7 @@ function setupCommand() {
|
|
|
4813
5916
|
printDual({
|
|
4814
5917
|
json: {
|
|
4815
5918
|
mode: mode.kind,
|
|
5919
|
+
...signInStepJson(signIn),
|
|
4816
5920
|
defaults_used: defaultsTaken,
|
|
4817
5921
|
dry_run: opts.dryRun === true,
|
|
4818
5922
|
tools: choice.tools.map((t) => t.id),
|
|
@@ -4827,11 +5931,13 @@ function setupCommand() {
|
|
|
4827
5931
|
human: () => {
|
|
4828
5932
|
if (opts.silent && changed.length === 0) return;
|
|
4829
5933
|
if (opts.dryRun) {
|
|
5934
|
+
printSignInStep(signIn);
|
|
4830
5935
|
printHuman(`Would change ${changed.length} file(s):
|
|
4831
5936
|
`);
|
|
4832
5937
|
for (const r of changed) printHuman(c.gray(` ${r.tool} \xB7 ${r.what}`));
|
|
4833
5938
|
return;
|
|
4834
5939
|
}
|
|
5940
|
+
printSignInStep(signIn);
|
|
4835
5941
|
printHuman(`Registered with ${choice.tools.map((t) => t.label).join(", ")}.
|
|
4836
5942
|
`);
|
|
4837
5943
|
printHuman(c.gray(` auto-update: ${choice.autoUpdate}`));
|
|
@@ -4867,7 +5973,7 @@ function setupCommand() {
|
|
|
4867
5973
|
}
|
|
4868
5974
|
});
|
|
4869
5975
|
if (mode.kind === "human" && brains.length === 0 && !opts.dryRun && isInteractive()) {
|
|
4870
|
-
const { path } = await
|
|
5976
|
+
const { path } = await prompts3({
|
|
4871
5977
|
type: "text",
|
|
4872
5978
|
name: "path",
|
|
4873
5979
|
message: "Path to your notes folder (blank to skip)"
|
|
@@ -4887,15 +5993,15 @@ async function applySetup(choice, dryRun) {
|
|
|
4887
5993
|
results.push({
|
|
4888
5994
|
tool: tool.label,
|
|
4889
5995
|
what: "guide",
|
|
4890
|
-
result: await injectBlock(
|
|
5996
|
+
result: await injectBlock(join18(homedir4(), tool.hubFile), hub, dryRun)
|
|
4891
5997
|
});
|
|
4892
5998
|
for (const skill of BRAND.subSkills) {
|
|
4893
|
-
const content = await loadAsset(
|
|
5999
|
+
const content = await loadAsset(join18(skill, "SKILL.md"));
|
|
4894
6000
|
results.push({
|
|
4895
6001
|
tool: tool.label,
|
|
4896
6002
|
what: skill,
|
|
4897
6003
|
result: await writeIfDifferent(
|
|
4898
|
-
|
|
6004
|
+
join18(homedir4(), tool.skillsDir, skill, "SKILL.md"),
|
|
4899
6005
|
content,
|
|
4900
6006
|
dryRun
|
|
4901
6007
|
)
|
|
@@ -4931,7 +6037,7 @@ async function resolveMode(opts) {
|
|
|
4931
6037
|
return { kind: "human", chosen: true };
|
|
4932
6038
|
}
|
|
4933
6039
|
if (!isInteractive() || isJsonMode()) return { kind: "agent", chosen: false };
|
|
4934
|
-
const { mode } = await
|
|
6040
|
+
const { mode } = await prompts3({
|
|
4935
6041
|
type: "select",
|
|
4936
6042
|
name: "mode",
|
|
4937
6043
|
message: "How would you like to set this up?",
|
|
@@ -4951,7 +6057,7 @@ async function detectTools() {
|
|
|
4951
6057
|
const home = homedir4();
|
|
4952
6058
|
const found = [];
|
|
4953
6059
|
for (const tool of TOOLS) {
|
|
4954
|
-
if (await
|
|
6060
|
+
if (await exists2(join18(home, tool.dir))) found.push(tool);
|
|
4955
6061
|
}
|
|
4956
6062
|
return found;
|
|
4957
6063
|
}
|
|
@@ -4988,7 +6094,7 @@ async function askHuman(detected, opts) {
|
|
|
4988
6094
|
printHuman(c.bold("\nmemgineering \u2014 setup\n"));
|
|
4989
6095
|
printHuman(c.gray(" Found: ") + detected.map((t) => t.label).join(", "));
|
|
4990
6096
|
printHuman("");
|
|
4991
|
-
const answers = await
|
|
6097
|
+
const answers = await prompts3([
|
|
4992
6098
|
{
|
|
4993
6099
|
type: "multiselect",
|
|
4994
6100
|
name: "tools",
|
|
@@ -5028,10 +6134,10 @@ async function injectBlock(filePath, content, dryRun) {
|
|
|
5028
6134
|
const block = `${BRAND.hubMarkerStart}
|
|
5029
6135
|
${content.trimEnd()}
|
|
5030
6136
|
${BRAND.hubMarkerEnd}`;
|
|
5031
|
-
const existing = await
|
|
6137
|
+
const existing = await readFile18(filePath, "utf8").catch(() => null);
|
|
5032
6138
|
if (existing === null) {
|
|
5033
6139
|
if (!dryRun) {
|
|
5034
|
-
await
|
|
6140
|
+
await mkdir12(dirname4(filePath), { recursive: true });
|
|
5035
6141
|
await writeThenRename(filePath, `${block}
|
|
5036
6142
|
`);
|
|
5037
6143
|
}
|
|
@@ -5056,17 +6162,17 @@ ${block}
|
|
|
5056
6162
|
return "injected";
|
|
5057
6163
|
}
|
|
5058
6164
|
async function writeIfDifferent(filePath, content, dryRun) {
|
|
5059
|
-
const existing = await
|
|
6165
|
+
const existing = await readFile18(filePath, "utf8").catch(() => null);
|
|
5060
6166
|
if (existing === content) return "unchanged";
|
|
5061
6167
|
if (!dryRun) {
|
|
5062
|
-
await
|
|
6168
|
+
await mkdir12(dirname4(filePath), { recursive: true });
|
|
5063
6169
|
await writeThenRename(filePath, content);
|
|
5064
6170
|
}
|
|
5065
6171
|
return existing === null ? "injected" : "updated";
|
|
5066
6172
|
}
|
|
5067
6173
|
async function installHook(dryRun) {
|
|
5068
|
-
const settingsPath =
|
|
5069
|
-
const raw = await
|
|
6174
|
+
const settingsPath = join18(homedir4(), ".claude", "settings.json");
|
|
6175
|
+
const raw = await readFile18(settingsPath, "utf8").catch(() => null);
|
|
5070
6176
|
let settings = {};
|
|
5071
6177
|
if (raw !== null) {
|
|
5072
6178
|
try {
|
|
@@ -5089,14 +6195,14 @@ async function installHook(dryRun) {
|
|
|
5089
6195
|
sessionStart.push({ hooks: [{ type: "command", command }] });
|
|
5090
6196
|
const next = { ...settings, hooks: { ...hooks, SessionStart: sessionStart } };
|
|
5091
6197
|
if (!dryRun) {
|
|
5092
|
-
await
|
|
6198
|
+
await mkdir12(dirname4(settingsPath), { recursive: true });
|
|
5093
6199
|
await writeThenRename(settingsPath, `${JSON.stringify(next, null, 2)}
|
|
5094
6200
|
`);
|
|
5095
6201
|
}
|
|
5096
6202
|
return raw === null ? "injected" : "updated";
|
|
5097
6203
|
}
|
|
5098
6204
|
async function loadAsset(relPath) {
|
|
5099
|
-
const raw = await
|
|
6205
|
+
const raw = await readFile18(join18(await assetRoot(), relPath), "utf8");
|
|
5100
6206
|
if (relPath === BRAND.hubFileName && raw.startsWith("---")) {
|
|
5101
6207
|
const end = raw.indexOf("\n---", 4);
|
|
5102
6208
|
if (end !== -1) return raw.slice(end + 4).trimStart();
|
|
@@ -5104,14 +6210,14 @@ async function loadAsset(relPath) {
|
|
|
5104
6210
|
return raw;
|
|
5105
6211
|
}
|
|
5106
6212
|
async function assetRoot() {
|
|
5107
|
-
const here =
|
|
5108
|
-
const bundled =
|
|
5109
|
-
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;
|
|
5110
6216
|
let dir = here;
|
|
5111
6217
|
for (; ; ) {
|
|
5112
|
-
const candidate =
|
|
5113
|
-
if (await
|
|
5114
|
-
const parent =
|
|
6218
|
+
const candidate = join18(dir, "packages", "skills");
|
|
6219
|
+
if (await exists2(join18(candidate, BRAND.hubFileName))) return candidate;
|
|
6220
|
+
const parent = dirname4(dir);
|
|
5115
6221
|
if (parent === dir) break;
|
|
5116
6222
|
dir = parent;
|
|
5117
6223
|
}
|
|
@@ -5121,7 +6227,7 @@ async function assetRoot() {
|
|
|
5121
6227
|
"Reinstall with `npm i -g memgineering`. If you are running from a checkout, build first: `pnpm -F memgineering build`."
|
|
5122
6228
|
);
|
|
5123
6229
|
}
|
|
5124
|
-
async function
|
|
6230
|
+
async function exists2(path) {
|
|
5125
6231
|
try {
|
|
5126
6232
|
await access(path);
|
|
5127
6233
|
return true;
|
|
@@ -5136,6 +6242,7 @@ var init_setup = __esm({
|
|
|
5136
6242
|
init_atomic();
|
|
5137
6243
|
init_brand();
|
|
5138
6244
|
init_errors();
|
|
6245
|
+
init_setup_signin();
|
|
5139
6246
|
init_config();
|
|
5140
6247
|
init_ui();
|
|
5141
6248
|
TOOLS = [
|
|
@@ -5168,7 +6275,7 @@ var init_setup = __esm({
|
|
|
5168
6275
|
});
|
|
5169
6276
|
|
|
5170
6277
|
// src/program.ts
|
|
5171
|
-
import { Command as
|
|
6278
|
+
import { Command as Command23 } from "commander";
|
|
5172
6279
|
|
|
5173
6280
|
// src/help.ts
|
|
5174
6281
|
init_ui();
|
|
@@ -5189,6 +6296,11 @@ var COMMAND_GROUPS = [
|
|
|
5189
6296
|
blurb: "occasional \u2014 correct what is remembered and what is read",
|
|
5190
6297
|
commands: ["retire", "unretire", "exclude", "unexclude"]
|
|
5191
6298
|
},
|
|
6299
|
+
{
|
|
6300
|
+
title: "ACCOUNT",
|
|
6301
|
+
blurb: "optional \u2014 connect a hosted brain; everything above works without it",
|
|
6302
|
+
commands: ["login", "logout", "whoami", "push", "pull"]
|
|
6303
|
+
},
|
|
5192
6304
|
{
|
|
5193
6305
|
title: "SYSTEM",
|
|
5194
6306
|
blurb: "install into your agents, and stay current",
|
|
@@ -5199,7 +6311,8 @@ var defaultFormatHelp = Help.prototype.formatHelp;
|
|
|
5199
6311
|
var ENV_ROWS = [
|
|
5200
6312
|
["MEMGINEERING_HOME=<dir>", "state + derived index (default: ~/.memgineering)"],
|
|
5201
6313
|
["MEMGINEERING_JSON=1", "force --json framing without the flag"],
|
|
5202
|
-
["MEMGINEERING_NO_UPDATE=1", "skip the update check for this run"]
|
|
6314
|
+
["MEMGINEERING_NO_UPDATE=1", "skip the update check for this run"],
|
|
6315
|
+
["MEMGINEERING_API_URL=<url>", "hosted brain server (default: https://api.memgineering.com)"]
|
|
5203
6316
|
];
|
|
5204
6317
|
function commandLabel(sub) {
|
|
5205
6318
|
const args = sub.registeredArguments.map((a) => a.required ? `<${a.name()}>` : `[${a.name()}]`).join(" ");
|
|
@@ -5329,7 +6442,16 @@ var OpVerbSchema = z7.enum([
|
|
|
5329
6442
|
"retire",
|
|
5330
6443
|
"unretire",
|
|
5331
6444
|
"exclude",
|
|
5332
|
-
"unexclude"
|
|
6445
|
+
"unexclude",
|
|
6446
|
+
/**
|
|
6447
|
+
* Written only by the hosted brain, when a note is uploaded at a chosen path.
|
|
6448
|
+
*
|
|
6449
|
+
* This CLI never produces one. It is here so that a ledger which came from a
|
|
6450
|
+
* hosted brain PARSES — a downloaded history containing a verb this schema
|
|
6451
|
+
* rejects would fail to load at all, taking the readable ops down with the
|
|
6452
|
+
* one it did not recognise.
|
|
6453
|
+
*/
|
|
6454
|
+
"import"
|
|
5333
6455
|
]);
|
|
5334
6456
|
var OpRecordSchema = z7.object({
|
|
5335
6457
|
op_id: z7.string().min(1),
|
|
@@ -5678,18 +6800,18 @@ async function commit2(root, base, next) {
|
|
|
5678
6800
|
function excludesPath(content, relPath) {
|
|
5679
6801
|
return isExcluded(normalizeName(relPath), false, parseIgnoreFile(content));
|
|
5680
6802
|
}
|
|
5681
|
-
|
|
6803
|
+
function planAddIgnoreEntry(content, relPath) {
|
|
5682
6804
|
const entry = normalizeName(relPath);
|
|
5683
|
-
if (excludesPath(
|
|
5684
|
-
return {
|
|
6805
|
+
if (excludesPath(content, relPath)) {
|
|
6806
|
+
return { content, changed: false };
|
|
5685
6807
|
}
|
|
5686
|
-
const needsNewline =
|
|
5687
|
-
const next = `${
|
|
6808
|
+
const needsNewline = content !== "" && !content.endsWith("\n");
|
|
6809
|
+
const next = `${content}${needsNewline ? "\n" : ""}${entry}
|
|
5688
6810
|
`;
|
|
5689
6811
|
const parsed = parseIgnoreFile(next);
|
|
5690
6812
|
if (parsed.unsupported.includes(entry)) {
|
|
5691
6813
|
throw Object.assign(new Error(`${FILE} cannot express a rule for this path: ${relPath}`), {
|
|
5692
|
-
hint: "Paths containing `[` or a backslash are outside what this ignore parser translates. Rename the file, or add the rule by hand and check `
|
|
6814
|
+
hint: "Paths containing `[` or a backslash are outside what this ignore parser translates. Rename the file, or add the rule by hand and check `memgineering recall` no longer returns it. Nothing was written."
|
|
5693
6815
|
});
|
|
5694
6816
|
}
|
|
5695
6817
|
if (!excludesPath(next, relPath)) {
|
|
@@ -5697,24 +6819,32 @@ async function addIgnoreEntry(root, relPath, base) {
|
|
|
5697
6819
|
hint: `This is a defect in the editor, not in your ${FILE}. Nothing was written.`
|
|
5698
6820
|
});
|
|
5699
6821
|
}
|
|
5700
|
-
assertNothingLost(
|
|
5701
|
-
return
|
|
6822
|
+
assertNothingLost(content, next, "excluding");
|
|
6823
|
+
return { content: next, changed: true };
|
|
5702
6824
|
}
|
|
5703
|
-
|
|
6825
|
+
function planRemoveIgnoreEntry(content, relPath) {
|
|
5704
6826
|
const entry = normalizeName(relPath);
|
|
5705
|
-
const lines2 =
|
|
6827
|
+
const lines2 = content.split("\n");
|
|
5706
6828
|
const kept = lines2.filter((line) => normalizeName(line.trim()) !== entry);
|
|
5707
|
-
if (kept.length === lines2.length) {
|
|
5708
|
-
return { ok: true, content: base.content, changed: false };
|
|
5709
|
-
}
|
|
6829
|
+
if (kept.length === lines2.length) return { content, changed: false };
|
|
5710
6830
|
const next = kept.join("\n");
|
|
5711
6831
|
if (excludesPath(next, relPath)) {
|
|
5712
6832
|
throw Object.assign(new Error(`${relPath} is still excluded after removing its line`), {
|
|
5713
6833
|
hint: `Another rule in ${FILE} or .gitignore also covers this path. Remove that one by hand \u2014 this command will not delete a rule you did not name. Nothing was written.`
|
|
5714
6834
|
});
|
|
5715
6835
|
}
|
|
5716
|
-
assertNothingLost(next,
|
|
5717
|
-
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);
|
|
5718
6848
|
}
|
|
5719
6849
|
|
|
5720
6850
|
// ../../packages/storage-adapter/src/local-file/cas-adapter.ts
|
|
@@ -6073,6 +7203,124 @@ async function readLock(path) {
|
|
|
6073
7203
|
|
|
6074
7204
|
// src/commands/curation.ts
|
|
6075
7205
|
init_index_build();
|
|
7206
|
+
|
|
7207
|
+
// src/lib/cloud-brain.ts
|
|
7208
|
+
init_api_client();
|
|
7209
|
+
async function openCloudBrain(target) {
|
|
7210
|
+
const credentials = await requireCredentials(target.apiUrl);
|
|
7211
|
+
return {
|
|
7212
|
+
brainId: target.brainId,
|
|
7213
|
+
name: target.name,
|
|
7214
|
+
token: credentials.token,
|
|
7215
|
+
apiUrl: target.apiUrl
|
|
7216
|
+
};
|
|
7217
|
+
}
|
|
7218
|
+
var call = async (brain, method, path, body) => (await apiRequest({
|
|
7219
|
+
method,
|
|
7220
|
+
path: `/v1/brains/${brain.brainId}${path}`,
|
|
7221
|
+
token: brain.token,
|
|
7222
|
+
baseUrl: brain.apiUrl,
|
|
7223
|
+
...body === void 0 ? {} : { body }
|
|
7224
|
+
})).body;
|
|
7225
|
+
var cloudRecall = (brain, args) => call(brain, "POST", "/recall", {
|
|
7226
|
+
query: args.query,
|
|
7227
|
+
...args.limit === void 0 ? {} : { limit: args.limit },
|
|
7228
|
+
...args.scope === void 0 ? {} : { scope: args.scope }
|
|
7229
|
+
});
|
|
7230
|
+
var cloudOpen = (brain, ref) => call(brain, "POST", "/open", { ref });
|
|
7231
|
+
var cloudEvidence = (brain, ref) => call(brain, "POST", "/evidence", { ref });
|
|
7232
|
+
var cloudRemember = (brain, args) => call(brain, "POST", "/remember", {
|
|
7233
|
+
text: args.text,
|
|
7234
|
+
...args.scope === void 0 ? {} : { scope: args.scope },
|
|
7235
|
+
...args.reason === void 0 ? {} : { reason: args.reason }
|
|
7236
|
+
});
|
|
7237
|
+
var cloudUndo = (brain, args) => call(brain, "POST", "/undo", {
|
|
7238
|
+
...args.opId === void 0 ? {} : { op_id: args.opId },
|
|
7239
|
+
...args.reason === void 0 ? {} : { reason: args.reason }
|
|
7240
|
+
});
|
|
7241
|
+
var cloudLog = (brain, args = {}) => {
|
|
7242
|
+
const query = new URLSearchParams();
|
|
7243
|
+
if (args.limit !== void 0) query.set("limit", String(args.limit));
|
|
7244
|
+
if (args.verbose === true) query.set("verbose", "true");
|
|
7245
|
+
const suffix = query.toString();
|
|
7246
|
+
return call(brain, "GET", `/log${suffix ? `?${suffix}` : ""}`);
|
|
7247
|
+
};
|
|
7248
|
+
var cloudPutNote = (brain, args) => call(brain, "POST", "/notes", args);
|
|
7249
|
+
var cloudListNotes = (brain, args = {}) => {
|
|
7250
|
+
const query = new URLSearchParams();
|
|
7251
|
+
if (args.cursor !== void 0) query.set("cursor", args.cursor);
|
|
7252
|
+
if (args.limit !== void 0) query.set("limit", String(args.limit));
|
|
7253
|
+
if (args.content === true) query.set("content", "1");
|
|
7254
|
+
const suffix = query.toString();
|
|
7255
|
+
return call(brain, "GET", `/notes${suffix ? `?${suffix}` : ""}`);
|
|
7256
|
+
};
|
|
7257
|
+
var cloudRevise = (brain, args) => call(brain, "POST", "/revise", {
|
|
7258
|
+
ref: args.ref,
|
|
7259
|
+
claim: args.claim,
|
|
7260
|
+
...args.summary === void 0 ? {} : { summary: args.summary },
|
|
7261
|
+
...args.title === void 0 ? {} : { title: args.title },
|
|
7262
|
+
...args.validFrom === void 0 ? {} : { valid_from: args.validFrom },
|
|
7263
|
+
...args.action === void 0 ? {} : { action: args.action },
|
|
7264
|
+
...args.contradicts === void 0 ? {} : { contradicts: [...args.contradicts] },
|
|
7265
|
+
...args.reason === void 0 ? {} : { reason: args.reason },
|
|
7266
|
+
...args.because === void 0 ? {} : { because: args.because },
|
|
7267
|
+
...args.dryRun === true ? { dry_run: true } : {}
|
|
7268
|
+
});
|
|
7269
|
+
var cloudLifecycle = (brain, action, args) => call(brain, "POST", `/${action}`, {
|
|
7270
|
+
ref: args.ref,
|
|
7271
|
+
...args.reason === void 0 ? {} : { reason: args.reason }
|
|
7272
|
+
});
|
|
7273
|
+
var cloudExclude = (brain, mode, args) => call(brain, "POST", `/${mode}`, {
|
|
7274
|
+
path: args.path,
|
|
7275
|
+
...args.reason === void 0 ? {} : { reason: args.reason }
|
|
7276
|
+
});
|
|
7277
|
+
var cloudResurface = (brain, args = {}) => call(brain, "POST", "/resurface", {
|
|
7278
|
+
...args.limit === void 0 ? {} : { limit: args.limit }
|
|
7279
|
+
});
|
|
7280
|
+
async function listCloudBrains(token, baseUrl) {
|
|
7281
|
+
const res = await apiRequest({
|
|
7282
|
+
method: "GET",
|
|
7283
|
+
path: "/v1/brains",
|
|
7284
|
+
token,
|
|
7285
|
+
baseUrl
|
|
7286
|
+
});
|
|
7287
|
+
return res.body?.brains ?? [];
|
|
7288
|
+
}
|
|
7289
|
+
async function createCloudBrain(token, baseUrl, name) {
|
|
7290
|
+
const res = await apiRequest({
|
|
7291
|
+
method: "POST",
|
|
7292
|
+
path: "/v1/brains",
|
|
7293
|
+
token,
|
|
7294
|
+
baseUrl,
|
|
7295
|
+
body: { name }
|
|
7296
|
+
});
|
|
7297
|
+
return res.body;
|
|
7298
|
+
}
|
|
7299
|
+
|
|
7300
|
+
// src/lib/target.ts
|
|
7301
|
+
init_errors();
|
|
7302
|
+
init_api_client();
|
|
7303
|
+
init_config();
|
|
7304
|
+
init_vault();
|
|
7305
|
+
async function resolveTarget(opts = {}) {
|
|
7306
|
+
if (opts.local !== true) {
|
|
7307
|
+
const cloud = await cloudTarget();
|
|
7308
|
+
if (cloud) return cloud;
|
|
7309
|
+
}
|
|
7310
|
+
const { brain, via } = await resolveBrain(opts.vault);
|
|
7311
|
+
return { kind: "local", brain, via };
|
|
7312
|
+
}
|
|
7313
|
+
async function cloudTarget() {
|
|
7314
|
+
const base = apiUrl();
|
|
7315
|
+
const config = await loadConfig();
|
|
7316
|
+
const cloud = config.brain.cloud;
|
|
7317
|
+
if (!cloud || cloud.api_url !== base) return null;
|
|
7318
|
+
const credentials = await currentCredentials(base);
|
|
7319
|
+
if (!credentials) return null;
|
|
7320
|
+
return { kind: "cloud", brainId: cloud.id, name: cloud.name, apiUrl: base };
|
|
7321
|
+
}
|
|
7322
|
+
|
|
7323
|
+
// src/commands/curation.ts
|
|
6076
7324
|
init_vault();
|
|
6077
7325
|
init_ui();
|
|
6078
7326
|
|
|
@@ -6082,28 +7330,28 @@ init_errors();
|
|
|
6082
7330
|
import { Command as Command4 } from "commander";
|
|
6083
7331
|
|
|
6084
7332
|
// src/lib/events.ts
|
|
6085
|
-
import { appendFile as appendFile2, mkdir as
|
|
6086
|
-
import { join as
|
|
6087
|
-
import { z as
|
|
6088
|
-
var EventSchema =
|
|
6089
|
-
ts:
|
|
6090
|
-
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"]),
|
|
6091
7339
|
/** Absent for resurface, which is the point of it. */
|
|
6092
|
-
query:
|
|
7340
|
+
query: z10.string().nullable().default(null),
|
|
6093
7341
|
/** Where the caller was. The signal resurface ranks by. */
|
|
6094
|
-
context_dir:
|
|
6095
|
-
detail:
|
|
6096
|
-
limit:
|
|
7342
|
+
context_dir: z10.string(),
|
|
7343
|
+
detail: z10.string(),
|
|
7344
|
+
limit: z10.number().int().positive().nullable().default(null),
|
|
6097
7345
|
/** Ids the call returned — the denominator for "was any of this used". */
|
|
6098
|
-
returned_ids:
|
|
7346
|
+
returned_ids: z10.array(z10.string()).default([]),
|
|
6099
7347
|
/** The one that got opened, when this event is an open. */
|
|
6100
|
-
opened_id:
|
|
7348
|
+
opened_id: z10.string().nullable().default(null)
|
|
6101
7349
|
});
|
|
6102
7350
|
function eventsDir(vaultRoot) {
|
|
6103
7351
|
return stateDirFor(vaultRoot);
|
|
6104
7352
|
}
|
|
6105
7353
|
function eventsFile(vaultRoot, ts) {
|
|
6106
|
-
return
|
|
7354
|
+
return join13(eventsDir(vaultRoot), `events-${ts.slice(0, 7)}.jsonl`);
|
|
6107
7355
|
}
|
|
6108
7356
|
async function recordEvent(vaultRoot, event) {
|
|
6109
7357
|
try {
|
|
@@ -6118,7 +7366,7 @@ async function recordEvent(vaultRoot, event) {
|
|
|
6118
7366
|
returned_ids: [...event.returnedIds ?? []],
|
|
6119
7367
|
opened_id: event.openedId ?? null
|
|
6120
7368
|
});
|
|
6121
|
-
await
|
|
7369
|
+
await mkdir9(eventsDir(vaultRoot), { recursive: true });
|
|
6122
7370
|
await appendFile2(eventsFile(vaultRoot, ts), `${JSON.stringify(parsed)}
|
|
6123
7371
|
`, {
|
|
6124
7372
|
encoding: "utf8",
|
|
@@ -6136,7 +7384,7 @@ async function readEvents(vaultRoot) {
|
|
|
6136
7384
|
}
|
|
6137
7385
|
const events = [];
|
|
6138
7386
|
for (const file of files2) {
|
|
6139
|
-
const raw = await
|
|
7387
|
+
const raw = await readFile14(join13(eventsDir(vaultRoot), file), "utf8");
|
|
6140
7388
|
for (const line of raw.split("\n")) {
|
|
6141
7389
|
if (line.trim() === "") continue;
|
|
6142
7390
|
try {
|
|
@@ -6202,7 +7450,7 @@ var BASE_FILES = [
|
|
|
6202
7450
|
}
|
|
6203
7451
|
];
|
|
6204
7452
|
var HUB_PATH = "00_HUB/HUB.md";
|
|
6205
|
-
function
|
|
7453
|
+
function bodyOf2(content) {
|
|
6206
7454
|
const stripped = content.replace(/^/, "");
|
|
6207
7455
|
const fence = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
|
|
6208
7456
|
return (fence ? stripped.slice(fence[0].length) : stripped).trim();
|
|
@@ -6210,8 +7458,8 @@ function bodyOf(content) {
|
|
|
6210
7458
|
function bodyStillTemplate(path, content) {
|
|
6211
7459
|
const template = files("brain")[path];
|
|
6212
7460
|
if (template === void 0) return false;
|
|
6213
|
-
const body =
|
|
6214
|
-
return body !== "" && body ===
|
|
7461
|
+
const body = bodyOf2(content);
|
|
7462
|
+
return body !== "" && body === bodyOf2(template);
|
|
6215
7463
|
}
|
|
6216
7464
|
async function scaffoldingAmong(adapter, notes) {
|
|
6217
7465
|
const basePaths = new Set(BASE_FILES.map((f) => f.path));
|
|
@@ -6287,112 +7535,147 @@ async function assembleStage(adapter, entry, detail, sectionName) {
|
|
|
6287
7535
|
init_vault();
|
|
6288
7536
|
init_ui();
|
|
6289
7537
|
function openCommand() {
|
|
6290
|
-
return new Command4("open").description("open one memory, as deeply as you ask").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--detail <level>", "card | summary | chunks | full (default: card)").option("--section <name>", "one section (implies --detail chunks)").action(
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
7538
|
+
return new Command4("open").description("open one memory, as deeply as you ask").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option("--detail <level>", "card | summary | chunks | full (default: card)").option("--section <name>", "one section (implies --detail chunks)").action(
|
|
7539
|
+
async (ref, opts) => {
|
|
7540
|
+
const detail = parseDetail(opts.detail, opts.section ? "chunks" : "card");
|
|
7541
|
+
const target = await resolveTarget({
|
|
7542
|
+
...opts.local === void 0 ? {} : { local: opts.local },
|
|
7543
|
+
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
7544
|
+
});
|
|
7545
|
+
if (target.kind === "cloud") {
|
|
7546
|
+
const cloud = await openCloudBrain(target);
|
|
7547
|
+
const found = await cloudOpen(cloud, ref);
|
|
7548
|
+
printDual({
|
|
7549
|
+
json: {
|
|
7550
|
+
id: found.id,
|
|
7551
|
+
handle: found.handle,
|
|
7552
|
+
path: found.path,
|
|
7553
|
+
revision: found.revision,
|
|
7554
|
+
detail,
|
|
7555
|
+
memory: found.memory,
|
|
7556
|
+
claims: found.claims,
|
|
7557
|
+
relations: found.relations,
|
|
7558
|
+
...detail === "card" ? {} : { body: found.body },
|
|
7559
|
+
brain: { hosted: true, name: target.name }
|
|
7560
|
+
},
|
|
7561
|
+
human: () => {
|
|
7562
|
+
printHuman(`## ${String(found.memory["title"] ?? found.path)}
|
|
7563
|
+
`);
|
|
7564
|
+
const summary = found.memory["summary"];
|
|
7565
|
+
if (typeof summary === "string" && summary !== "") printHuman(`> ${summary}
|
|
7566
|
+
`);
|
|
7567
|
+
printHuman(c.gray(`${found.path} \xB7 ${target.name}`));
|
|
7568
|
+
if (detail !== "card" && found.body !== "") printHuman(`
|
|
7569
|
+
${found.body}`);
|
|
7570
|
+
}
|
|
7571
|
+
});
|
|
7572
|
+
return;
|
|
7573
|
+
}
|
|
7574
|
+
const brain = target.brain;
|
|
7575
|
+
const adapter = await openBrainOrExplain(brain.root);
|
|
7576
|
+
const built = await openIndex(adapter, brain.root);
|
|
7577
|
+
const entry = resolveRef(built.entries, ref);
|
|
7578
|
+
await recordEvent(brain.root, {
|
|
7579
|
+
verb: "open",
|
|
6315
7580
|
detail,
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
7581
|
+
returnedIds: [entry.memory.id],
|
|
7582
|
+
openedId: entry.memory.id
|
|
7583
|
+
});
|
|
7584
|
+
const stage = await assembleStage(adapter, entry, detail, opts.section);
|
|
7585
|
+
const claims = entry.claims;
|
|
7586
|
+
const now = /* @__PURE__ */ new Date();
|
|
7587
|
+
const handle2 = displayHandle(
|
|
7588
|
+
entry.memory.id,
|
|
7589
|
+
built.entries.map((e) => e.memory.id)
|
|
7590
|
+
);
|
|
7591
|
+
const scaffolding = (await scaffoldingAmong(adapter, [{ path: entry.path, title: entry.memory.title }])).has(entry.path);
|
|
7592
|
+
printDual({
|
|
7593
|
+
json: {
|
|
7594
|
+
id: entry.memory.id,
|
|
7595
|
+
handle: handle2,
|
|
7596
|
+
path: entry.path,
|
|
7597
|
+
detail,
|
|
7598
|
+
scaffolding,
|
|
7599
|
+
memory: entry.memory,
|
|
7600
|
+
claims,
|
|
7601
|
+
relations: entry.relations,
|
|
7602
|
+
...stage
|
|
7603
|
+
},
|
|
7604
|
+
human: () => {
|
|
7605
|
+
printHuman(`## ${entry.memory.title}
|
|
6324
7606
|
`);
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
7607
|
+
if (scaffolding) {
|
|
7608
|
+
printHuman(
|
|
7609
|
+
c.yellow("not filled in yet") + c.gray(" \u2014 still the template `init` wrote")
|
|
7610
|
+
);
|
|
7611
|
+
} else {
|
|
7612
|
+
printHuman(
|
|
7613
|
+
`${confidenceOf(entry.memory, claims)} \xB7 ${freshnessOf(entry.memory, claims, now)} \xB7 ${entry.memory.provenance.mapping}`
|
|
7614
|
+
);
|
|
7615
|
+
}
|
|
7616
|
+
printHuman(`
|
|
6335
7617
|
\`id: ${entry.memory.id}\` \xB7 \`${entry.path}\`
|
|
6336
7618
|
`);
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
7619
|
+
if (entry.memory.summary) printHuman(quoteBlock(entry.memory.summary) + "\n");
|
|
7620
|
+
for (const claim of claims) {
|
|
7621
|
+
printHuman(`- ${claim.text}`);
|
|
7622
|
+
printHuman(c.gray(` ${claim.confidence} \xB7 ${claim.status}`));
|
|
7623
|
+
}
|
|
7624
|
+
if (claims.length === 0 && scaffolding) {
|
|
7625
|
+
printHuman(
|
|
7626
|
+
c.gray(
|
|
7627
|
+
`_Nobody has written this yet. The line above is the prompt \`init\` left, and the
|
|
6346
7628
|
body is its instructions \u2014 neither is something the user said, so do not answer
|
|
6347
7629
|
from them._
|
|
6348
7630
|
|
|
6349
7631
|
This one is filled in by asking. \`memgineering onboard\` prints the questions;
|
|
6350
7632
|
write the body, then record the conclusion:
|
|
6351
7633
|
memgineering revise ${handle2} --claim "<what they told you>" --summary "<one line>"`
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
7634
|
+
)
|
|
7635
|
+
);
|
|
7636
|
+
} else if (claims.length === 0) {
|
|
7637
|
+
printHuman(
|
|
7638
|
+
c.gray(
|
|
7639
|
+
entry.memory.provenance.mapping === "explicit" ? "_The title and summary above came from this note, but it declares no `claim` \u2014 so nothing here states a conclusion. That is what `stated` above refers to: where the fields came from, not that a conclusion exists._" : "_No claim declared \u2014 this card was inferred from the note._"
|
|
7640
|
+
)
|
|
7641
|
+
);
|
|
7642
|
+
printHuman(
|
|
7643
|
+
c.gray(
|
|
7644
|
+
`To state one:
|
|
6363
7645
|
memgineering revise ${handle2} --claim "<the conclusion>" --summary "<one line>"
|
|
6364
7646
|
Add --contradicts <id> when it disagrees with another memory; that edge is what
|
|
6365
7647
|
makes the two surface together as a conflict.`
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
7648
|
+
)
|
|
7649
|
+
);
|
|
7650
|
+
}
|
|
7651
|
+
if (stage.outline && stage.outline.length > 0) {
|
|
7652
|
+
printHuman("\n### sections\n");
|
|
7653
|
+
for (const heading of stage.outline) printHuman(`- ${heading}`);
|
|
7654
|
+
printHuman("");
|
|
7655
|
+
}
|
|
7656
|
+
if (stage.sections) {
|
|
7657
|
+
for (const section of stage.sections) {
|
|
7658
|
+
printHuman(`
|
|
6377
7659
|
### ${section.label}
|
|
6378
7660
|
`);
|
|
6379
|
-
|
|
7661
|
+
printHuman(quoteBlock(section.body));
|
|
7662
|
+
}
|
|
7663
|
+
printHuman("");
|
|
7664
|
+
}
|
|
7665
|
+
if (stage.body) {
|
|
7666
|
+
printHuman("\n### full\n");
|
|
7667
|
+
printHuman(quoteBlock(stage.body));
|
|
7668
|
+
printHuman("");
|
|
7669
|
+
}
|
|
7670
|
+
if (detail === "card") {
|
|
7671
|
+
printHuman(
|
|
7672
|
+
c.gray(`Deeper: \`memgineering open ${handle2} --detail summary | chunks | full\``)
|
|
7673
|
+
);
|
|
6380
7674
|
}
|
|
6381
|
-
printHuman("");
|
|
6382
|
-
}
|
|
6383
|
-
if (stage.body) {
|
|
6384
|
-
printHuman("\n### full\n");
|
|
6385
|
-
printHuman(quoteBlock(stage.body));
|
|
6386
|
-
printHuman("");
|
|
6387
|
-
}
|
|
6388
|
-
if (detail === "card") {
|
|
6389
|
-
printHuman(
|
|
6390
|
-
c.gray(`Deeper: \`memgineering open ${handle2} --detail summary | chunks | full\``)
|
|
6391
|
-
);
|
|
6392
7675
|
}
|
|
6393
|
-
}
|
|
6394
|
-
}
|
|
6395
|
-
|
|
7676
|
+
});
|
|
7677
|
+
}
|
|
7678
|
+
);
|
|
6396
7679
|
}
|
|
6397
7680
|
function resolveRef(entries, ref) {
|
|
6398
7681
|
const ids = entries.map((e) => e.memory.id);
|
|
@@ -6450,8 +7733,16 @@ init_link();
|
|
|
6450
7733
|
function lifecycleCommand(action) {
|
|
6451
7734
|
return new Command6(action).description(
|
|
6452
7735
|
action === "retire" ? "mark a memory as no longer current \u2014 the note itself is not changed" : "mark a retired memory as current again"
|
|
6453
|
-
).argument("<ref>", "handle, id, path, or exact title").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the memory is in").action(async (ref, opts) => {
|
|
7736
|
+
).argument("<ref>", "handle, id, path, or exact title").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the memory is in").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(async (ref, opts) => {
|
|
6454
7737
|
const rationale = sanitizeRationale(opts.reason);
|
|
7738
|
+
const where = await resolveTarget({
|
|
7739
|
+
...opts.local === void 0 ? {} : { local: opts.local },
|
|
7740
|
+
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
7741
|
+
});
|
|
7742
|
+
if (where.kind === "cloud") {
|
|
7743
|
+
await lifecycleHosted(where, action, ref, opts.reason);
|
|
7744
|
+
return;
|
|
7745
|
+
}
|
|
6455
7746
|
const { brain } = await resolveBrain(opts.vault);
|
|
6456
7747
|
const adapter = await LocalFileCasAdapter.openVault(brain.root, {
|
|
6457
7748
|
createPrefix: brain.observationsDir
|
|
@@ -6459,40 +7750,8 @@ function lifecycleCommand(action) {
|
|
|
6459
7750
|
const built = await openIndex(adapter, brain.root);
|
|
6460
7751
|
const entry = resolveRef(built.entries, ref);
|
|
6461
7752
|
const result = commit(
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
subject: entry.memory.title,
|
|
6465
|
-
scope: entry.memory.scope,
|
|
6466
|
-
kind: entry.memory.type,
|
|
6467
|
-
candidate_claim: entry.memory.summary ?? entry.memory.title,
|
|
6468
|
-
entities: [],
|
|
6469
|
-
importance: "normal"
|
|
6470
|
-
},
|
|
6471
|
-
matches: [
|
|
6472
|
-
{
|
|
6473
|
-
id: entry.memory.id,
|
|
6474
|
-
title: entry.memory.title,
|
|
6475
|
-
summary: entry.memory.summary,
|
|
6476
|
-
path: entry.path,
|
|
6477
|
-
revision: entry.revision,
|
|
6478
|
-
freshness: "current",
|
|
6479
|
-
score: 1
|
|
6480
|
-
}
|
|
6481
|
-
],
|
|
6482
|
-
contradicts: [],
|
|
6483
|
-
scopes: entry.memory.scope ? [entry.memory.scope] : []
|
|
6484
|
-
},
|
|
6485
|
-
{
|
|
6486
|
-
action,
|
|
6487
|
-
memory_id: entry.memory.id,
|
|
6488
|
-
claim: null,
|
|
6489
|
-
reason: opts.reason ?? `${action}d directly`,
|
|
6490
|
-
// For a standing change the memory's own summary IS the passage the
|
|
6491
|
-
// decision is about: it is what the note currently says, and it is
|
|
6492
|
-
// what stops being current.
|
|
6493
|
-
source_excerpt: entry.memory.summary ?? entry.memory.title,
|
|
6494
|
-
target_path: entry.path
|
|
6495
|
-
}
|
|
7753
|
+
asSingleMatch(entry),
|
|
7754
|
+
lifecycleInput(entry, action, opts.reason ?? null)
|
|
6496
7755
|
);
|
|
6497
7756
|
if (result.action === "ignore") {
|
|
6498
7757
|
throw memgError(
|
|
@@ -6554,8 +7813,16 @@ Undo: \`${undoHint(record.op_id, opts.vault)}\``));
|
|
|
6554
7813
|
function ignoreCommand(mode) {
|
|
6555
7814
|
return new Command6(mode).description(
|
|
6556
7815
|
mode === "exclude" ? "stop reading a note entirely \u2014 the note itself is not changed" : "read a note again after excluding it"
|
|
6557
|
-
).argument("<path>", "path inside the brain, e.g. decisions/launch-date.md").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the note is in").action(async (path, opts) => {
|
|
7816
|
+
).argument("<path>", "path inside the brain, e.g. decisions/launch-date.md").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the note is in").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(async (path, opts) => {
|
|
6558
7817
|
const rationale = sanitizeRationale(opts.reason);
|
|
7818
|
+
const where = await resolveTarget({
|
|
7819
|
+
...opts.local === void 0 ? {} : { local: opts.local },
|
|
7820
|
+
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
7821
|
+
});
|
|
7822
|
+
if (where.kind === "cloud") {
|
|
7823
|
+
await ignoreHosted(where, mode, path, opts.reason);
|
|
7824
|
+
return;
|
|
7825
|
+
}
|
|
6559
7826
|
const { brain } = await resolveBrain(opts.vault);
|
|
6560
7827
|
const adapter = await LocalFileCasAdapter.openVault(brain.root, {
|
|
6561
7828
|
createPrefix: brain.observationsDir
|
|
@@ -6608,6 +7875,57 @@ Undo: \`${undoHint(record.op_id, opts.vault)}\``));
|
|
|
6608
7875
|
});
|
|
6609
7876
|
});
|
|
6610
7877
|
}
|
|
7878
|
+
async function lifecycleHosted(where, action, ref, reason) {
|
|
7879
|
+
const brain = await openCloudBrain(where);
|
|
7880
|
+
const result = await cloudLifecycle(brain, action, {
|
|
7881
|
+
ref,
|
|
7882
|
+
...reason === void 0 ? {} : { reason }
|
|
7883
|
+
});
|
|
7884
|
+
printDual({
|
|
7885
|
+
json: { brain: where.name, ...result },
|
|
7886
|
+
human: () => {
|
|
7887
|
+
printHuman(
|
|
7888
|
+
action === "retire" ? `Retired in **${where.name}**.
|
|
7889
|
+
` : `Current again in **${where.name}**.
|
|
7890
|
+
`
|
|
7891
|
+
);
|
|
7892
|
+
printHuman(`\`${result.target}\`
|
|
7893
|
+
`);
|
|
7894
|
+
printHuman(c.gray(result.note));
|
|
7895
|
+
printHuman(
|
|
7896
|
+
c.gray(
|
|
7897
|
+
`
|
|
7898
|
+
To put it back: \`memgineering ${action === "retire" ? "unretire" : "retire"} ${ref}\``
|
|
7899
|
+
)
|
|
7900
|
+
);
|
|
7901
|
+
}
|
|
7902
|
+
});
|
|
7903
|
+
}
|
|
7904
|
+
async function ignoreHosted(where, mode, path, reason) {
|
|
7905
|
+
const brain = await openCloudBrain(where);
|
|
7906
|
+
const result = await cloudExclude(brain, mode, {
|
|
7907
|
+
path,
|
|
7908
|
+
...reason === void 0 ? {} : { reason }
|
|
7909
|
+
});
|
|
7910
|
+
printDual({
|
|
7911
|
+
json: { brain: where.name, ...result },
|
|
7912
|
+
human: () => {
|
|
7913
|
+
if (!result.changed) {
|
|
7914
|
+
printHuman(c.gray(result.note));
|
|
7915
|
+
return;
|
|
7916
|
+
}
|
|
7917
|
+
printHuman(
|
|
7918
|
+
mode === "exclude" ? `No longer reading \`${result.path}\` in **${where.name}**.
|
|
7919
|
+
` : `Reading \`${result.path}\` again in **${where.name}**.
|
|
7920
|
+
`
|
|
7921
|
+
);
|
|
7922
|
+
printHuman(c.gray(result.note));
|
|
7923
|
+
if (result.op_id) printHuman(c.gray(`
|
|
7924
|
+
Undo: \`memgineering ${opposite(mode)} ${path}\``));
|
|
7925
|
+
}
|
|
7926
|
+
});
|
|
7927
|
+
}
|
|
7928
|
+
var opposite = (mode) => mode === "exclude" ? "unexclude" : "exclude";
|
|
6611
7929
|
async function mustMatchAFile(root, adapter, path) {
|
|
6612
7930
|
const wanted = path.normalize("NFC").replace(/^\/+/, "");
|
|
6613
7931
|
const absolute = resolve6(root, wanted);
|
|
@@ -6633,8 +7951,31 @@ init_index_build();
|
|
|
6633
7951
|
init_vault();
|
|
6634
7952
|
init_ui();
|
|
6635
7953
|
function evidenceCommand() {
|
|
6636
|
-
return new Command7("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").action(async (ref, opts) => {
|
|
6637
|
-
const
|
|
7954
|
+
return new Command7("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").action(async (ref, opts) => {
|
|
7955
|
+
const target = await resolveTarget({
|
|
7956
|
+
...opts.local === void 0 ? {} : { local: opts.local },
|
|
7957
|
+
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
7958
|
+
});
|
|
7959
|
+
if (target.kind === "cloud") {
|
|
7960
|
+
const cloud = await openCloudBrain(target);
|
|
7961
|
+
const report2 = await cloudEvidence(cloud, ref);
|
|
7962
|
+
printDual({
|
|
7963
|
+
json: { ...report2, brain: { hosted: true, name: target.name } },
|
|
7964
|
+
human: () => {
|
|
7965
|
+
const window = report2["counts_cover_days"];
|
|
7966
|
+
printHuman(
|
|
7967
|
+
`surfaced ${String(report2["surfaced"] ?? 0)} \xB7 opened ${String(report2["opened"] ?? 0)}`
|
|
7968
|
+
);
|
|
7969
|
+
if (typeof window === "number") {
|
|
7970
|
+
printHuman(c.gray(` counts cover the last ${window} days, not all time`));
|
|
7971
|
+
}
|
|
7972
|
+
const why = report2["last_reason"];
|
|
7973
|
+
if (typeof why === "string" && why !== "") printHuman(c.gray(` last change: ${why}`));
|
|
7974
|
+
}
|
|
7975
|
+
});
|
|
7976
|
+
return;
|
|
7977
|
+
}
|
|
7978
|
+
const brain = target.brain;
|
|
6638
7979
|
const adapter = await openBrainOrExplain(brain.root);
|
|
6639
7980
|
const built = await openIndex(adapter, brain.root);
|
|
6640
7981
|
const entry = resolveRef(built.entries, ref);
|
|
@@ -6798,8 +8139,41 @@ import { Command as Command8 } from "commander";
|
|
|
6798
8139
|
init_vault();
|
|
6799
8140
|
init_ui();
|
|
6800
8141
|
function logCommand() {
|
|
6801
|
-
return new Command8("log").description("what changed in this brain, and what can still be undone").option("--limit <n>", "how many operations", (v) => parseInt(v, 10), 20).option("--verbose", "include hashes and reverse patches").option("--vault <path>", "which brain to read").action(async (opts) => {
|
|
6802
|
-
const
|
|
8142
|
+
return new Command8("log").description("what changed in this brain, and what can still be undone").option("--limit <n>", "how many operations", (v) => parseInt(v, 10), 20).option("--verbose", "include hashes and reverse patches").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").action(async (opts) => {
|
|
8143
|
+
const target = await resolveTarget({
|
|
8144
|
+
...opts.local === void 0 ? {} : { local: opts.local },
|
|
8145
|
+
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
8146
|
+
});
|
|
8147
|
+
if (target.kind === "cloud") {
|
|
8148
|
+
const cloud = await openCloudBrain(target);
|
|
8149
|
+
const history = await cloudLog(cloud, {
|
|
8150
|
+
limit: opts.limit,
|
|
8151
|
+
...opts.verbose === true ? { verbose: true } : {}
|
|
8152
|
+
});
|
|
8153
|
+
printDual({
|
|
8154
|
+
json: { ...history, brain: { hosted: true, name: target.name } },
|
|
8155
|
+
human: () => {
|
|
8156
|
+
const ops2 = history["operations"] ?? [];
|
|
8157
|
+
const diverged2 = history["divergence"] ?? [];
|
|
8158
|
+
printHuman(`## log \u2014 ${target.name} (${ops2.length} operations)
|
|
8159
|
+
`);
|
|
8160
|
+
for (const op of ops2) {
|
|
8161
|
+
printHuman(
|
|
8162
|
+
`${String(op["ts"] ?? "")} ${String(op["verb"] ?? "")} ${String(op["target"] ?? "")}`
|
|
8163
|
+
);
|
|
8164
|
+
printHuman(c.gray(` \`${String(op["op_id"] ?? "")}\``));
|
|
8165
|
+
}
|
|
8166
|
+
if (diverged2.length > 0) {
|
|
8167
|
+
printHuman(
|
|
8168
|
+
c.yellow(`
|
|
8169
|
+
${diverged2.length} of these no longer match the note on the server.`)
|
|
8170
|
+
);
|
|
8171
|
+
}
|
|
8172
|
+
}
|
|
8173
|
+
});
|
|
8174
|
+
return;
|
|
8175
|
+
}
|
|
8176
|
+
const brain = target.brain;
|
|
6803
8177
|
const adapter = await openBrainOrExplain(brain.root);
|
|
6804
8178
|
const { ops, skipped } = await readOps(brain.root);
|
|
6805
8179
|
const recent = ops.slice(-opts.limit).reverse();
|
|
@@ -6896,12 +8270,111 @@ async function readOrNull(adapter, path) {
|
|
|
6896
8270
|
}
|
|
6897
8271
|
}
|
|
6898
8272
|
|
|
8273
|
+
// src/program.ts
|
|
8274
|
+
init_login();
|
|
8275
|
+
|
|
8276
|
+
// src/commands/logout.ts
|
|
8277
|
+
init_errors();
|
|
8278
|
+
init_api_client();
|
|
8279
|
+
init_credentials();
|
|
8280
|
+
init_pending_login();
|
|
8281
|
+
init_ui();
|
|
8282
|
+
import { Command as Command10 } from "commander";
|
|
8283
|
+
function logoutCommand() {
|
|
8284
|
+
return new Command10("logout").description("sign out of a hosted brain").option("--all", "revoke every token on the account, not just this machine\u2019s").option(
|
|
8285
|
+
"--local",
|
|
8286
|
+
"forget the token here without telling the server \u2014 only for a server you cannot reach"
|
|
8287
|
+
).action(async (opts) => {
|
|
8288
|
+
const base = apiUrl();
|
|
8289
|
+
const credentials = await readCredentials();
|
|
8290
|
+
await clearPendingLogin();
|
|
8291
|
+
if (!credentials) {
|
|
8292
|
+
printDual({
|
|
8293
|
+
json: { signed_out: true, was_signed_in: false, api_url: base },
|
|
8294
|
+
human: () => printHuman("Not signed in \u2014 nothing to do.")
|
|
8295
|
+
});
|
|
8296
|
+
return;
|
|
8297
|
+
}
|
|
8298
|
+
if (opts.local) {
|
|
8299
|
+
await clearCredentials();
|
|
8300
|
+
printDual({
|
|
8301
|
+
json: { signed_out: true, revoked_on_server: false, api_url: credentials.api_url },
|
|
8302
|
+
human: () => {
|
|
8303
|
+
printHuman("Forgotten here.");
|
|
8304
|
+
printHuman(
|
|
8305
|
+
c.yellow(
|
|
8306
|
+
" The token was NOT revoked \u2014 it still works for anyone holding a copy.\n Run `memgineering logout --all` once the server is reachable."
|
|
8307
|
+
)
|
|
8308
|
+
);
|
|
8309
|
+
}
|
|
8310
|
+
});
|
|
8311
|
+
return;
|
|
8312
|
+
}
|
|
8313
|
+
let revoked = null;
|
|
8314
|
+
try {
|
|
8315
|
+
if (opts.all) {
|
|
8316
|
+
const res = await apiRequest({
|
|
8317
|
+
method: "POST",
|
|
8318
|
+
path: "/v1/tokens/revoke-all",
|
|
8319
|
+
token: credentials.token,
|
|
8320
|
+
baseUrl: credentials.api_url
|
|
8321
|
+
});
|
|
8322
|
+
revoked = res.body?.revoked ?? null;
|
|
8323
|
+
} else {
|
|
8324
|
+
const me = await apiRequest({
|
|
8325
|
+
method: "GET",
|
|
8326
|
+
path: "/v1/me",
|
|
8327
|
+
token: credentials.token,
|
|
8328
|
+
baseUrl: credentials.api_url
|
|
8329
|
+
});
|
|
8330
|
+
const id = me.body?.tokens?.current_id;
|
|
8331
|
+
if (id) {
|
|
8332
|
+
await apiRequest({
|
|
8333
|
+
method: "DELETE",
|
|
8334
|
+
path: `/v1/tokens/${id}`,
|
|
8335
|
+
token: credentials.token,
|
|
8336
|
+
baseUrl: credentials.api_url
|
|
8337
|
+
});
|
|
8338
|
+
}
|
|
8339
|
+
revoked = 1;
|
|
8340
|
+
}
|
|
8341
|
+
} catch (err) {
|
|
8342
|
+
const code = err instanceof MemgError ? err.code : "";
|
|
8343
|
+
if (code !== "not_authenticated" && code !== "not_found") {
|
|
8344
|
+
throw err instanceof MemgError ? new MemgError({
|
|
8345
|
+
code: err.code,
|
|
8346
|
+
message: err.message,
|
|
8347
|
+
hint: `${err.hint} The token is still stored here and still works; nothing was changed. \`memgineering logout --local\` forgets it on this machine without revoking it.`
|
|
8348
|
+
}) : err;
|
|
8349
|
+
}
|
|
8350
|
+
}
|
|
8351
|
+
await clearCredentials();
|
|
8352
|
+
printDual({
|
|
8353
|
+
json: {
|
|
8354
|
+
signed_out: true,
|
|
8355
|
+
revoked_on_server: true,
|
|
8356
|
+
revoked,
|
|
8357
|
+
all: opts.all === true,
|
|
8358
|
+
api_url: credentials.api_url
|
|
8359
|
+
},
|
|
8360
|
+
human: () => {
|
|
8361
|
+
printHuman(opts.all ? "Signed out everywhere." : "Signed out.");
|
|
8362
|
+
printHuman(
|
|
8363
|
+
c.gray(
|
|
8364
|
+
opts.all ? " Every token on the account is dead. `memgineering login` starts a new one." : " This machine\u2019s token is revoked. Other machines are unaffected."
|
|
8365
|
+
)
|
|
8366
|
+
);
|
|
8367
|
+
}
|
|
8368
|
+
});
|
|
8369
|
+
});
|
|
8370
|
+
}
|
|
8371
|
+
|
|
6899
8372
|
// src/commands/onboard.ts
|
|
6900
|
-
import { Command as
|
|
8373
|
+
import { Command as Command11 } from "commander";
|
|
6901
8374
|
init_vault();
|
|
6902
8375
|
init_ui();
|
|
6903
8376
|
function onboardCommand() {
|
|
6904
|
-
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) => {
|
|
6905
8378
|
const { brain } = await resolveBrain(opts.vault);
|
|
6906
8379
|
const adapter = await openBrainOrExplain(brain.root);
|
|
6907
8380
|
const untouched = await untouchedBase(
|
|
@@ -6963,48 +8436,402 @@ function onboardCommand() {
|
|
|
6963
8436
|
});
|
|
6964
8437
|
}
|
|
6965
8438
|
|
|
8439
|
+
// src/commands/pull.ts
|
|
8440
|
+
init_errors();
|
|
8441
|
+
init_api_client();
|
|
8442
|
+
import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
|
|
8443
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join16, resolve as resolve7, sep as sep2 } from "path";
|
|
8444
|
+
import { Command as Command12 } from "commander";
|
|
8445
|
+
init_ui();
|
|
8446
|
+
function pullCommand() {
|
|
8447
|
+
return new Command12("pull").description("download your hosted brain to a folder on this machine").argument("[destination]", "folder to write into (created if missing)").option("--brain <name>", "which hosted brain to download").option("--dry-run", "say what would be written and write nothing").action(async (destination, opts) => {
|
|
8448
|
+
const source = await resolveSource(opts.brain);
|
|
8449
|
+
const target = resolve7(destination ?? `./${source.name}`);
|
|
8450
|
+
const first = await cloudListNotes(source, { limit: 1 });
|
|
8451
|
+
if (first.total === 0) {
|
|
8452
|
+
throw memgError(
|
|
8453
|
+
"invalid_input",
|
|
8454
|
+
`\u201C${source.name}\u201D has no notes to download`,
|
|
8455
|
+
"Nothing was written. `memgineering push` carries a brain on this disk up to the hosted one, and `remember` writes a first note into it."
|
|
8456
|
+
);
|
|
8457
|
+
}
|
|
8458
|
+
if (opts.dryRun) {
|
|
8459
|
+
const preview = await collectPaths(source);
|
|
8460
|
+
printDual({
|
|
8461
|
+
json: {
|
|
8462
|
+
dry_run: true,
|
|
8463
|
+
brain: { id: source.brainId, name: source.name },
|
|
8464
|
+
into: target,
|
|
8465
|
+
notes: first.total,
|
|
8466
|
+
excluded: preview.filter((n) => n.excluded === true).length,
|
|
8467
|
+
paths: preview.slice(0, 100).map((n) => n.path),
|
|
8468
|
+
...preview.length > 100 ? { paths_truncated: preview.length - 100 } : {}
|
|
8469
|
+
},
|
|
8470
|
+
human: () => {
|
|
8471
|
+
printHuman(`## pull (dry run)
|
|
8472
|
+
`);
|
|
8473
|
+
printHuman(`${first.total} note(s) from \u201C${source.name}\u201D into \`${target}\`
|
|
8474
|
+
`);
|
|
8475
|
+
for (const note of preview.slice(0, 20)) {
|
|
8476
|
+
printHuman(c.gray(` ${note.path}${note.excluded === true ? " (excluded)" : ""}`));
|
|
8477
|
+
}
|
|
8478
|
+
if (preview.length > 20) printHuman(c.gray(` \u2026 and ${preview.length - 20} more`));
|
|
8479
|
+
printHuman(
|
|
8480
|
+
c.gray(
|
|
8481
|
+
"\nNothing was written. A real run never replaces a file that is already\nthere \u2014 it skips it and says so."
|
|
8482
|
+
)
|
|
8483
|
+
);
|
|
8484
|
+
}
|
|
8485
|
+
});
|
|
8486
|
+
return;
|
|
8487
|
+
}
|
|
8488
|
+
progress(`downloading ${first.total} note(s) from \u201C${source.name}\u201D\u2026`);
|
|
8489
|
+
const written = [];
|
|
8490
|
+
const already = [];
|
|
8491
|
+
const excluded = [];
|
|
8492
|
+
let cursor;
|
|
8493
|
+
do {
|
|
8494
|
+
const page = await cloudListNotes(source, {
|
|
8495
|
+
...cursor === void 0 ? {} : { cursor },
|
|
8496
|
+
content: true
|
|
8497
|
+
});
|
|
8498
|
+
for (const note of page.notes) {
|
|
8499
|
+
const file = safeJoin(target, note.path);
|
|
8500
|
+
if (await exists(file)) {
|
|
8501
|
+
already.push(note.path);
|
|
8502
|
+
continue;
|
|
8503
|
+
}
|
|
8504
|
+
await mkdir11(dirname3(file), { recursive: true });
|
|
8505
|
+
await writeFile9(file, note.content ?? "", "utf8");
|
|
8506
|
+
written.push(note.path);
|
|
8507
|
+
if (note.excluded === true) excluded.push(note.path);
|
|
8508
|
+
}
|
|
8509
|
+
cursor = page.next_cursor;
|
|
8510
|
+
} while (cursor);
|
|
8511
|
+
printDual({
|
|
8512
|
+
json: {
|
|
8513
|
+
brain: { id: source.brainId, name: source.name },
|
|
8514
|
+
into: target,
|
|
8515
|
+
written: written.length,
|
|
8516
|
+
already_there: already.length,
|
|
8517
|
+
excluded: excluded.length,
|
|
8518
|
+
next: `\`memgineering link ${target}\` reads this folder as a brain on this machine. Until then it is markdown and nothing else.`
|
|
8519
|
+
},
|
|
8520
|
+
human: () => {
|
|
8521
|
+
printHuman(c.green(`\u2713 ${written.length} note(s) written to \`${target}\``));
|
|
8522
|
+
if (already.length > 0) {
|
|
8523
|
+
printHuman(
|
|
8524
|
+
c.gray(` ${already.length} were already there \u2014 left untouched, not overwritten`)
|
|
8525
|
+
);
|
|
8526
|
+
}
|
|
8527
|
+
if (excluded.length > 0) {
|
|
8528
|
+
printHuman(
|
|
8529
|
+
c.yellow(` ${excluded.length} of them are notes this brain excludes from recall`) + c.gray("\n they came down because they are yours; `.memgignore` came with them")
|
|
8530
|
+
);
|
|
8531
|
+
}
|
|
8532
|
+
printHuman(
|
|
8533
|
+
c.gray(
|
|
8534
|
+
`
|
|
8535
|
+
This is a folder of markdown, not yet a brain on this machine.
|
|
8536
|
+
\`memgineering link ${target}\` makes it one \u2014 it shows what it would read first.`
|
|
8537
|
+
)
|
|
8538
|
+
);
|
|
8539
|
+
}
|
|
8540
|
+
});
|
|
8541
|
+
});
|
|
8542
|
+
}
|
|
8543
|
+
async function collectPaths(source) {
|
|
8544
|
+
const all = [];
|
|
8545
|
+
let cursor;
|
|
8546
|
+
do {
|
|
8547
|
+
const page = await cloudListNotes(source, cursor === void 0 ? {} : { cursor });
|
|
8548
|
+
all.push(...page.notes);
|
|
8549
|
+
cursor = page.next_cursor;
|
|
8550
|
+
} while (cursor);
|
|
8551
|
+
return all;
|
|
8552
|
+
}
|
|
8553
|
+
async function resolveSource(named) {
|
|
8554
|
+
const base = apiUrl();
|
|
8555
|
+
const credentials = await requireCredentials(base);
|
|
8556
|
+
const brains = await listCloudBrains(credentials.token, base);
|
|
8557
|
+
if (brains.length === 0) {
|
|
8558
|
+
throw memgError(
|
|
8559
|
+
"invalid_input",
|
|
8560
|
+
"this account has no hosted brains",
|
|
8561
|
+
"Nothing to download. `memgineering push` carries the brain on this disk up to a hosted one."
|
|
8562
|
+
);
|
|
8563
|
+
}
|
|
8564
|
+
const found = named ? brains.find((b) => b.name === named || b.id === named) : brains.length === 1 ? brains[0] : void 0;
|
|
8565
|
+
if (!found) {
|
|
8566
|
+
throw memgError(
|
|
8567
|
+
"invalid_input",
|
|
8568
|
+
named ? `no hosted brain called \u201C${named}\u201D` : "more than one hosted brain \u2014 say which",
|
|
8569
|
+
`Pass one of: ${brains.map((b) => b.name).join(", ")} (e.g. \`memgineering pull --brain ${brains[0]?.name}\`)`
|
|
8570
|
+
);
|
|
8571
|
+
}
|
|
8572
|
+
return { brainId: found.id, name: found.name, token: credentials.token, apiUrl: base };
|
|
8573
|
+
}
|
|
8574
|
+
function safeJoin(root, relative4) {
|
|
8575
|
+
if (isAbsolute2(relative4)) {
|
|
8576
|
+
throw memgError(
|
|
8577
|
+
"server_refused",
|
|
8578
|
+
`the server sent an absolute path: ${relative4}`,
|
|
8579
|
+
"Nothing was written for it. This should not happen \u2014 please report it."
|
|
8580
|
+
);
|
|
8581
|
+
}
|
|
8582
|
+
const full = resolve7(join16(root, relative4));
|
|
8583
|
+
if (full !== root && !full.startsWith(root + sep2)) {
|
|
8584
|
+
throw memgError(
|
|
8585
|
+
"server_refused",
|
|
8586
|
+
`the server sent a path that escapes the destination: ${relative4}`,
|
|
8587
|
+
"Nothing was written for it. This should not happen \u2014 please report it."
|
|
8588
|
+
);
|
|
8589
|
+
}
|
|
8590
|
+
return full;
|
|
8591
|
+
}
|
|
8592
|
+
async function exists(file) {
|
|
8593
|
+
try {
|
|
8594
|
+
await readFile16(file);
|
|
8595
|
+
return true;
|
|
8596
|
+
} catch {
|
|
8597
|
+
return false;
|
|
8598
|
+
}
|
|
8599
|
+
}
|
|
8600
|
+
|
|
8601
|
+
// src/commands/push.ts
|
|
8602
|
+
init_errors();
|
|
8603
|
+
init_config();
|
|
8604
|
+
init_api_client();
|
|
8605
|
+
import { Command as Command13 } from "commander";
|
|
8606
|
+
init_vault();
|
|
8607
|
+
init_ui();
|
|
8608
|
+
function pushCommand() {
|
|
8609
|
+
return new Command13("push").description("upload the brain on this machine to your hosted one").option("--vault <path>", "which local brain to upload").option("--brain <name>", "the hosted brain to upload into (created if it does not exist)").option("--dry-run", "list what would be uploaded and send nothing").action(async (opts) => {
|
|
8610
|
+
const { brain: local } = await resolveBrain(opts.vault);
|
|
8611
|
+
const adapter = await openBrainOrExplain(local.root);
|
|
8612
|
+
const scan = await adapter.scan();
|
|
8613
|
+
const paths = scan.files.filter((p) => /\.(md|markdown)$/i.test(p));
|
|
8614
|
+
if (paths.length === 0) {
|
|
8615
|
+
throw memgError(
|
|
8616
|
+
"invalid_input",
|
|
8617
|
+
`there is nothing to upload in ${local.root}`,
|
|
8618
|
+
"This brain has no markdown notes that the rules allow. `memgineering reindex` prints what is being left out and why."
|
|
8619
|
+
);
|
|
8620
|
+
}
|
|
8621
|
+
if (opts.dryRun) {
|
|
8622
|
+
printDual({
|
|
8623
|
+
json: {
|
|
8624
|
+
dry_run: true,
|
|
8625
|
+
from: local.root,
|
|
8626
|
+
notes: paths.length,
|
|
8627
|
+
paths: paths.slice(0, 100),
|
|
8628
|
+
...paths.length > 100 ? { paths_truncated: paths.length - 100 } : {}
|
|
8629
|
+
},
|
|
8630
|
+
human: () => {
|
|
8631
|
+
printHuman(`## push (dry run)
|
|
8632
|
+
`);
|
|
8633
|
+
printHuman(`${paths.length} note(s) from \`${local.root}\`
|
|
8634
|
+
`);
|
|
8635
|
+
for (const path of paths.slice(0, 20)) printHuman(c.gray(` ${path}`));
|
|
8636
|
+
if (paths.length > 20) printHuman(c.gray(` \u2026 and ${paths.length - 20} more`));
|
|
8637
|
+
printHuman(
|
|
8638
|
+
c.gray(
|
|
8639
|
+
"\nNothing was sent. Notes carrying credential material are refused by the\nserver and would be listed in the summary; a dry run cannot know which,\nbecause it does not read the bytes."
|
|
8640
|
+
)
|
|
8641
|
+
);
|
|
8642
|
+
}
|
|
8643
|
+
});
|
|
8644
|
+
return;
|
|
8645
|
+
}
|
|
8646
|
+
const destination = await resolveDestination(opts.brain, local.root);
|
|
8647
|
+
progress(`uploading ${paths.length} note(s) to \u201C${destination.name}\u201D\u2026`);
|
|
8648
|
+
const uploaded = [];
|
|
8649
|
+
const already = [];
|
|
8650
|
+
const refused = [];
|
|
8651
|
+
for (const path of paths) {
|
|
8652
|
+
let content;
|
|
8653
|
+
try {
|
|
8654
|
+
content = (await adapter.read(path)).content;
|
|
8655
|
+
} catch (err) {
|
|
8656
|
+
refused.push({
|
|
8657
|
+
path,
|
|
8658
|
+
code: err.code ?? "read_failed",
|
|
8659
|
+
message: err.message
|
|
8660
|
+
});
|
|
8661
|
+
continue;
|
|
8662
|
+
}
|
|
8663
|
+
try {
|
|
8664
|
+
await cloudPutNote(destination, { path, content });
|
|
8665
|
+
uploaded.push(path);
|
|
8666
|
+
} catch (err) {
|
|
8667
|
+
const code = err.code;
|
|
8668
|
+
if (code === "note_exists") {
|
|
8669
|
+
already.push(path);
|
|
8670
|
+
continue;
|
|
8671
|
+
}
|
|
8672
|
+
if (code === "write_refused" || code === "invalid_path" || code === "rationale_refused") {
|
|
8673
|
+
refused.push({ path, code, message: err.message });
|
|
8674
|
+
continue;
|
|
8675
|
+
}
|
|
8676
|
+
throw Object.assign(err, {
|
|
8677
|
+
hint: `${uploaded.length} note(s) went up before this. Nothing was deleted locally, and running \`memgineering push\` again resumes from where it stopped. ` + (err.hint ?? "")
|
|
8678
|
+
});
|
|
8679
|
+
}
|
|
8680
|
+
}
|
|
8681
|
+
await pointConfigAt(destination);
|
|
8682
|
+
printDual({
|
|
8683
|
+
json: {
|
|
8684
|
+
from: local.root,
|
|
8685
|
+
brain: { id: destination.brainId, name: destination.name },
|
|
8686
|
+
uploaded: uploaded.length,
|
|
8687
|
+
already_there: already.length,
|
|
8688
|
+
refused
|
|
8689
|
+
},
|
|
8690
|
+
human: () => {
|
|
8691
|
+
printHuman(c.green(`\u2713 ${uploaded.length} note(s) uploaded to \u201C${destination.name}\u201D`));
|
|
8692
|
+
if (already.length > 0) {
|
|
8693
|
+
printHuman(c.gray(` ${already.length} were already there \u2014 left as they are`));
|
|
8694
|
+
}
|
|
8695
|
+
if (refused.length > 0) {
|
|
8696
|
+
printHuman(
|
|
8697
|
+
c.yellow(`
|
|
8698
|
+
\u26A0 ${refused.length} note(s) were NOT uploaded:`) + c.gray("\n they are untouched on this machine and can be fixed and pushed again")
|
|
8699
|
+
);
|
|
8700
|
+
for (const r of refused.slice(0, 10)) {
|
|
8701
|
+
printHuman(c.gray(` ${r.path} (${r.code})`));
|
|
8702
|
+
}
|
|
8703
|
+
if (refused.length > 10) {
|
|
8704
|
+
printHuman(c.gray(` \u2026 and ${refused.length - 10} more`));
|
|
8705
|
+
}
|
|
8706
|
+
}
|
|
8707
|
+
printHuman(
|
|
8708
|
+
c.gray(
|
|
8709
|
+
`
|
|
8710
|
+
This machine now reads \u201C${destination.name}\u201D. Nothing was deleted from
|
|
8711
|
+
\`${local.root}\` \u2014 \`memgineering recall --local\` still reads it.`
|
|
8712
|
+
)
|
|
8713
|
+
);
|
|
8714
|
+
}
|
|
8715
|
+
});
|
|
8716
|
+
});
|
|
8717
|
+
}
|
|
8718
|
+
async function resolveDestination(named, root) {
|
|
8719
|
+
const base = apiUrl();
|
|
8720
|
+
const credentials = await requireCredentials(base);
|
|
8721
|
+
const config = await loadConfig();
|
|
8722
|
+
const brains = await listCloudBrains(credentials.token, base);
|
|
8723
|
+
const pick = (summary) => ({
|
|
8724
|
+
brainId: summary.id,
|
|
8725
|
+
name: summary.name,
|
|
8726
|
+
token: credentials.token,
|
|
8727
|
+
apiUrl: base
|
|
8728
|
+
});
|
|
8729
|
+
if (named) {
|
|
8730
|
+
const found = brains.find((b) => b.name === named || b.id === named);
|
|
8731
|
+
return pick(found ?? await createCloudBrain(credentials.token, base, named));
|
|
8732
|
+
}
|
|
8733
|
+
const current = config.brain.cloud;
|
|
8734
|
+
if (current && current.api_url === base) {
|
|
8735
|
+
const found = brains.find((b) => b.id === current.id);
|
|
8736
|
+
if (found) return pick(found);
|
|
8737
|
+
}
|
|
8738
|
+
const fallbackName = root.split("/").filter(Boolean).pop() ?? "brain";
|
|
8739
|
+
const existing = brains.find((b) => b.name === fallbackName);
|
|
8740
|
+
return pick(existing ?? await createCloudBrain(credentials.token, base, fallbackName));
|
|
8741
|
+
}
|
|
8742
|
+
async function pointConfigAt(destination) {
|
|
8743
|
+
const config = await loadConfig();
|
|
8744
|
+
await saveConfig({
|
|
8745
|
+
...config,
|
|
8746
|
+
brain: {
|
|
8747
|
+
...config.brain,
|
|
8748
|
+
cloud: { id: destination.brainId, name: destination.name, api_url: destination.apiUrl }
|
|
8749
|
+
}
|
|
8750
|
+
});
|
|
8751
|
+
}
|
|
8752
|
+
|
|
6966
8753
|
// src/commands/recall.ts
|
|
6967
8754
|
init_src();
|
|
6968
|
-
import { Command as
|
|
8755
|
+
import { Command as Command14 } from "commander";
|
|
6969
8756
|
init_index_build();
|
|
8757
|
+
init_errors();
|
|
6970
8758
|
init_vault();
|
|
6971
8759
|
init_ui();
|
|
6972
8760
|
function recallCommand() {
|
|
6973
|
-
return new
|
|
8761
|
+
return new Command14("recall").description("recall memory cards for a question").argument("<query>", "what you want to remember").option("--vault <path>", "which brain to search").option("--local", "search the brain on this machine, even when signed in to a hosted one").option("--scope <scope>", "restrict to one scope").option("--limit <n>", "max cards", (v) => parseInt(v, 10), 8).option("--detail <level>", "title | card | summary | chunks | full (default: card)").action(
|
|
6974
8762
|
async (query, opts) => {
|
|
6975
8763
|
const detail = parseDetail(opts.detail, "card");
|
|
6976
|
-
const
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
const result = recall(built.entries, query, {
|
|
6980
|
-
limit: opts.limit,
|
|
6981
|
-
...opts.scope ? { scope: opts.scope } : {}
|
|
8764
|
+
const target = await resolveTarget({
|
|
8765
|
+
...opts.local === void 0 ? {} : { local: opts.local },
|
|
8766
|
+
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
6982
8767
|
});
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
8768
|
+
let rows;
|
|
8769
|
+
let isScaffolding;
|
|
8770
|
+
let reasons;
|
|
8771
|
+
let pathOf;
|
|
8772
|
+
let entries;
|
|
8773
|
+
let result;
|
|
8774
|
+
if (target.kind === "cloud") {
|
|
8775
|
+
if (detail !== "title" && detail !== "card") {
|
|
8776
|
+
throw memgError(
|
|
8777
|
+
"invalid_input",
|
|
8778
|
+
`\`--detail ${detail}\` is not available on a hosted brain`,
|
|
8779
|
+
`Nothing was read. Hosted recall returns cards; \`memgineering open <handle> --detail ${detail}\` fetches one note's prose, and \`--local\` runs the whole search against the brain on this machine.`
|
|
8780
|
+
);
|
|
8781
|
+
}
|
|
8782
|
+
const cloud = await openCloudBrain(target);
|
|
8783
|
+
const answered = await cloudRecall(cloud, {
|
|
8784
|
+
query,
|
|
8785
|
+
limit: opts.limit,
|
|
8786
|
+
...opts.scope ? { scope: opts.scope } : {}
|
|
8787
|
+
});
|
|
8788
|
+
rows = answered.memory_cards.map((card) => ({ card, handle: card.handle, stage: {} }));
|
|
8789
|
+
isScaffolding = () => false;
|
|
8790
|
+
reasons = /* @__PURE__ */ new Map();
|
|
8791
|
+
pathOf = () => null;
|
|
8792
|
+
entries = null;
|
|
8793
|
+
result = {
|
|
8794
|
+
memory_cards: answered.memory_cards,
|
|
8795
|
+
related: answered.related ?? [],
|
|
8796
|
+
conflicts: answered.conflicts ?? []
|
|
8797
|
+
};
|
|
8798
|
+
} else {
|
|
8799
|
+
const brain = target.brain;
|
|
8800
|
+
const adapter = await openBrainOrExplain(brain.root);
|
|
8801
|
+
const built = await openIndex(adapter, brain.root);
|
|
8802
|
+
const local = recall(built.entries, query, {
|
|
8803
|
+
limit: opts.limit,
|
|
8804
|
+
...opts.scope ? { scope: opts.scope } : {}
|
|
8805
|
+
});
|
|
8806
|
+
result = local;
|
|
8807
|
+
entries = built.entries;
|
|
8808
|
+
await recordEvent(brain.root, {
|
|
8809
|
+
verb: "recall",
|
|
8810
|
+
query,
|
|
8811
|
+
detail,
|
|
8812
|
+
limit: opts.limit,
|
|
8813
|
+
returnedIds: result.memory_cards.map((card) => card.id)
|
|
8814
|
+
});
|
|
8815
|
+
const allIds = built.entries.map((e) => e.memory.id);
|
|
8816
|
+
const byId = new Map(built.entries.map((e) => [e.memory.id, e]));
|
|
8817
|
+
pathOf = (card) => byId.get(card.id)?.path ?? null;
|
|
8818
|
+
rows = await Promise.all(
|
|
8819
|
+
result.memory_cards.map(async (card) => ({
|
|
8820
|
+
card,
|
|
8821
|
+
handle: displayHandle(card.id, allIds),
|
|
8822
|
+
stage: await stageFor(adapter, byId.get(card.id), detail)
|
|
8823
|
+
}))
|
|
8824
|
+
);
|
|
8825
|
+
const scaffolding = await scaffoldingAmong(
|
|
8826
|
+
adapter,
|
|
8827
|
+
rows.map(({ card }) => {
|
|
8828
|
+
const entry = byId.get(card.id);
|
|
8829
|
+
return { path: entry?.path ?? "", title: entry?.memory.title ?? "" };
|
|
8830
|
+
})
|
|
8831
|
+
);
|
|
8832
|
+
isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
|
|
8833
|
+
reasons = result.conflicts.length > 0 ? await conflictReasons(brain.root, result.conflicts, byId) : /* @__PURE__ */ new Map();
|
|
8834
|
+
}
|
|
7008
8835
|
printDual({
|
|
7009
8836
|
json: {
|
|
7010
8837
|
query,
|
|
@@ -7012,7 +8839,7 @@ function recallCommand() {
|
|
|
7012
8839
|
cards: rows.map(({ card, handle: handle2, stage }) => ({
|
|
7013
8840
|
...card,
|
|
7014
8841
|
handle: handle2,
|
|
7015
|
-
path:
|
|
8842
|
+
path: pathOf(card),
|
|
7016
8843
|
// A fact about the file, not a judgement about the match: this
|
|
7017
8844
|
// note is still exactly what `init` wrote, so nobody has answered
|
|
7018
8845
|
// it yet.
|
|
@@ -7028,7 +8855,7 @@ function recallCommand() {
|
|
|
7028
8855
|
},
|
|
7029
8856
|
human: () => {
|
|
7030
8857
|
const reachedSomethingWritten = rows.some(({ card }) => !isScaffolding(card));
|
|
7031
|
-
const otherScript = reachedSomethingWritten ? null : unreachableScript(query,
|
|
8858
|
+
const otherScript = reachedSomethingWritten ? null : entries ? unreachableScript(query, entries) : null;
|
|
7032
8859
|
const sayWhy = () => {
|
|
7033
8860
|
printHuman(
|
|
7034
8861
|
c.yellow(
|
|
@@ -7166,7 +8993,8 @@ function ago2(days) {
|
|
|
7166
8993
|
function unreachableScript(query, entries) {
|
|
7167
8994
|
const queryScripts = scriptsIn(query);
|
|
7168
8995
|
if (queryScripts.size === 0) return null;
|
|
7169
|
-
const
|
|
8996
|
+
const SCAFFOLD = ["01_BASE/", "00_HUB/"];
|
|
8997
|
+
const written = entries.filter((e) => !SCAFFOLD.some((dir) => e.path.startsWith(dir)));
|
|
7170
8998
|
const counts = /* @__PURE__ */ new Map();
|
|
7171
8999
|
for (const entry of written) {
|
|
7172
9000
|
const script = dominantScriptOf(`${entry.memory.title} ${entry.memory.summary ?? ""}`);
|
|
@@ -7175,6 +9003,12 @@ function unreachableScript(query, entries) {
|
|
|
7175
9003
|
if (counts.size === 0) return null;
|
|
7176
9004
|
const [dominant, seen] = [...counts].sort((a, b) => b[1] - a[1])[0];
|
|
7177
9005
|
if (queryScripts.has(dominant)) return null;
|
|
9006
|
+
if (queryScripts.size > 0) {
|
|
9007
|
+
for (const entry of written) {
|
|
9008
|
+
const script = dominantScriptOf(`${entry.memory.title} ${entry.memory.summary ?? ""}`);
|
|
9009
|
+
if (script && queryScripts.has(script)) return null;
|
|
9010
|
+
}
|
|
9011
|
+
}
|
|
7178
9012
|
return seen >= Math.max(2, written.length / 2) ? dominant : null;
|
|
7179
9013
|
}
|
|
7180
9014
|
var SCRIPT_RANGES = [
|
|
@@ -7213,73 +9047,103 @@ function scriptsIn(text) {
|
|
|
7213
9047
|
}
|
|
7214
9048
|
|
|
7215
9049
|
// src/commands/remember.ts
|
|
7216
|
-
import { Command as
|
|
9050
|
+
import { Command as Command15 } from "commander";
|
|
7217
9051
|
init_errors();
|
|
7218
9052
|
init_vault();
|
|
7219
9053
|
init_ui();
|
|
7220
9054
|
function rememberCommand() {
|
|
7221
|
-
return new
|
|
9055
|
+
return new Command15("remember").description("write something down now, with no approval step").argument("<text>", "what to remember").option("--vault <path>", "which brain to write to").option("--local", "write to the brain on this machine, even when signed in to a hosted one").option("--scope <scope>", "what this is about \u2014 a project, an area").option(
|
|
7222
9056
|
"--reason <text>",
|
|
7223
9057
|
"why you are recording this \u2014 goes in the ledger, not in the note (the fact itself belongs in <text>)"
|
|
7224
|
-
).action(
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
"invalid_input",
|
|
7229
|
-
"nothing to remember",
|
|
7230
|
-
'Pass the text: `memgineering remember "deploys are manual \u2014 launchctl by hand"`'
|
|
7231
|
-
);
|
|
7232
|
-
}
|
|
7233
|
-
const rationale = sanitizeRationale(opts.reason);
|
|
7234
|
-
const { brain } = await resolveBrain(opts.vault);
|
|
7235
|
-
await openBrainOrExplain(brain.root);
|
|
7236
|
-
const now = /* @__PURE__ */ new Date();
|
|
7237
|
-
const id = newId();
|
|
7238
|
-
const month = now.toISOString().slice(0, 7);
|
|
7239
|
-
const dir = `${brain.observationsDir.replace(/\/+$/, "")}/${month}`;
|
|
7240
|
-
const path = `${dir}/obs-${id}.md`;
|
|
7241
|
-
const content = renderObservation({
|
|
7242
|
-
id,
|
|
7243
|
-
text: body,
|
|
7244
|
-
now,
|
|
7245
|
-
...opts.scope ? { scope: opts.scope } : {}
|
|
7246
|
-
});
|
|
7247
|
-
const record = await withWriteLock(brain.root, async () => {
|
|
7248
|
-
const adapter = await LocalFileCasAdapter.openVault(brain.root, {
|
|
7249
|
-
// Writes are confined to the observations folder. `remember` creates;
|
|
7250
|
-
// it has no business being able to author a file anywhere in someone's
|
|
7251
|
-
// vault, and the adapter enforces that rather than this command
|
|
7252
|
-
// promising it.
|
|
7253
|
-
createPrefix: brain.observationsDir
|
|
7254
|
-
});
|
|
7255
|
-
const result = await adapter.create(path, content);
|
|
7256
|
-
if (!result.ok) {
|
|
9058
|
+
).action(
|
|
9059
|
+
async (text, opts) => {
|
|
9060
|
+
const body = stripControl(text).trim();
|
|
9061
|
+
if (body === "") {
|
|
7257
9062
|
throw memgError(
|
|
7258
9063
|
"invalid_input",
|
|
7259
|
-
|
|
7260
|
-
|
|
9064
|
+
"nothing to remember",
|
|
9065
|
+
'Pass the text: `memgineering remember "deploys are manual \u2014 launchctl by hand"`'
|
|
7261
9066
|
);
|
|
7262
9067
|
}
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
hashBefore: null,
|
|
7268
|
-
hashAfter: hashContent(content),
|
|
7269
|
-
rationale,
|
|
7270
|
-
now
|
|
9068
|
+
const rationale = sanitizeRationale(opts.reason);
|
|
9069
|
+
const target = await resolveTarget({
|
|
9070
|
+
...opts.local === void 0 ? {} : { local: opts.local },
|
|
9071
|
+
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
7271
9072
|
});
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
9073
|
+
if (target.kind === "cloud") {
|
|
9074
|
+
const cloud = await openCloudBrain(target);
|
|
9075
|
+
const written = await cloudRemember(cloud, {
|
|
9076
|
+
text: body,
|
|
9077
|
+
...opts.scope ? { scope: opts.scope } : {},
|
|
9078
|
+
...rationale ? { reason: rationale } : {}
|
|
9079
|
+
});
|
|
9080
|
+
printDual({
|
|
9081
|
+
json: {
|
|
9082
|
+
id: written.id,
|
|
9083
|
+
path: written.path,
|
|
9084
|
+
op_id: written.op_id,
|
|
9085
|
+
scope: written.scope,
|
|
9086
|
+
brain: { hosted: true, name: target.name }
|
|
9087
|
+
},
|
|
9088
|
+
human: () => {
|
|
9089
|
+
printHuman(`Remembered in ${target.name}.
|
|
7277
9090
|
`);
|
|
7278
|
-
|
|
7279
|
-
|
|
9091
|
+
printHuman(`note: \`${written.id}\` \xB7 \`${written.path}\``);
|
|
9092
|
+
printHuman(c.gray(`undo: \`memgineering undo ${written.op_id}\``));
|
|
9093
|
+
}
|
|
9094
|
+
});
|
|
9095
|
+
return;
|
|
7280
9096
|
}
|
|
7281
|
-
|
|
7282
|
-
|
|
9097
|
+
const brain = target.brain;
|
|
9098
|
+
await openBrainOrExplain(brain.root);
|
|
9099
|
+
const now = /* @__PURE__ */ new Date();
|
|
9100
|
+
const id = newId();
|
|
9101
|
+
const month = now.toISOString().slice(0, 7);
|
|
9102
|
+
const dir = `${brain.observationsDir.replace(/\/+$/, "")}/${month}`;
|
|
9103
|
+
const path = `${dir}/obs-${id}.md`;
|
|
9104
|
+
const content = renderObservation({
|
|
9105
|
+
id,
|
|
9106
|
+
text: body,
|
|
9107
|
+
now,
|
|
9108
|
+
...opts.scope ? { scope: opts.scope } : {}
|
|
9109
|
+
});
|
|
9110
|
+
const record = await withWriteLock(brain.root, async () => {
|
|
9111
|
+
const adapter = await LocalFileCasAdapter.openVault(brain.root, {
|
|
9112
|
+
// Writes are confined to the observations folder. `remember` creates;
|
|
9113
|
+
// it has no business being able to author a file anywhere in someone's
|
|
9114
|
+
// vault, and the adapter enforces that rather than this command
|
|
9115
|
+
// promising it.
|
|
9116
|
+
createPrefix: brain.observationsDir
|
|
9117
|
+
});
|
|
9118
|
+
const result = await adapter.create(path, content);
|
|
9119
|
+
if (!result.ok) {
|
|
9120
|
+
throw memgError(
|
|
9121
|
+
"invalid_input",
|
|
9122
|
+
`a note already exists at ${path}`,
|
|
9123
|
+
"This should not happen \u2014 ids are random. Run the command again."
|
|
9124
|
+
);
|
|
9125
|
+
}
|
|
9126
|
+
return appendOp(brain.root, {
|
|
9127
|
+
verb: "remember",
|
|
9128
|
+
target: path,
|
|
9129
|
+
memoryId: id,
|
|
9130
|
+
hashBefore: null,
|
|
9131
|
+
hashAfter: hashContent(content),
|
|
9132
|
+
rationale,
|
|
9133
|
+
now
|
|
9134
|
+
});
|
|
9135
|
+
});
|
|
9136
|
+
printDual({
|
|
9137
|
+
json: { id, path, op_id: record.op_id, scope: opts.scope ?? null },
|
|
9138
|
+
human: () => {
|
|
9139
|
+
printHuman(`Remembered.
|
|
9140
|
+
`);
|
|
9141
|
+
printHuman(`note: \`${id}\` \xB7 \`${path}\``);
|
|
9142
|
+
printHuman(c.gray(`undo: \`${undoHint(record.op_id, opts.vault)}\``));
|
|
9143
|
+
}
|
|
9144
|
+
});
|
|
9145
|
+
}
|
|
9146
|
+
);
|
|
7283
9147
|
}
|
|
7284
9148
|
function renderObservation(args) {
|
|
7285
9149
|
const firstLine = args.text.split("\n")[0] ?? args.text;
|
|
@@ -7310,163 +9174,170 @@ function yamlString(value) {
|
|
|
7310
9174
|
|
|
7311
9175
|
// src/commands/resurface.ts
|
|
7312
9176
|
init_src();
|
|
7313
|
-
import { Command as
|
|
9177
|
+
import { Command as Command16 } from "commander";
|
|
7314
9178
|
init_index_build();
|
|
7315
9179
|
init_vault();
|
|
7316
9180
|
init_ui();
|
|
7317
9181
|
function resurfaceCommand() {
|
|
7318
|
-
return new
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
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
|
-
|
|
9182
|
+
return new Command16("resurface").description("what is worth having in view right now, without being asked").option("--context-dir <path>", "where the work is happening (default: cwd)").option("--limit <n>", "how many cards", (v) => parseInt(v, 10), 5).option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(
|
|
9183
|
+
async (opts) => {
|
|
9184
|
+
const contextDir = opts.contextDir ?? process.cwd();
|
|
9185
|
+
const where = await resolveTarget({
|
|
9186
|
+
...opts.local === void 0 ? {} : { local: opts.local },
|
|
9187
|
+
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
9188
|
+
});
|
|
9189
|
+
if (where.kind === "cloud") {
|
|
9190
|
+
await resurfaceHosted(where, opts.limit);
|
|
9191
|
+
return;
|
|
9192
|
+
}
|
|
9193
|
+
const { brain } = await resolveBrain(opts.vault, contextDir);
|
|
9194
|
+
const adapter = await openBrainOrExplain(brain.root);
|
|
9195
|
+
const built = await openIndex(adapter, brain.root);
|
|
9196
|
+
const events = await readEvents(brain.root);
|
|
9197
|
+
const now = /* @__PURE__ */ new Date();
|
|
9198
|
+
const ranked = rank(built.entries, events, {
|
|
9199
|
+
now,
|
|
9200
|
+
// The CLI is the only side that knows where a question was asked from,
|
|
9201
|
+
// so it is the only side that can answer this.
|
|
9202
|
+
here: (event) => isInside(event.context_dir, contextDir)
|
|
9203
|
+
});
|
|
9204
|
+
const TEMPLATE_SLOTS = 2;
|
|
9205
|
+
const untouchedPaths = await untouchedBase(
|
|
9206
|
+
adapter,
|
|
9207
|
+
ranked.map((r) => r.entry.path)
|
|
9208
|
+
);
|
|
9209
|
+
const isTemplate = (r) => untouchedPaths.has(r.entry.path);
|
|
9210
|
+
const templates = ranked.filter(isTemplate);
|
|
9211
|
+
const written = ranked.filter((r) => !isTemplate(r));
|
|
9212
|
+
const shown = [
|
|
9213
|
+
...templates.slice(0, TEMPLATE_SLOTS),
|
|
9214
|
+
...written.slice(0, Math.max(0, opts.limit - Math.min(TEMPLATE_SLOTS, templates.length)))
|
|
9215
|
+
].sort((a, b) => ranked.indexOf(a) - ranked.indexOf(b)).slice(0, opts.limit);
|
|
9216
|
+
const rankedShown = shown.length > 0 ? shown : ranked.slice(0, opts.limit);
|
|
9217
|
+
const untouched = untouchedPaths;
|
|
9218
|
+
const allIds = built.entries.map((e) => e.memory.id);
|
|
9219
|
+
await recordEvent(brain.root, {
|
|
9220
|
+
verb: "resurface",
|
|
9221
|
+
contextDir,
|
|
9222
|
+
detail: "card",
|
|
9223
|
+
limit: opts.limit,
|
|
9224
|
+
returnedIds: rankedShown.map((r) => r.entry.memory.id),
|
|
9225
|
+
now
|
|
9226
|
+
});
|
|
9227
|
+
printDual({
|
|
9228
|
+
json: {
|
|
9229
|
+
context_dir: contextDir,
|
|
9230
|
+
cards: rankedShown.map(({ entry, score, why }) => ({
|
|
9231
|
+
id: entry.memory.id,
|
|
9232
|
+
handle: displayHandle(entry.memory.id, allIds),
|
|
9233
|
+
title: entry.memory.title,
|
|
9234
|
+
summary: entry.memory.summary,
|
|
9235
|
+
path: entry.path,
|
|
9236
|
+
score: Number(score.toFixed(3)),
|
|
9237
|
+
why,
|
|
9238
|
+
unfilled: untouched.has(entry.path)
|
|
9239
|
+
}))
|
|
9240
|
+
},
|
|
9241
|
+
human: () => {
|
|
9242
|
+
if (rankedShown.length === 0) {
|
|
9243
|
+
printHuman("## resurface\n");
|
|
9244
|
+
printHuman("_Nothing to bring up yet._\n");
|
|
7371
9245
|
printHuman(
|
|
7372
9246
|
c.gray(
|
|
7373
|
-
|
|
7374
|
-
This is still the file \`init\` created \u2014 the line below is its prompt, not
|
|
7375
|
-
something you wrote. Fill it in and it becomes a memory like any other.`
|
|
9247
|
+
"This ranks by what has been recalled here before, so it gets useful once the brain has been used a few times."
|
|
7376
9248
|
)
|
|
7377
9249
|
);
|
|
9250
|
+
return;
|
|
7378
9251
|
}
|
|
7379
|
-
|
|
7380
|
-
${quoteBlock(entry.memory.summary)}`);
|
|
7381
|
-
printHuman(`
|
|
7382
|
-
\`open: ${displayHandle(entry.memory.id, allIds)}\`
|
|
9252
|
+
printHuman(`## resurface (${rankedShown.length} cards)
|
|
7383
9253
|
`);
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
}
|
|
7416
|
-
}
|
|
7417
|
-
const hasBase = entries.some((e) => e.path.startsWith("01_BASE/"));
|
|
7418
|
-
const scored = [];
|
|
7419
|
-
for (const entry of entries) {
|
|
7420
|
-
const id = entry.memory.id;
|
|
7421
|
-
const freshness = freshnessOf(entry.memory, entry.claims, opts.now);
|
|
7422
|
-
if (freshness === "retired") continue;
|
|
7423
|
-
const why = [];
|
|
7424
|
-
let score = 0;
|
|
7425
|
-
const hereCount = hereCounts.get(id) ?? 0;
|
|
7426
|
-
if (hereCount > 0) {
|
|
7427
|
-
score += WEIGHT.contextRecall * Math.log2(1 + hereCount);
|
|
7428
|
-
why.push(`recalled ${hereCount}\xD7 in this folder`);
|
|
7429
|
-
}
|
|
7430
|
-
const anyCount = (anyCounts.get(id) ?? 0) - hereCount;
|
|
7431
|
-
if (anyCount > 0) {
|
|
7432
|
-
score += WEIGHT.anyRecall * Math.log2(1 + anyCount);
|
|
7433
|
-
if (hereCount === 0) why.push(`recalled ${anyCount}\xD7 elsewhere`);
|
|
7434
|
-
}
|
|
7435
|
-
const touched = lastTouched.get(id);
|
|
7436
|
-
if (touched !== void 0) {
|
|
7437
|
-
const days = (opts.now.getTime() - touched) / 864e5;
|
|
7438
|
-
score += WEIGHT.recency * Math.pow(0.5, days / 7);
|
|
9254
|
+
const noEvidenceYet = !rankedShown.some(
|
|
9255
|
+
({ why }) => why.some((w) => w.startsWith("recalled"))
|
|
9256
|
+
);
|
|
9257
|
+
for (const [i, { entry, why }] of rankedShown.entries()) {
|
|
9258
|
+
const blank = untouched.has(entry.path);
|
|
9259
|
+
printHuman(`### ${i + 1}. ${entry.memory.title}`);
|
|
9260
|
+
printHuman(blank ? c.yellow("not filled in yet") : why.join(" \xB7 "));
|
|
9261
|
+
if (blank) {
|
|
9262
|
+
const hasWritten = rankedShown.some((r) => !untouched.has(r.entry.path));
|
|
9263
|
+
printHuman(
|
|
9264
|
+
c.gray(
|
|
9265
|
+
hasWritten ? `
|
|
9266
|
+
Still the template \`init\` wrote \u2014 the line below is its prompt.` : `
|
|
9267
|
+
This is still the file \`init\` created \u2014 the line below is its prompt, not
|
|
9268
|
+
something you wrote. Fill it in and it becomes a memory like any other.`
|
|
9269
|
+
)
|
|
9270
|
+
);
|
|
9271
|
+
}
|
|
9272
|
+
if (entry.memory.summary) printHuman(`
|
|
9273
|
+
${quoteBlock(entry.memory.summary)}`);
|
|
9274
|
+
printHuman(`
|
|
9275
|
+
\`open: ${displayHandle(entry.memory.id, allIds)}\`
|
|
9276
|
+
`);
|
|
9277
|
+
}
|
|
9278
|
+
printHuman(
|
|
9279
|
+
c.gray(
|
|
9280
|
+
(noEvidenceYet ? "Nothing has been recalled in this folder yet, so the order above is what the brain holds rather than what matters here.\n" : "") + 'These are the user\u2019s own notes and decisions \u2014 follow them without being asked, and\nsay so if you are about to do something one of them rules out. Before answering\nanything that sounds already settled, `memgineering recall "<their words>"`; when\nthis session decides something durable, `memgineering remember "<it>" --reason "<why>"`.'
|
|
9281
|
+
)
|
|
9282
|
+
);
|
|
9283
|
+
}
|
|
9284
|
+
});
|
|
7439
9285
|
}
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
9286
|
+
);
|
|
9287
|
+
}
|
|
9288
|
+
async function resurfaceHosted(where, limit) {
|
|
9289
|
+
const brain = await openCloudBrain(where);
|
|
9290
|
+
const result = await cloudResurface(brain, { limit });
|
|
9291
|
+
printDual({
|
|
9292
|
+
json: {
|
|
9293
|
+
brain: where.name,
|
|
9294
|
+
cards: result.cards,
|
|
9295
|
+
ranked_by: result.ranked_by,
|
|
9296
|
+
...result.hint ? { hint: result.hint } : {}
|
|
9297
|
+
},
|
|
9298
|
+
human: () => {
|
|
9299
|
+
if (result.cards.length === 0) {
|
|
9300
|
+
printHuman(`## resurface \u2014 ${where.name}
|
|
9301
|
+
`);
|
|
9302
|
+
printHuman("_Nothing to bring up yet._\n");
|
|
9303
|
+
if (result.hint) printHuman(c.gray(result.hint));
|
|
9304
|
+
return;
|
|
9305
|
+
}
|
|
9306
|
+
printHuman(`## resurface \u2014 ${where.name} (${result.cards.length} cards)
|
|
9307
|
+
`);
|
|
9308
|
+
for (const [i, card] of result.cards.entries()) {
|
|
9309
|
+
printHuman(`### ${i + 1}. ${card.title}`);
|
|
9310
|
+
if (card.why.length > 0) printHuman(card.why.join(" \xB7 "));
|
|
9311
|
+
if (card.summary) printHuman(`
|
|
9312
|
+
${quoteBlock(card.summary)}`);
|
|
9313
|
+
printHuman(`
|
|
9314
|
+
\`open: ${card.handle}\`
|
|
9315
|
+
`);
|
|
7447
9316
|
}
|
|
9317
|
+
printHuman(
|
|
9318
|
+
c.gray(
|
|
9319
|
+
`Ranked by ${result.ranked_by.join(", ")}. A hosted brain does not record where a
|
|
9320
|
+
question was asked from, so "in this folder" is not one of them \u2014 that signal
|
|
9321
|
+
exists only for a brain on this machine.`
|
|
9322
|
+
)
|
|
9323
|
+
);
|
|
7448
9324
|
}
|
|
7449
|
-
|
|
7450
|
-
if (score > 0) scored.push({ entry, score, why });
|
|
7451
|
-
}
|
|
7452
|
-
return scored.sort(
|
|
7453
|
-
(a, b) => b.score - a.score || a.entry.memory.id.localeCompare(b.entry.memory.id)
|
|
7454
|
-
);
|
|
9325
|
+
});
|
|
7455
9326
|
}
|
|
7456
9327
|
|
|
7457
9328
|
// src/commands/revise.ts
|
|
7458
9329
|
init_src();
|
|
7459
|
-
import { readFile as
|
|
7460
|
-
import { Command as
|
|
9330
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
9331
|
+
import { Command as Command17 } from "commander";
|
|
7461
9332
|
init_errors();
|
|
7462
9333
|
init_index_build();
|
|
7463
9334
|
init_vault();
|
|
7464
9335
|
init_ui();
|
|
7465
9336
|
function reviseCommand() {
|
|
7466
|
-
return new
|
|
9337
|
+
return new Command17("revise").description("change a memory's conclusion, and record how to undo it").argument("<ref>", "handle, id, path, or exact title").option("--claim <text>", "what is now true").option("--summary <text>", "the one line recall will show (defaults to the claim)").option("--title <text>", "the heading recall shows above it (unchanged when omitted)").option(
|
|
7467
9338
|
"--valid-from <iso>",
|
|
7468
9339
|
"when this became true (defaults: supersede = now, otherwise unchanged)"
|
|
7469
|
-
).option("--action <kind>", "reinforce | supersede | conflict", "supersede").option("--contradicts <ids...>", "memories this disagrees with").option("--reason <text>", "why this change is right").option("--because <text>", "the passage from the note that justifies it").option("--input <json>", "the whole change as JSON (file path, `-`, or inline)").option("--dry-run", "show the diff and write nothing").option("--vault <path>", "which brain to write to").addHelpText(
|
|
9340
|
+
).option("--action <kind>", "reinforce | supersede | conflict", "supersede").option("--contradicts <ids...>", "memories this disagrees with").option("--reason <text>", "why this change is right").option("--because <text>", "the passage from the note that justifies it").option("--input <json>", "the whole change as JSON (file path, `-`, or inline)").option("--dry-run", "show the diff and write nothing").option("--vault <path>", "which brain to write to").option("--local", "use the brain on this machine, even when signed in to a hosted one").addHelpText(
|
|
7470
9341
|
"after",
|
|
7471
9342
|
[
|
|
7472
9343
|
"",
|
|
@@ -7482,6 +9353,14 @@ function reviseCommand() {
|
|
|
7482
9353
|
"always true, you have only just been told it. `supersede` would date it today."
|
|
7483
9354
|
].join("\n")
|
|
7484
9355
|
).action(async (ref, opts) => {
|
|
9356
|
+
const where = await resolveTarget({
|
|
9357
|
+
...opts.local === void 0 ? {} : { local: opts.local },
|
|
9358
|
+
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
9359
|
+
});
|
|
9360
|
+
if (where.kind === "cloud") {
|
|
9361
|
+
await reviseHosted(where, ref, opts);
|
|
9362
|
+
return;
|
|
9363
|
+
}
|
|
7485
9364
|
const { brain } = await resolveBrain(opts.vault);
|
|
7486
9365
|
await openBrainOrExplain(brain.root);
|
|
7487
9366
|
const adapter = await LocalFileCasAdapter.openVault(brain.root, {
|
|
@@ -7502,7 +9381,7 @@ function reviseCommand() {
|
|
|
7502
9381
|
);
|
|
7503
9382
|
}
|
|
7504
9383
|
const proposal = result.proposal;
|
|
7505
|
-
if (proposal.action !== "conflict" && rationale === null && saysNothingNew(entry, input)) {
|
|
9384
|
+
if (proposal.action !== "conflict" && rationale === null && saysNothingNew(entry, input, stripControl)) {
|
|
7506
9385
|
throw memgError(
|
|
7507
9386
|
"invalid_input",
|
|
7508
9387
|
`${entry.memory.title} already says this \u2014 nothing was written`,
|
|
@@ -7572,7 +9451,12 @@ To see what is there now: \`memgineering open ` + ref + "`"
|
|
|
7572
9451
|
});
|
|
7573
9452
|
return { record, content: outcome.content };
|
|
7574
9453
|
});
|
|
7575
|
-
const drift = proposal.action === "supersede" ? bodyStillAsserts(
|
|
9454
|
+
const drift = proposal.action === "supersede" ? bodyStillAsserts(
|
|
9455
|
+
applied.content,
|
|
9456
|
+
previousClaim(entry),
|
|
9457
|
+
proposal.claim?.text ?? null,
|
|
9458
|
+
stripControl
|
|
9459
|
+
) : null;
|
|
7576
9460
|
const unwrittenBody = bodyStillTemplate(entry.path, applied.content);
|
|
7577
9461
|
printDual({
|
|
7578
9462
|
json: {
|
|
@@ -7632,7 +9516,95 @@ Undo: \`${undoHint(applied.record.op_id, opts.vault)}\``));
|
|
|
7632
9516
|
});
|
|
7633
9517
|
});
|
|
7634
9518
|
}
|
|
7635
|
-
|
|
9519
|
+
async function reviseHosted(where, ref, opts) {
|
|
9520
|
+
if (opts.input) {
|
|
9521
|
+
throw memgError(
|
|
9522
|
+
"invalid_input",
|
|
9523
|
+
"--input is not supported against a hosted brain",
|
|
9524
|
+
"It can name a different memory and set fields the flags do not expose, so honouring part of it would report a change nobody asked for. Pass the change as flags (`--claim`, `--summary`, `--action`), or use `--local` to apply the JSON to the brain on this machine. Nothing was sent."
|
|
9525
|
+
);
|
|
9526
|
+
}
|
|
9527
|
+
if (!opts.claim) {
|
|
9528
|
+
throw memgError(
|
|
9529
|
+
"invalid_input",
|
|
9530
|
+
"revise needs to know what is now true",
|
|
9531
|
+
'Pass `--claim "<the new conclusion>"`.'
|
|
9532
|
+
);
|
|
9533
|
+
}
|
|
9534
|
+
const brain = await openCloudBrain(where);
|
|
9535
|
+
const result = await cloudRevise(brain, {
|
|
9536
|
+
ref,
|
|
9537
|
+
claim: opts.claim,
|
|
9538
|
+
action: opts.action,
|
|
9539
|
+
...opts.summary === void 0 ? {} : { summary: opts.summary },
|
|
9540
|
+
...opts.title === void 0 ? {} : { title: opts.title },
|
|
9541
|
+
...opts.validFrom === void 0 ? {} : { validFrom: opts.validFrom },
|
|
9542
|
+
...opts.contradicts === void 0 ? {} : { contradicts: opts.contradicts },
|
|
9543
|
+
...opts.reason === void 0 ? {} : { reason: opts.reason },
|
|
9544
|
+
...opts.because === void 0 ? {} : { because: opts.because },
|
|
9545
|
+
...opts.dryRun === true ? { dryRun: true } : {}
|
|
9546
|
+
});
|
|
9547
|
+
printDual({
|
|
9548
|
+
json: { brain: where.name, ...result },
|
|
9549
|
+
human: () => {
|
|
9550
|
+
if (result.dry_run) {
|
|
9551
|
+
printHuman(`## revise (dry run) \u2014 ${where.name}
|
|
9552
|
+
`);
|
|
9553
|
+
printHuman(`\`${result.target}\`
|
|
9554
|
+
`);
|
|
9555
|
+
printHuman(renderDiff(result.before, result.after));
|
|
9556
|
+
printHuman(c.gray("\nNothing was written. Drop --dry-run to apply."));
|
|
9557
|
+
return;
|
|
9558
|
+
}
|
|
9559
|
+
if (opts.action === "conflict") {
|
|
9560
|
+
printHuman(`Recorded a disagreement in **${where.name}**.
|
|
9561
|
+
`);
|
|
9562
|
+
printHuman(`\`${result.target}\`
|
|
9563
|
+
`);
|
|
9564
|
+
printHuman(
|
|
9565
|
+
c.gray(
|
|
9566
|
+
"The claim you passed was NOT written. A conflict says two memories disagree\nand leaves both standing \u2014 recall surfaces them together so you can decide.\nTo change what this note concludes, use `--action supersede`.\n"
|
|
9567
|
+
)
|
|
9568
|
+
);
|
|
9569
|
+
} else {
|
|
9570
|
+
printHuman(`Revised in **${where.name}**.
|
|
9571
|
+
`);
|
|
9572
|
+
printHuman(`\`${result.target}\`
|
|
9573
|
+
`);
|
|
9574
|
+
}
|
|
9575
|
+
printHuman(renderDiff(result.before, result.after));
|
|
9576
|
+
if (result.body_drift) {
|
|
9577
|
+
printHuman(
|
|
9578
|
+
c.yellow(`
|
|
9579
|
+
\u26A0 the body still reads "${result.body_drift}"`) + c.gray(
|
|
9580
|
+
"\n \u2014 the frontmatter now concludes differently. The prose is yours and was not\n touched; edit it yourself if it should agree."
|
|
9581
|
+
)
|
|
9582
|
+
);
|
|
9583
|
+
}
|
|
9584
|
+
if (result.critical_target) {
|
|
9585
|
+
printHuman(
|
|
9586
|
+
c.yellow(
|
|
9587
|
+
`
|
|
9588
|
+
\u26A0 critical target \u2014 this note defines how the agent behaves. Tell your owner what changed.`
|
|
9589
|
+
)
|
|
9590
|
+
);
|
|
9591
|
+
}
|
|
9592
|
+
if (result.reverse?.supported && result.reverse.body?.claim) {
|
|
9593
|
+
printHuman(
|
|
9594
|
+
c.gray(
|
|
9595
|
+
`
|
|
9596
|
+
To put it back: \`memgineering revise ${ref} --claim ${JSON.stringify(
|
|
9597
|
+
result.reverse.body.claim
|
|
9598
|
+
)}\``
|
|
9599
|
+
)
|
|
9600
|
+
);
|
|
9601
|
+
} else if (result.reverse?.hint) {
|
|
9602
|
+
printHuman(c.gray(`
|
|
9603
|
+
${result.reverse.hint}`));
|
|
9604
|
+
}
|
|
9605
|
+
}
|
|
9606
|
+
});
|
|
9607
|
+
}
|
|
7636
9608
|
async function buildCommitInput(entry, opts) {
|
|
7637
9609
|
if (opts.input) {
|
|
7638
9610
|
const raw = await readInput(opts.input);
|
|
@@ -7653,104 +9625,20 @@ async function buildCommitInput(entry, opts) {
|
|
|
7653
9625
|
"reinforce (same conclusion, new support) \xB7 supersede (replaces it) \xB7 conflict (records a disagreement without deciding).\nTo change standing instead, use `retire` or `unretire`."
|
|
7654
9626
|
);
|
|
7655
9627
|
}
|
|
7656
|
-
return {
|
|
9628
|
+
return reviseInput(entry, {
|
|
7657
9629
|
action,
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
// ran. A tester added a reason to a memory and watched its start date
|
|
7667
|
-
// move a day later than the thing it describes. `valid_from` is the field
|
|
7668
|
-
// a temporal memory store exists to get right, and it was being used as
|
|
7669
|
-
// an updated-at; the drift is silent and cumulative.
|
|
7670
|
-
//
|
|
7671
|
-
// `supersede` still stamps now, because a replaced conclusion IS starting
|
|
7672
|
-
// now — that is what supersede means. `reinforce` and `conflict` keep
|
|
7673
|
-
// whatever the memory already declared, and `--valid-from` overrides
|
|
7674
|
-
// either when the caller knows the real date.
|
|
7675
|
-
valid_from: validFrom(action, opts, entry),
|
|
7676
|
-
confidence: "stated",
|
|
7677
|
-
summary: action === "conflict" ? null : opts.summary ?? opts.claim,
|
|
7678
|
-
// Never defaulted from the claim, unlike the summary. A summary that is
|
|
7679
|
-
// missing makes the change invisible, so filling it in is a rescue; a
|
|
7680
|
-
// title that is missing simply means the author's heading still fits, and
|
|
7681
|
-
// overwriting it would be rewriting their classification uninvited.
|
|
7682
|
-
title: action === "conflict" ? null : opts.title ?? null
|
|
7683
|
-
},
|
|
7684
|
-
relations: {
|
|
7685
|
-
depends_on: [],
|
|
7686
|
-
relates_to: [],
|
|
7687
|
-
contradicts: opts.contradicts ?? []
|
|
7688
|
-
},
|
|
7689
|
-
reason: opts.reason ?? NO_REASON_GIVEN,
|
|
7690
|
-
// The engine refuses to submit without an excerpt, because approving a
|
|
7691
|
-
// diff on an assertion alone is not consent. Nothing here is approved by a
|
|
7692
|
-
// person, but the requirement still earns its keep: it forces the record
|
|
7693
|
-
// to say what the change was based on rather than only what it did.
|
|
7694
|
-
source_excerpt: opts.because ?? entry.memory.summary ?? entry.memory.title,
|
|
7695
|
-
target_path: entry.path
|
|
7696
|
-
};
|
|
7697
|
-
}
|
|
7698
|
-
function asSingleMatch(entry) {
|
|
7699
|
-
return {
|
|
7700
|
-
candidate: {
|
|
7701
|
-
subject: entry.memory.title,
|
|
7702
|
-
scope: entry.memory.scope,
|
|
7703
|
-
kind: entry.memory.type,
|
|
7704
|
-
candidate_claim: entry.memory.summary ?? entry.memory.title,
|
|
7705
|
-
entities: [],
|
|
7706
|
-
importance: "normal"
|
|
7707
|
-
},
|
|
7708
|
-
matches: [
|
|
7709
|
-
{
|
|
7710
|
-
id: entry.memory.id,
|
|
7711
|
-
title: entry.memory.title,
|
|
7712
|
-
summary: entry.memory.summary,
|
|
7713
|
-
path: entry.path,
|
|
7714
|
-
revision: entry.revision,
|
|
7715
|
-
freshness: freshnessOf(entry.memory, entry.claims, /* @__PURE__ */ new Date()),
|
|
7716
|
-
score: 1
|
|
7717
|
-
}
|
|
7718
|
-
],
|
|
7719
|
-
contradicts: [],
|
|
7720
|
-
scopes: entry.memory.scope ? [entry.memory.scope] : []
|
|
7721
|
-
};
|
|
9630
|
+
claim: opts.claim,
|
|
9631
|
+
...opts.summary === void 0 ? {} : { summary: opts.summary },
|
|
9632
|
+
...opts.title === void 0 ? {} : { title: opts.title },
|
|
9633
|
+
...opts.validFrom === void 0 ? {} : { validFrom: opts.validFrom },
|
|
9634
|
+
...opts.contradicts === void 0 ? {} : { contradicts: opts.contradicts },
|
|
9635
|
+
...opts.reason === void 0 ? {} : { reason: opts.reason },
|
|
9636
|
+
...opts.because === void 0 ? {} : { because: opts.because }
|
|
9637
|
+
});
|
|
7722
9638
|
}
|
|
7723
9639
|
function isCritical(entry) {
|
|
7724
9640
|
return /^01_BASE\//.test(entry.path);
|
|
7725
9641
|
}
|
|
7726
|
-
function previousClaim(entry) {
|
|
7727
|
-
const current = entry.claims.find((claim) => claim.id === entry.memory.current_claim) ?? entry.claims[0];
|
|
7728
|
-
return current?.text ?? null;
|
|
7729
|
-
}
|
|
7730
|
-
function saysNothingNew(entry, input) {
|
|
7731
|
-
const norm = (v) => stripControl(v ?? "").trim();
|
|
7732
|
-
const claim = input.claim;
|
|
7733
|
-
if (!claim || norm(claim.text) === "") return false;
|
|
7734
|
-
if (norm(claim.text) !== norm(previousClaim(entry))) return false;
|
|
7735
|
-
if (claim.summary != null && norm(claim.summary) !== norm(entry.memory.summary)) return false;
|
|
7736
|
-
if (claim.title != null && norm(claim.title) !== norm(entry.memory.title)) return false;
|
|
7737
|
-
const declared = new Set(
|
|
7738
|
-
entry.relations.filter((r) => r.type === "contradicts").map((r) => r.to)
|
|
7739
|
-
);
|
|
7740
|
-
return (input.relations?.contradicts ?? []).every((id) => declared.has(id));
|
|
7741
|
-
}
|
|
7742
|
-
function bodyStillAsserts(content, oldClaim, newClaim) {
|
|
7743
|
-
if (oldClaim === null) return null;
|
|
7744
|
-
const line = stripControl(oldClaim.split("\n")[0] ?? "").trim();
|
|
7745
|
-
if (line === "") return null;
|
|
7746
|
-
if (line === stripControl(newClaim?.split("\n")[0] ?? "").trim()) return null;
|
|
7747
|
-
return bodyOf2(content).includes(line) ? line : null;
|
|
7748
|
-
}
|
|
7749
|
-
function bodyOf2(content) {
|
|
7750
|
-
const stripped = content.replace(/^/, "");
|
|
7751
|
-
const fence = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
|
|
7752
|
-
return fence ? stripped.slice(fence[0].length) : stripped;
|
|
7753
|
-
}
|
|
7754
9642
|
function renderDiff(before, after) {
|
|
7755
9643
|
return ["```diff", ...lines(before, "-"), ...lines(after, "+"), "```"].join("\n");
|
|
7756
9644
|
}
|
|
@@ -7758,13 +9646,13 @@ function lines(text, marker) {
|
|
|
7758
9646
|
return text === "" ? [] : text.split("\n").map((l) => `${marker} ${l}`);
|
|
7759
9647
|
}
|
|
7760
9648
|
async function readInput(source) {
|
|
7761
|
-
const raw = source === "-" ? await new Promise((
|
|
9649
|
+
const raw = source === "-" ? await new Promise((resolve11, reject) => {
|
|
7762
9650
|
let buf = "";
|
|
7763
9651
|
process.stdin.setEncoding("utf8");
|
|
7764
9652
|
process.stdin.on("data", (chunk) => buf += chunk);
|
|
7765
|
-
process.stdin.on("end", () =>
|
|
9653
|
+
process.stdin.on("end", () => resolve11(buf));
|
|
7766
9654
|
process.stdin.on("error", reject);
|
|
7767
|
-
}) : await
|
|
9655
|
+
}) : await readFile17(source, "utf8").catch(() => source);
|
|
7768
9656
|
try {
|
|
7769
9657
|
return JSON.parse(raw);
|
|
7770
9658
|
} catch {
|
|
@@ -7775,25 +9663,19 @@ async function readInput(source) {
|
|
|
7775
9663
|
);
|
|
7776
9664
|
}
|
|
7777
9665
|
}
|
|
7778
|
-
function validFrom(action, opts, entry) {
|
|
7779
|
-
if (opts.validFrom) return opts.validFrom;
|
|
7780
|
-
if (action === "supersede") return (/* @__PURE__ */ new Date()).toISOString();
|
|
7781
|
-
const declared = entry.claims.find((c2) => c2.valid_from)?.valid_from;
|
|
7782
|
-
return declared ?? null;
|
|
7783
|
-
}
|
|
7784
9666
|
|
|
7785
9667
|
// src/program.ts
|
|
7786
9668
|
init_setup();
|
|
7787
9669
|
|
|
7788
9670
|
// src/commands/update.ts
|
|
7789
|
-
import { Command as
|
|
9671
|
+
import { Command as Command19 } from "commander";
|
|
7790
9672
|
|
|
7791
9673
|
// src/version.ts
|
|
7792
9674
|
import { readFileSync } from "fs";
|
|
7793
9675
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7794
|
-
import { dirname as
|
|
7795
|
-
var __dir =
|
|
7796
|
-
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");
|
|
7797
9679
|
var VERSION = (() => {
|
|
7798
9680
|
try {
|
|
7799
9681
|
const parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
@@ -7809,7 +9691,7 @@ init_config();
|
|
|
7809
9691
|
// src/lib/provenance.ts
|
|
7810
9692
|
import { execFile } from "child_process";
|
|
7811
9693
|
import { realpath as realpath3 } from "fs/promises";
|
|
7812
|
-
import { dirname as
|
|
9694
|
+
import { dirname as dirname6, join as join20, resolve as resolve9 } from "path";
|
|
7813
9695
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7814
9696
|
import { promisify } from "util";
|
|
7815
9697
|
var run = promisify(execFile);
|
|
@@ -7830,7 +9712,7 @@ var MANAGERS = [
|
|
|
7830
9712
|
];
|
|
7831
9713
|
async function detectProvenance(selfPath) {
|
|
7832
9714
|
const entry = selfPath ?? fileURLToPath3(import.meta.url);
|
|
7833
|
-
const selfDir = await realpath3(
|
|
9715
|
+
const selfDir = await realpath3(dirname6(entry)).catch(() => dirname6(entry));
|
|
7834
9716
|
if (/[\\/]_npx[\\/]|[\\/]\.npm[\\/]_cacache[\\/]/.test(selfDir)) {
|
|
7835
9717
|
return {
|
|
7836
9718
|
kind: "npx",
|
|
@@ -7875,8 +9757,8 @@ async function prefixOf(bin, args) {
|
|
|
7875
9757
|
}
|
|
7876
9758
|
}
|
|
7877
9759
|
function isInside2(child, parent) {
|
|
7878
|
-
const c2 =
|
|
7879
|
-
const p =
|
|
9760
|
+
const c2 = resolve9(child);
|
|
9761
|
+
const p = resolve9(parent);
|
|
7880
9762
|
return c2 === p || c2.startsWith(p.endsWith("/") ? p : `${p}/`);
|
|
7881
9763
|
}
|
|
7882
9764
|
|
|
@@ -7884,16 +9766,16 @@ function isInside2(child, parent) {
|
|
|
7884
9766
|
init_atomic();
|
|
7885
9767
|
init_brand();
|
|
7886
9768
|
import { execFile as execFile2 } from "child_process";
|
|
7887
|
-
import { mkdir as
|
|
7888
|
-
import { join as
|
|
9769
|
+
import { mkdir as mkdir13, readFile as readFile19, writeFile as writeFile10 } from "fs/promises";
|
|
9770
|
+
import { join as join21 } from "path";
|
|
7889
9771
|
import { promisify as promisify2 } from "util";
|
|
7890
9772
|
import semver from "semver";
|
|
7891
|
-
import { z as
|
|
9773
|
+
import { z as z11 } from "zod";
|
|
7892
9774
|
var run2 = promisify2(execFile2);
|
|
7893
|
-
var PolicySchema =
|
|
7894
|
-
latest:
|
|
7895
|
-
minimum_supported:
|
|
7896
|
-
notice:
|
|
9775
|
+
var PolicySchema = z11.object({
|
|
9776
|
+
latest: z11.string().optional(),
|
|
9777
|
+
minimum_supported: z11.string().optional(),
|
|
9778
|
+
notice: z11.string().optional()
|
|
7897
9779
|
});
|
|
7898
9780
|
var POLICY_URL = "https://memgineering.com/version-policy.json";
|
|
7899
9781
|
var NOTICE_MAX = 300;
|
|
@@ -7958,19 +9840,19 @@ function shouldCheck(lastCheck, now) {
|
|
|
7958
9840
|
if (!Number.isFinite(last)) return true;
|
|
7959
9841
|
return now.getTime() - last > 864e5;
|
|
7960
9842
|
}
|
|
7961
|
-
var UpdateStateSchema =
|
|
7962
|
-
at:
|
|
7963
|
-
ok:
|
|
7964
|
-
from:
|
|
7965
|
-
to:
|
|
7966
|
-
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()
|
|
7967
9849
|
});
|
|
7968
9850
|
function statePath() {
|
|
7969
|
-
return
|
|
9851
|
+
return join21(brandHome(), "update-state.json");
|
|
7970
9852
|
}
|
|
7971
9853
|
async function readUpdateState() {
|
|
7972
9854
|
try {
|
|
7973
|
-
const parsed = UpdateStateSchema.safeParse(JSON.parse(await
|
|
9855
|
+
const parsed = UpdateStateSchema.safeParse(JSON.parse(await readFile19(statePath(), "utf8")));
|
|
7974
9856
|
return parsed.success ? parsed.data : null;
|
|
7975
9857
|
} catch {
|
|
7976
9858
|
return null;
|
|
@@ -7978,7 +9860,7 @@ async function readUpdateState() {
|
|
|
7978
9860
|
}
|
|
7979
9861
|
async function writeUpdateState(state) {
|
|
7980
9862
|
try {
|
|
7981
|
-
await
|
|
9863
|
+
await mkdir13(brandHome(), { recursive: true });
|
|
7982
9864
|
await writeThenRename(statePath(), `${JSON.stringify(state, null, 2)}
|
|
7983
9865
|
`);
|
|
7984
9866
|
} catch {
|
|
@@ -8034,7 +9916,7 @@ function shortMessage(err) {
|
|
|
8034
9916
|
// src/commands/update.ts
|
|
8035
9917
|
init_ui();
|
|
8036
9918
|
function updateCommand() {
|
|
8037
|
-
return new
|
|
9919
|
+
return new Command19("update").description("update memgineering itself").option("--check", "report what is available and change nothing").action(async (opts) => {
|
|
8038
9920
|
const status = await checkForUpdate(VERSION);
|
|
8039
9921
|
const provenance = await detectProvenance();
|
|
8040
9922
|
const previous = await readUpdateState();
|
|
@@ -8140,117 +10022,140 @@ function reportBelowMinimum(below) {
|
|
|
8140
10022
|
}
|
|
8141
10023
|
|
|
8142
10024
|
// src/commands/undo.ts
|
|
8143
|
-
import { readFile as
|
|
8144
|
-
import { join as
|
|
8145
|
-
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";
|
|
8146
10028
|
init_atomic();
|
|
8147
10029
|
init_errors();
|
|
8148
10030
|
init_vault();
|
|
8149
10031
|
init_ui();
|
|
8150
10032
|
function undoCommand() {
|
|
8151
|
-
return new
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
10033
|
+
return new Command20("undo").description("take back the last change, or a named one").argument("[op]", "operation id (default: the most recent)").option("--reason <text>", "why it is being taken back \u2014 kept in the ledger").option("--vault <path>", "which brain to act on").option("--local", "act on the brain on this machine, even when signed in to a hosted one").action(
|
|
10034
|
+
async (opId, opts) => {
|
|
10035
|
+
const rationale = sanitizeRationale(opts.reason);
|
|
10036
|
+
const where = await resolveTarget({
|
|
10037
|
+
...opts.local === void 0 ? {} : { local: opts.local },
|
|
10038
|
+
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
10039
|
+
});
|
|
10040
|
+
if (where.kind === "cloud") {
|
|
10041
|
+
const cloud = await openCloudBrain(where);
|
|
10042
|
+
const undone = await cloudUndo(cloud, {
|
|
10043
|
+
...opId === void 0 ? {} : { opId },
|
|
10044
|
+
...rationale ? { reason: rationale } : {}
|
|
10045
|
+
});
|
|
10046
|
+
printDual({
|
|
10047
|
+
json: { ...undone, brain: { hosted: true, name: where.name } },
|
|
10048
|
+
human: () => {
|
|
10049
|
+
printHuman(`Took back \`${undone.undone}\` \u2014 a ${undone.verb}.
|
|
10050
|
+
`);
|
|
10051
|
+
printHuman(c.gray(` ${undone.target}`));
|
|
10052
|
+
printHuman(c.gray(` recorded as \`${undone.op_id}\``));
|
|
10053
|
+
}
|
|
10054
|
+
});
|
|
10055
|
+
return;
|
|
10056
|
+
}
|
|
10057
|
+
const brain = where.brain;
|
|
10058
|
+
await openBrainOrExplain(brain.root);
|
|
10059
|
+
const { ops, skipped } = await readOps(brain.root);
|
|
10060
|
+
if (opId === void 0 && skipped > 0) {
|
|
10061
|
+
throw memgError(
|
|
10062
|
+
"ledger_divergence",
|
|
10063
|
+
`this brain has ${skipped} unreadable ledger line(s), so "the last change" is not knowable`,
|
|
10064
|
+
'Usually the tail of a run that was interrupted mid-write \u2014 and the line that was lost is normally the newest, so undoing "the last change" would reverse an older one instead. Run `memgineering log` to see what is readable, then undo by id.'
|
|
10065
|
+
);
|
|
10066
|
+
}
|
|
10067
|
+
const matches = opId ? matchIds(
|
|
10068
|
+
opId,
|
|
10069
|
+
ops.map((o) => o.op_id)
|
|
10070
|
+
) : [];
|
|
10071
|
+
if (matches.length > 1) {
|
|
10072
|
+
throw memgError(
|
|
10073
|
+
"ambiguous_ref",
|
|
10074
|
+
`"${opId}" matches ${matches.length} operations`,
|
|
10075
|
+
`Pass the whole id \u2014 undo deletes a note, so this is not a guess worth making.
|
|
8172
10076
|
${matches.slice(0, 8).map((id) => ` ${id}`).join("\n")}${matches.length > 8 ? "\n \u2026" : ""}`
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
throw memgError(
|
|
8189
|
-
"nothing_to_undo",
|
|
8190
|
-
"that operation is itself an undo",
|
|
8191
|
-
"Undoing an undo is a fresh change \u2014 make it with `revise` or `remember`."
|
|
8192
|
-
);
|
|
8193
|
-
}
|
|
8194
|
-
if (ops.some((o) => o.verb === "undo" && o.supersedes === target.op_id)) {
|
|
8195
|
-
throw memgError(
|
|
8196
|
-
"nothing_to_undo",
|
|
8197
|
-
`${target.op_id} has already been undone`,
|
|
8198
|
-
"Run `memgineering log` to see the current state of this brain."
|
|
8199
|
-
);
|
|
8200
|
-
}
|
|
8201
|
-
const record = await withWriteLock(brain.root, async () => {
|
|
8202
|
-
const absolute = join19(brain.root, target.target);
|
|
8203
|
-
const current = await readFile17(absolute, "utf8").catch(() => null);
|
|
8204
|
-
const actualHash = current === null ? null : hashContent(current);
|
|
8205
|
-
if (actualHash !== target.hash_after) {
|
|
10077
|
+
);
|
|
10078
|
+
}
|
|
10079
|
+
const target = opId ? ops.find((o) => o.op_id === matches[0]) : lastReversible(ops);
|
|
10080
|
+
if (!target) {
|
|
10081
|
+
throw opId ? memgError(
|
|
10082
|
+
"nothing_to_undo",
|
|
10083
|
+
`no operation here matches: ${opId}`,
|
|
10084
|
+
"An id is 26 characters; a prefix must be at least 5 and must be unique. `memgineering log` shows what this brain recorded."
|
|
10085
|
+
) : memgError(
|
|
10086
|
+
"nothing_to_undo",
|
|
10087
|
+
"nothing to undo in this brain",
|
|
10088
|
+
"Only changes made through memgineering are recorded. Edits made in your editor are yours to revert."
|
|
10089
|
+
);
|
|
10090
|
+
}
|
|
10091
|
+
if (target.verb === "undo") {
|
|
8206
10092
|
throw memgError(
|
|
8207
|
-
"
|
|
8208
|
-
|
|
8209
|
-
|
|
10093
|
+
"nothing_to_undo",
|
|
10094
|
+
"that operation is itself an undo",
|
|
10095
|
+
"Undoing an undo is a fresh change \u2014 make it with `revise` or `remember`."
|
|
8210
10096
|
);
|
|
8211
10097
|
}
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
10098
|
+
if (ops.some((o) => o.verb === "undo" && o.supersedes === target.op_id)) {
|
|
10099
|
+
throw memgError(
|
|
10100
|
+
"nothing_to_undo",
|
|
10101
|
+
`${target.op_id} has already been undone`,
|
|
10102
|
+
"Run `memgineering log` to see the current state of this brain."
|
|
10103
|
+
);
|
|
10104
|
+
}
|
|
10105
|
+
const record = await withWriteLock(brain.root, async () => {
|
|
10106
|
+
const absolute = join22(brain.root, target.target);
|
|
10107
|
+
const current = await readFile20(absolute, "utf8").catch(() => null);
|
|
10108
|
+
const actualHash = current === null ? null : hashContent(current);
|
|
10109
|
+
if (actualHash !== target.hash_after) {
|
|
10110
|
+
throw memgError(
|
|
10111
|
+
"undo_conflict",
|
|
10112
|
+
`${target.target} has changed since that operation`,
|
|
10113
|
+
await conflictHint(brain.root, target)
|
|
10114
|
+
);
|
|
10115
|
+
}
|
|
10116
|
+
const reverted = await reverse(brain, target, current);
|
|
10117
|
+
return appendOp(brain.root, {
|
|
10118
|
+
verb: "undo",
|
|
10119
|
+
target: target.target,
|
|
10120
|
+
memoryId: target.memory_id,
|
|
10121
|
+
hashBefore: actualHash,
|
|
10122
|
+
hashAfter: reverted === null ? null : hashContent(reverted),
|
|
10123
|
+
rationale,
|
|
10124
|
+
supersedes: target.op_id
|
|
10125
|
+
});
|
|
8221
10126
|
});
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
printHuman(`Undone: ${target.verb} on \`${target.target}\`
|
|
10127
|
+
printDual({
|
|
10128
|
+
json: {
|
|
10129
|
+
undone: target.op_id,
|
|
10130
|
+
verb: target.verb,
|
|
10131
|
+
target: target.target,
|
|
10132
|
+
undone_ts: target.ts,
|
|
10133
|
+
undone_reason: target.rationale,
|
|
10134
|
+
op_id: record.op_id
|
|
10135
|
+
},
|
|
10136
|
+
human: () => {
|
|
10137
|
+
printHuman(`Undone: ${target.verb} on \`${target.target}\`
|
|
8234
10138
|
`);
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
10139
|
+
if (opId === void 0) {
|
|
10140
|
+
printHuman(c.gray(`It was recorded ${target.ts}.`));
|
|
10141
|
+
if (target.rationale === null) printHuman(c.gray("No reason was recorded for it."));
|
|
10142
|
+
else printHuman(quoteBlock(target.rationale));
|
|
10143
|
+
const minutes = minutesSince(target.ts);
|
|
10144
|
+
if (minutes !== null && minutes >= STALE_UNDO_MINUTES) {
|
|
10145
|
+
printHuman(
|
|
10146
|
+
c.gray(
|
|
10147
|
+
`
|
|
8244
10148
|
that operation was ${minutes} minutes old \u2014 pass the op id to be sure it was the one you meant.`
|
|
8245
|
-
|
|
8246
|
-
|
|
10149
|
+
)
|
|
10150
|
+
);
|
|
10151
|
+
}
|
|
8247
10152
|
}
|
|
8248
|
-
|
|
8249
|
-
printHuman(c.gray(`
|
|
10153
|
+
printHuman(c.gray(`
|
|
8250
10154
|
Recorded as ${record.op_id} \u2014 the history keeps both.`));
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
10155
|
+
}
|
|
10156
|
+
});
|
|
10157
|
+
}
|
|
10158
|
+
);
|
|
8254
10159
|
}
|
|
8255
10160
|
var STALE_UNDO_MINUTES = 10;
|
|
8256
10161
|
function minutesSince(iso) {
|
|
@@ -8259,7 +10164,7 @@ function minutesSince(iso) {
|
|
|
8259
10164
|
return Math.max(0, Math.floor((Date.now() - at) / 6e4));
|
|
8260
10165
|
}
|
|
8261
10166
|
async function reverse(brain, op, current) {
|
|
8262
|
-
const absolute =
|
|
10167
|
+
const absolute = join22(brain.root, op.target);
|
|
8263
10168
|
if (op.verb === "remember") {
|
|
8264
10169
|
if (current === null) {
|
|
8265
10170
|
throw memgError(
|
|
@@ -8324,7 +10229,7 @@ async function conflictHint(vaultRoot, op) {
|
|
|
8324
10229
|
Read what it says now, then decide what should stay.`;
|
|
8325
10230
|
const snapshot = await readSnapshot(vaultRoot, op.op_id);
|
|
8326
10231
|
return snapshot === null ? base : `${base}
|
|
8327
|
-
The text from before that change is still on this machine: ${
|
|
10232
|
+
The text from before that change is still on this machine: ${join22(snapshotDir(vaultRoot), `${op.op_id}.txt`)}`;
|
|
8328
10233
|
}
|
|
8329
10234
|
|
|
8330
10235
|
// src/commands/use.ts
|
|
@@ -8334,8 +10239,8 @@ init_errors();
|
|
|
8334
10239
|
init_config();
|
|
8335
10240
|
init_vault();
|
|
8336
10241
|
init_ui();
|
|
8337
|
-
import { isAbsolute as
|
|
8338
|
-
import { Command as
|
|
10242
|
+
import { isAbsolute as isAbsolute4, relative as relative3, resolve as resolve10 } from "path";
|
|
10243
|
+
import { Command as Command21 } from "commander";
|
|
8339
10244
|
var VIA_MEANS = {
|
|
8340
10245
|
flag: "because --vault named it",
|
|
8341
10246
|
pointer: `because a \`${BRAND.pointerFileName}\` file here points at it`,
|
|
@@ -8344,11 +10249,11 @@ var VIA_MEANS = {
|
|
|
8344
10249
|
default: "because it is your default brain (`memgineering use --default <path>` changes it)"
|
|
8345
10250
|
};
|
|
8346
10251
|
function useCommand() {
|
|
8347
|
-
return new
|
|
10252
|
+
return new Command21("use").description("bind this folder (and everything under it) to one brain").argument("[brain]", "path to the brain \u2014 omit to show what this folder resolves to").option("--here <dir>", "write the pointer in this directory instead of the cwd").option(
|
|
8348
10253
|
"--default",
|
|
8349
10254
|
"make this the brain used anywhere no pointer applies, instead of binding one folder"
|
|
8350
10255
|
).action(async (brainPath, opts) => {
|
|
8351
|
-
const where = await canonicalize(
|
|
10256
|
+
const where = await canonicalize(resolve10(opts.here ?? process.cwd()));
|
|
8352
10257
|
if (!brainPath) {
|
|
8353
10258
|
const pointer = await readPointer(where);
|
|
8354
10259
|
const resolved = await resolveBrain(void 0, where).catch((err) => err);
|
|
@@ -8385,7 +10290,7 @@ ${hint}`));
|
|
|
8385
10290
|
});
|
|
8386
10291
|
return;
|
|
8387
10292
|
}
|
|
8388
|
-
const root = await canonicalize(
|
|
10293
|
+
const root = await canonicalize(resolve10(brainPath));
|
|
8389
10294
|
const cfg = await loadConfig();
|
|
8390
10295
|
if (!findBrain(cfg, root)) {
|
|
8391
10296
|
throw memgError(
|
|
@@ -8412,16 +10317,16 @@ A pointer to an unlinked folder would fail on every command instead of at this o
|
|
|
8412
10317
|
return;
|
|
8413
10318
|
}
|
|
8414
10319
|
const value = isInside(root, where) ? relative3(where, root) || "." : root;
|
|
8415
|
-
const file =
|
|
10320
|
+
const file = resolve10(where, BRAND.pointerFileName);
|
|
8416
10321
|
await writeThenRename(file, `${value}
|
|
8417
10322
|
`);
|
|
8418
10323
|
printDual({
|
|
8419
|
-
json: { pointer: file, brain: root, relative: !
|
|
10324
|
+
json: { pointer: file, brain: root, relative: !isAbsolute4(value) },
|
|
8420
10325
|
human: () => {
|
|
8421
10326
|
printHuman(`This folder now uses \`${root}\`.
|
|
8422
10327
|
`);
|
|
8423
10328
|
printHuman(c.gray(` ${file}`));
|
|
8424
|
-
if (!
|
|
10329
|
+
if (!isAbsolute4(value)) {
|
|
8425
10330
|
printHuman(
|
|
8426
10331
|
c.gray(
|
|
8427
10332
|
" Written as a relative path \u2014 safe to commit, and it will resolve on a teammate\u2019s machine once they link the same brain."
|
|
@@ -8433,11 +10338,56 @@ A pointer to an unlinked folder would fail on every command instead of at this o
|
|
|
8433
10338
|
});
|
|
8434
10339
|
}
|
|
8435
10340
|
|
|
10341
|
+
// src/commands/whoami.ts
|
|
10342
|
+
init_api_client();
|
|
10343
|
+
init_ui();
|
|
10344
|
+
import { Command as Command22 } from "commander";
|
|
10345
|
+
function whoamiCommand() {
|
|
10346
|
+
return new Command22("whoami").description("show which account this machine is signed in as").action(async () => {
|
|
10347
|
+
const base = apiUrl();
|
|
10348
|
+
const credentials = await requireCredentials(base);
|
|
10349
|
+
const res = await apiRequest({
|
|
10350
|
+
method: "GET",
|
|
10351
|
+
path: "/v1/me",
|
|
10352
|
+
token: credentials.token,
|
|
10353
|
+
baseUrl: base
|
|
10354
|
+
});
|
|
10355
|
+
const account = res.body?.account ?? {};
|
|
10356
|
+
const live = res.body?.tokens?.live ?? null;
|
|
10357
|
+
printDual({
|
|
10358
|
+
json: {
|
|
10359
|
+
account: {
|
|
10360
|
+
id: account.id ?? credentials.account_id,
|
|
10361
|
+
provider: account.provider ?? null,
|
|
10362
|
+
display_name: account.display_name ?? null,
|
|
10363
|
+
email: account.email ?? null,
|
|
10364
|
+
created_at: account.created_at ?? null
|
|
10365
|
+
},
|
|
10366
|
+
tokens: { live },
|
|
10367
|
+
api_url: base,
|
|
10368
|
+
signed_in_at: credentials.signed_in_at
|
|
10369
|
+
},
|
|
10370
|
+
human: () => {
|
|
10371
|
+
printHuman(c.bold(account.display_name ?? account.id ?? credentials.account_id));
|
|
10372
|
+
if (account.email) printHuman(c.gray(` ${account.email}`));
|
|
10373
|
+
printHuman(c.gray(` on ${base}`));
|
|
10374
|
+
if (live !== null) {
|
|
10375
|
+
printHuman(
|
|
10376
|
+
c.gray(
|
|
10377
|
+
` ${live} ${live === 1 ? "token" : "tokens"} signed in \u2014 \`memgineering logout --all\` ends them all.`
|
|
10378
|
+
)
|
|
10379
|
+
);
|
|
10380
|
+
}
|
|
10381
|
+
}
|
|
10382
|
+
});
|
|
10383
|
+
});
|
|
10384
|
+
}
|
|
10385
|
+
|
|
8436
10386
|
// src/program.ts
|
|
8437
10387
|
init_ui();
|
|
8438
10388
|
init_brand();
|
|
8439
10389
|
function buildProgram() {
|
|
8440
|
-
const program = new
|
|
10390
|
+
const program = new Command23("memgineering").version(VERSION).description("memgineering \u2014 one memory for the AI you connect").option("--json", "emit JSON to stdout instead of markdown", false).hook("preAction", (thisCommand) => {
|
|
8441
10391
|
const opts = thisCommand.optsWithGlobals();
|
|
8442
10392
|
if (opts.json || process.env[BRAND.jsonEnvVar] === "1") setJsonMode(true);
|
|
8443
10393
|
});
|
|
@@ -8449,6 +10399,8 @@ function buildProgram() {
|
|
|
8449
10399
|
program.addCommand(reviseCommand());
|
|
8450
10400
|
program.addCommand(undoCommand());
|
|
8451
10401
|
program.addCommand(resurfaceCommand());
|
|
10402
|
+
program.addCommand(pushCommand());
|
|
10403
|
+
program.addCommand(pullCommand());
|
|
8452
10404
|
program.addCommand(onboardCommand());
|
|
8453
10405
|
program.addCommand(initCommand());
|
|
8454
10406
|
program.addCommand(linkCommand());
|
|
@@ -8462,6 +10414,9 @@ function buildProgram() {
|
|
|
8462
10414
|
program.addCommand(excludeCommand());
|
|
8463
10415
|
program.addCommand(unexcludeCommand());
|
|
8464
10416
|
program.addCommand(setupCommand());
|
|
10417
|
+
program.addCommand(loginCommand());
|
|
10418
|
+
program.addCommand(logoutCommand());
|
|
10419
|
+
program.addCommand(whoamiCommand());
|
|
8465
10420
|
program.addCommand(updateCommand());
|
|
8466
10421
|
return program;
|
|
8467
10422
|
}
|