memgineering 0.3.0 → 0.4.1
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 +99 -0
- package/assets/memgineering-memory/SKILL.md +55 -6
- package/assets/memgineering-setup/SKILL.md +15 -1
- package/dist/index.js +583 -321
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2624,6 +2624,11 @@ var init_atomic = __esm({
|
|
|
2624
2624
|
import { mkdir as mkdir3, readFile as readFile5 } from "fs/promises";
|
|
2625
2625
|
import { join as join5, resolve as resolve2 } from "path";
|
|
2626
2626
|
import { z as z6 } from "zod";
|
|
2627
|
+
function withDefaultBrain(brain) {
|
|
2628
|
+
if (brain.default) return brain;
|
|
2629
|
+
const oldest = [...brain.brains].sort((a, b) => a.linkedAt.localeCompare(b.linkedAt))[0];
|
|
2630
|
+
return oldest ? { ...brain, default: oldest.root } : brain;
|
|
2631
|
+
}
|
|
2627
2632
|
function configPath() {
|
|
2628
2633
|
return join5(brandHome(), "config.json");
|
|
2629
2634
|
}
|
|
@@ -2666,7 +2671,22 @@ var init_config = __esm({
|
|
|
2666
2671
|
});
|
|
2667
2672
|
ConfigSchema = z6.object({
|
|
2668
2673
|
brain: z6.object({
|
|
2669
|
-
brains: z6.array(BrainSchema).default([])
|
|
2674
|
+
brains: z6.array(BrainSchema).default([]),
|
|
2675
|
+
/**
|
|
2676
|
+
* The brain to use when nothing else says which — root path, or absent.
|
|
2677
|
+
*
|
|
2678
|
+
* Exists because linking a second brain used to break the first one
|
|
2679
|
+
* everywhere. `resolveBrain` answered "1 brain linked" with that brain and
|
|
2680
|
+
* "more than 1" with a refusal, so a scratch folder linked for an
|
|
2681
|
+
* afternoon made every command in every directory ambiguous — including
|
|
2682
|
+
* for other tools on the same machine, which is how a Gemini session lost
|
|
2683
|
+
* a preference its user had just asked it to remember.
|
|
2684
|
+
*
|
|
2685
|
+
* Set by the first `link` or `init` and never moved on its own. A default
|
|
2686
|
+
* that followed the newest link would have pointed at the scratch folder,
|
|
2687
|
+
* which is the failure with the sign flipped.
|
|
2688
|
+
*/
|
|
2689
|
+
default: z6.string().min(1).optional()
|
|
2670
2690
|
}).default({ brains: [] }),
|
|
2671
2691
|
system: z6.object({
|
|
2672
2692
|
/** Self-update: `on` by default, decided at setup. */
|
|
@@ -2712,6 +2732,13 @@ async function resolveBrain(explicit, cwd = process.cwd()) {
|
|
|
2712
2732
|
const containing = brains.filter((b) => isInside(cwd, b.root)).sort((a, b) => b.root.length - a.root.length)[0];
|
|
2713
2733
|
if (containing) return { brain: containing, via: "cwd" };
|
|
2714
2734
|
if (brains.length === 1) return { brain: brains[0], via: "only" };
|
|
2735
|
+
const preferred = cfg.brain.default;
|
|
2736
|
+
if (preferred) {
|
|
2737
|
+
const known = brains.find((b) => b.root === preferred);
|
|
2738
|
+
if (known && (await liveBrains([known])).length === 1) {
|
|
2739
|
+
return { brain: known, via: "default" };
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2715
2742
|
const present = await liveBrains(brains);
|
|
2716
2743
|
if (present.length === 1) return { brain: present[0], via: "only" };
|
|
2717
2744
|
if (brains.length === 0) {
|
|
@@ -2726,7 +2753,10 @@ async function resolveBrain(explicit, cwd = process.cwd()) {
|
|
|
2726
2753
|
throw memgError(
|
|
2727
2754
|
"ambiguous_vault",
|
|
2728
2755
|
`${present.length} of ${brains.length} linked brains are reachable, so this is ambiguous`,
|
|
2729
|
-
`
|
|
2756
|
+
`Set the one you actually use, once, and this stops happening everywhere:
|
|
2757
|
+
memgineering use --default ${present[0]?.root ?? "<path>"}
|
|
2758
|
+
|
|
2759
|
+
Or answer just for this run with --vault <path>, or bind this folder with \`memgineering use <path>\`.
|
|
2730
2760
|
${listed}${missing.length > 0 ? `
|
|
2731
2761
|
A folder marked (missing) is not on disk right now. If it is gone for good, \`memgineering unlink ${missing[0]?.root}\` stops it getting in the way.` : ""}`
|
|
2732
2762
|
);
|
|
@@ -2820,10 +2850,205 @@ var init_index_build = __esm({
|
|
|
2820
2850
|
}
|
|
2821
2851
|
});
|
|
2822
2852
|
|
|
2853
|
+
// src/commands/init.ts
|
|
2854
|
+
import { mkdir as mkdir8, readdir as readdir5, writeFile as writeFile6 } from "fs/promises";
|
|
2855
|
+
import { join as join12, resolve as resolve5 } from "path";
|
|
2856
|
+
import { Command as Command3 } from "commander";
|
|
2857
|
+
function initCommand() {
|
|
2858
|
+
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) => {
|
|
2859
|
+
const { root: canonicalRoot2, indexed } = await createBrain(path, opts.name);
|
|
2860
|
+
printDual({
|
|
2861
|
+
json: {
|
|
2862
|
+
brain: canonicalRoot2,
|
|
2863
|
+
folders: FOLDERS,
|
|
2864
|
+
files: Object.keys(files(opts.name)),
|
|
2865
|
+
observations_dir: OBSERVATIONS_DIR,
|
|
2866
|
+
indexed
|
|
2867
|
+
},
|
|
2868
|
+
human: () => {
|
|
2869
|
+
printHuman(`Created a brain at \`${canonicalRoot2}\`.
|
|
2870
|
+
`);
|
|
2871
|
+
printHuman(`- \`01_BASE/\` \u2014 five files your agent reads first`);
|
|
2872
|
+
printHuman(`- \`${OBSERVATIONS_DIR}/\` \u2014 where \`remember\` writes`);
|
|
2873
|
+
printHuman(`- nine more folders, ready when you need them
|
|
2874
|
+
`);
|
|
2875
|
+
printHuman(c.gray("The five base files are empty templates.\n"));
|
|
2876
|
+
printHuman(
|
|
2877
|
+
c.gray(
|
|
2878
|
+
"AGENT: offer to fill them in now, in conversation \u2014 one question at a time,\nstarting with who they are and how they want you to work. Do not make them\ntype into a file. `memgineering-memory` has the questions and how to write\nthe answers back.\n"
|
|
2879
|
+
)
|
|
2880
|
+
);
|
|
2881
|
+
printHuman(`If it does not offer, ask it: **"set up my memory"** / **"\uB0B4 \uC815\uBCF4 \uCC44\uC6CC\uC918"**`);
|
|
2882
|
+
printHuman(c.gray("Or run `memgineering onboard` \u2014 it prints what to ask.\n"));
|
|
2883
|
+
printHuman(c.gray('Then: `memgineering remember "<something worth keeping>"`'));
|
|
2884
|
+
}
|
|
2885
|
+
});
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
async function createBrain(path, name) {
|
|
2889
|
+
const root = resolve5(path);
|
|
2890
|
+
const existing = await readdir5(root).catch(() => null);
|
|
2891
|
+
if (existing && existing.filter((e) => !e.startsWith(".")).length > 0) {
|
|
2892
|
+
throw memgError(
|
|
2893
|
+
"already_initialized",
|
|
2894
|
+
`${root} already has files in it`,
|
|
2895
|
+
`\`init\` is for a fresh folder. To use notes you already have, read them as they are:
|
|
2896
|
+
memgineering link ${path}`
|
|
2897
|
+
);
|
|
2898
|
+
}
|
|
2899
|
+
await mkdir8(root, { recursive: true });
|
|
2900
|
+
for (const dir of FOLDERS) await mkdir8(join12(root, dir), { recursive: true });
|
|
2901
|
+
for (const [file, content] of Object.entries(files(name))) {
|
|
2902
|
+
await writeFile6(join12(root, file), content, "utf8");
|
|
2903
|
+
}
|
|
2904
|
+
const canonicalRoot2 = await canonicalize(root);
|
|
2905
|
+
const adapter = await LocalFileReadAdapter.openVault(canonicalRoot2);
|
|
2906
|
+
const built = await openIndex(adapter, canonicalRoot2);
|
|
2907
|
+
const cfg = await loadConfig();
|
|
2908
|
+
const brains = cfg.brain.brains.filter((b) => b.root !== canonicalRoot2);
|
|
2909
|
+
brains.push({
|
|
2910
|
+
root: canonicalRoot2,
|
|
2911
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2912
|
+
observationsDir: OBSERVATIONS_DIR
|
|
2913
|
+
});
|
|
2914
|
+
await saveConfig({ ...cfg, brain: withDefaultBrain({ ...cfg.brain, brains }) });
|
|
2915
|
+
return { root: canonicalRoot2, indexed: built.entries.length };
|
|
2916
|
+
}
|
|
2917
|
+
function files(name) {
|
|
2918
|
+
const base = (id, title, summary, body) => [
|
|
2919
|
+
"---",
|
|
2920
|
+
"memgineering:",
|
|
2921
|
+
` id: ${id}`,
|
|
2922
|
+
` title: ${title}`,
|
|
2923
|
+
` summary: ${summary}`,
|
|
2924
|
+
" scope: base",
|
|
2925
|
+
"---",
|
|
2926
|
+
"",
|
|
2927
|
+
`# ${title}`,
|
|
2928
|
+
"",
|
|
2929
|
+
body,
|
|
2930
|
+
""
|
|
2931
|
+
].join("\n");
|
|
2932
|
+
return {
|
|
2933
|
+
"00_HUB/HUB.md": [
|
|
2934
|
+
"---",
|
|
2935
|
+
"memgineering:",
|
|
2936
|
+
" id: hub",
|
|
2937
|
+
` title: ${name}`,
|
|
2938
|
+
" summary: Where to look for what, in this brain.",
|
|
2939
|
+
"---",
|
|
2940
|
+
"",
|
|
2941
|
+
`# ${name}`,
|
|
2942
|
+
"",
|
|
2943
|
+
"This file says where things are. It holds no content of its own \u2014",
|
|
2944
|
+
"when it starts explaining something instead of pointing at it, that",
|
|
2945
|
+
"explanation belongs in a note.",
|
|
2946
|
+
"",
|
|
2947
|
+
"## Read first",
|
|
2948
|
+
"",
|
|
2949
|
+
"- `01_BASE/CRITICAL_FACTS.md` \u2014 the handful of things that must never be wrong",
|
|
2950
|
+
"- `01_BASE/BOUNDARIES.md` \u2014 what not to do without asking",
|
|
2951
|
+
"",
|
|
2952
|
+
"## Where things go",
|
|
2953
|
+
"",
|
|
2954
|
+
"| Folder | What lives here |",
|
|
2955
|
+
"| --- | --- |",
|
|
2956
|
+
"| `10_ENTITIES/` | People, projects, systems \u2014 one note each |",
|
|
2957
|
+
"| `20_TIMELINE/` | What happened, when. `observations/` is written by the agent |",
|
|
2958
|
+
"| `30_ACTIONS/` | Open work and decisions waiting on something |",
|
|
2959
|
+
"| `40_KNOWLEDGE/` | Conclusions worth keeping: how things work, why they were decided |",
|
|
2960
|
+
"| `50_SKILLS/` | Procedures \u2014 the steps for doing a thing again |",
|
|
2961
|
+
"| `60_TEMPLATES/` | Shapes for notes you write often |",
|
|
2962
|
+
"| `70_SOURCES/` | Where something came from: links, papers, transcripts |",
|
|
2963
|
+
"| `80_SHAREABLE/` | Notes you would be willing to publish |",
|
|
2964
|
+
"| `90_ARCHIVE/` | Kept, but no longer current |",
|
|
2965
|
+
""
|
|
2966
|
+
].join("\n"),
|
|
2967
|
+
"01_BASE/CRITICAL_FACTS.md": base(
|
|
2968
|
+
"critical-facts",
|
|
2969
|
+
"Critical facts",
|
|
2970
|
+
"The few things that must never be wrong.",
|
|
2971
|
+
[
|
|
2972
|
+
"Keep this short enough to read every session. Anything that can be",
|
|
2973
|
+
"looked up when needed belongs in a note instead.",
|
|
2974
|
+
"",
|
|
2975
|
+
"- "
|
|
2976
|
+
].join("\n")
|
|
2977
|
+
),
|
|
2978
|
+
"01_BASE/USER.md": base(
|
|
2979
|
+
"user",
|
|
2980
|
+
"About me",
|
|
2981
|
+
"Who I am and what context I work in.",
|
|
2982
|
+
[
|
|
2983
|
+
"Background, role, what you are working on, what an agent should assume",
|
|
2984
|
+
"you already know.",
|
|
2985
|
+
"",
|
|
2986
|
+
"- "
|
|
2987
|
+
].join("\n")
|
|
2988
|
+
),
|
|
2989
|
+
"01_BASE/PREFERENCES.md": base(
|
|
2990
|
+
"preferences",
|
|
2991
|
+
"Preferences",
|
|
2992
|
+
"How I like answers, code and decisions handled.",
|
|
2993
|
+
["Style, verbosity, how much to check in, how you like code written.", "", "- "].join("\n")
|
|
2994
|
+
),
|
|
2995
|
+
"01_BASE/BOUNDARIES.md": base(
|
|
2996
|
+
"boundaries",
|
|
2997
|
+
"Boundaries",
|
|
2998
|
+
"What an agent must not do without asking me first.",
|
|
2999
|
+
[
|
|
3000
|
+
"Privacy, spending, anything irreversible, anything that leaves this",
|
|
3001
|
+
"machine. Be specific \u2014 a boundary that has to be interpreted is one",
|
|
3002
|
+
"that will be interpreted generously.",
|
|
3003
|
+
"",
|
|
3004
|
+
"- "
|
|
3005
|
+
].join("\n")
|
|
3006
|
+
),
|
|
3007
|
+
"01_BASE/TOOLING.md": base(
|
|
3008
|
+
"tooling",
|
|
3009
|
+
"Tooling",
|
|
3010
|
+
"The tools, machines and commands I actually use.",
|
|
3011
|
+
[
|
|
3012
|
+
"Editors, languages, deploy commands, where things run. Enough that an",
|
|
3013
|
+
"agent does not have to guess or discover it again each time.",
|
|
3014
|
+
"",
|
|
3015
|
+
"- "
|
|
3016
|
+
].join("\n")
|
|
3017
|
+
)
|
|
3018
|
+
};
|
|
3019
|
+
}
|
|
3020
|
+
var OBSERVATIONS_DIR, FOLDERS;
|
|
3021
|
+
var init_init = __esm({
|
|
3022
|
+
"src/commands/init.ts"() {
|
|
3023
|
+
"use strict";
|
|
3024
|
+
init_adapter();
|
|
3025
|
+
init_errors();
|
|
3026
|
+
init_config();
|
|
3027
|
+
init_index_build();
|
|
3028
|
+
init_vault();
|
|
3029
|
+
init_ui();
|
|
3030
|
+
OBSERVATIONS_DIR = "20_TIMELINE/observations";
|
|
3031
|
+
FOLDERS = [
|
|
3032
|
+
"00_HUB",
|
|
3033
|
+
"01_BASE",
|
|
3034
|
+
"10_ENTITIES",
|
|
3035
|
+
"20_TIMELINE",
|
|
3036
|
+
"20_TIMELINE/observations",
|
|
3037
|
+
"30_ACTIONS",
|
|
3038
|
+
"40_KNOWLEDGE",
|
|
3039
|
+
"50_SKILLS",
|
|
3040
|
+
"60_TEMPLATES",
|
|
3041
|
+
"70_SOURCES",
|
|
3042
|
+
"80_SHAREABLE",
|
|
3043
|
+
"90_ARCHIVE"
|
|
3044
|
+
];
|
|
3045
|
+
}
|
|
3046
|
+
});
|
|
3047
|
+
|
|
2823
3048
|
// src/commands/link.ts
|
|
2824
3049
|
import { readFile as readFile13 } from "fs/promises";
|
|
2825
|
-
import { join as
|
|
2826
|
-
import { Command as
|
|
3050
|
+
import { join as join13 } from "path";
|
|
3051
|
+
import { Command as Command5 } from "commander";
|
|
2827
3052
|
import prompts from "prompts";
|
|
2828
3053
|
function sameBasename(all, entry) {
|
|
2829
3054
|
const base = (entry.split("/").pop() ?? entry).normalize("NFC").toLowerCase();
|
|
@@ -2846,7 +3071,7 @@ function spreadAcrossFolders(rows, max) {
|
|
|
2846
3071
|
return picked;
|
|
2847
3072
|
}
|
|
2848
3073
|
function linkCommand() {
|
|
2849
|
-
return new
|
|
3074
|
+
return new Command5("link").description("read a notes folder as a brain \u2014 audits its rules first").argument("<path>", "path to the notes folder").option("--yes", "skip the confirmation").option("--dry-run", "show exactly what would be stored, then stop").option(
|
|
2850
3075
|
"--observations <dir>",
|
|
2851
3076
|
"where `remember` writes, relative to the folder",
|
|
2852
3077
|
DEFAULT_OBSERVATIONS_DIR
|
|
@@ -3017,12 +3242,12 @@ async function recordLink(adapter, observationsDir) {
|
|
|
3017
3242
|
linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3018
3243
|
observationsDir: observationsDir.replace(/^\/+|\/+$/g, "")
|
|
3019
3244
|
});
|
|
3020
|
-
await saveConfig({ ...cfg, brain: { brains } });
|
|
3245
|
+
await saveConfig({ ...cfg, brain: withDefaultBrain({ ...cfg.brain, brains }) });
|
|
3021
3246
|
return built;
|
|
3022
3247
|
}
|
|
3023
3248
|
async function refuseSilentRules(path, denied, excluded, unsupported) {
|
|
3024
3249
|
const denyEntries = (await LocalFileReadAdapter.readDenyFile(path, ".memgdeny")).entries;
|
|
3025
|
-
const rawDenyLines = await readFile13(
|
|
3250
|
+
const rawDenyLines = await readFile13(join13(path, ".memgdeny"), "utf8").then(
|
|
3026
3251
|
(t) => t.split("\n").map((l) => l.trim()).filter((l) => l !== "" && !l.startsWith("#"))
|
|
3027
3252
|
).catch(() => []);
|
|
3028
3253
|
const asWritten = (folded) => rawDenyLines.find(
|
|
@@ -3176,216 +3401,19 @@ Brain: ${root}`));
|
|
|
3176
3401
|
});
|
|
3177
3402
|
return ok === true;
|
|
3178
3403
|
}
|
|
3179
|
-
var DEFAULT_OBSERVATIONS_DIR;
|
|
3180
|
-
var init_link = __esm({
|
|
3181
|
-
"src/commands/link.ts"() {
|
|
3182
|
-
"use strict";
|
|
3183
|
-
init_adapter();
|
|
3184
|
-
init_rules_file();
|
|
3185
|
-
init_src();
|
|
3186
|
-
init_errors();
|
|
3187
|
-
init_config();
|
|
3188
|
-
init_index_build();
|
|
3189
|
-
init_vault();
|
|
3190
|
-
init_ui();
|
|
3191
|
-
DEFAULT_OBSERVATIONS_DIR = "_observations";
|
|
3192
|
-
}
|
|
3193
|
-
});
|
|
3194
|
-
|
|
3195
|
-
// src/commands/init.ts
|
|
3196
|
-
import { mkdir as mkdir8, readdir as readdir5, writeFile as writeFile6 } from "fs/promises";
|
|
3197
|
-
import { join as join13, resolve as resolve6 } from "path";
|
|
3198
|
-
import { Command as Command7 } from "commander";
|
|
3199
|
-
function initCommand() {
|
|
3200
|
-
return new Command7("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) => {
|
|
3201
|
-
const { root: canonicalRoot2, indexed } = await createBrain(path, opts.name);
|
|
3202
|
-
printDual({
|
|
3203
|
-
json: {
|
|
3204
|
-
brain: canonicalRoot2,
|
|
3205
|
-
folders: FOLDERS,
|
|
3206
|
-
files: Object.keys(files(opts.name)),
|
|
3207
|
-
observations_dir: OBSERVATIONS_DIR,
|
|
3208
|
-
indexed
|
|
3209
|
-
},
|
|
3210
|
-
human: () => {
|
|
3211
|
-
printHuman(`Created a brain at \`${canonicalRoot2}\`.
|
|
3212
|
-
`);
|
|
3213
|
-
printHuman(`- \`01_BASE/\` \u2014 five files your agent reads first`);
|
|
3214
|
-
printHuman(`- \`${OBSERVATIONS_DIR}/\` \u2014 where \`remember\` writes`);
|
|
3215
|
-
printHuman(`- nine more folders, ready when you need them
|
|
3216
|
-
`);
|
|
3217
|
-
printHuman(c.gray("The five base files are empty templates.\n"));
|
|
3218
|
-
printHuman(
|
|
3219
|
-
c.gray(
|
|
3220
|
-
"AGENT: offer to fill them in now, in conversation \u2014 one question at a time,\nstarting with who they are and how they want you to work. Do not make them\ntype into a file. `memgineering-memory` has the questions and how to write\nthe answers back.\n"
|
|
3221
|
-
)
|
|
3222
|
-
);
|
|
3223
|
-
printHuman(
|
|
3224
|
-
`If it does not offer, ask it: **"set up my memory"** / **"\uB0B4 \uC815\uBCF4 \uCC44\uC6CC\uC918"**`
|
|
3225
|
-
);
|
|
3226
|
-
printHuman(c.gray("Or run `memgineering onboard` \u2014 it prints what to ask.\n"));
|
|
3227
|
-
printHuman(c.gray('Then: `memgineering remember "<something worth keeping>"`'));
|
|
3228
|
-
}
|
|
3229
|
-
});
|
|
3230
|
-
});
|
|
3231
|
-
}
|
|
3232
|
-
async function createBrain(path, name) {
|
|
3233
|
-
const root = resolve6(path);
|
|
3234
|
-
const existing = await readdir5(root).catch(() => null);
|
|
3235
|
-
if (existing && existing.filter((e) => !e.startsWith(".")).length > 0) {
|
|
3236
|
-
throw memgError(
|
|
3237
|
-
"already_initialized",
|
|
3238
|
-
`${root} already has files in it`,
|
|
3239
|
-
`\`init\` is for a fresh folder. To use notes you already have, read them as they are:
|
|
3240
|
-
memgineering link ${path}`
|
|
3241
|
-
);
|
|
3242
|
-
}
|
|
3243
|
-
await mkdir8(root, { recursive: true });
|
|
3244
|
-
for (const dir of FOLDERS) await mkdir8(join13(root, dir), { recursive: true });
|
|
3245
|
-
for (const [file, content] of Object.entries(files(name))) {
|
|
3246
|
-
await writeFile6(join13(root, file), content, "utf8");
|
|
3247
|
-
}
|
|
3248
|
-
const canonicalRoot2 = await canonicalize(root);
|
|
3249
|
-
const adapter = await LocalFileReadAdapter.openVault(canonicalRoot2);
|
|
3250
|
-
const built = await openIndex(adapter, canonicalRoot2);
|
|
3251
|
-
const cfg = await loadConfig();
|
|
3252
|
-
const brains = cfg.brain.brains.filter((b) => b.root !== canonicalRoot2);
|
|
3253
|
-
brains.push({
|
|
3254
|
-
root: canonicalRoot2,
|
|
3255
|
-
linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3256
|
-
observationsDir: OBSERVATIONS_DIR
|
|
3257
|
-
});
|
|
3258
|
-
await saveConfig({ ...cfg, brain: { brains } });
|
|
3259
|
-
return { root: canonicalRoot2, indexed: built.entries.length };
|
|
3260
|
-
}
|
|
3261
|
-
function files(name) {
|
|
3262
|
-
const base = (id, title, summary, body) => [
|
|
3263
|
-
"---",
|
|
3264
|
-
"memgineering:",
|
|
3265
|
-
` id: ${id}`,
|
|
3266
|
-
` title: ${title}`,
|
|
3267
|
-
` summary: ${summary}`,
|
|
3268
|
-
" scope: base",
|
|
3269
|
-
"---",
|
|
3270
|
-
"",
|
|
3271
|
-
`# ${title}`,
|
|
3272
|
-
"",
|
|
3273
|
-
body,
|
|
3274
|
-
""
|
|
3275
|
-
].join("\n");
|
|
3276
|
-
return {
|
|
3277
|
-
"00_HUB/HUB.md": [
|
|
3278
|
-
"---",
|
|
3279
|
-
"memgineering:",
|
|
3280
|
-
" id: hub",
|
|
3281
|
-
` title: ${name}`,
|
|
3282
|
-
" summary: Where to look for what, in this brain.",
|
|
3283
|
-
"---",
|
|
3284
|
-
"",
|
|
3285
|
-
`# ${name}`,
|
|
3286
|
-
"",
|
|
3287
|
-
"This file says where things are. It holds no content of its own \u2014",
|
|
3288
|
-
"when it starts explaining something instead of pointing at it, that",
|
|
3289
|
-
"explanation belongs in a note.",
|
|
3290
|
-
"",
|
|
3291
|
-
"## Read first",
|
|
3292
|
-
"",
|
|
3293
|
-
"- `01_BASE/CRITICAL_FACTS.md` \u2014 the handful of things that must never be wrong",
|
|
3294
|
-
"- `01_BASE/BOUNDARIES.md` \u2014 what not to do without asking",
|
|
3295
|
-
"",
|
|
3296
|
-
"## Where things go",
|
|
3297
|
-
"",
|
|
3298
|
-
"| Folder | What lives here |",
|
|
3299
|
-
"| --- | --- |",
|
|
3300
|
-
"| `10_ENTITIES/` | People, projects, systems \u2014 one note each |",
|
|
3301
|
-
"| `20_TIMELINE/` | What happened, when. `observations/` is written by the agent |",
|
|
3302
|
-
"| `30_ACTIONS/` | Open work and decisions waiting on something |",
|
|
3303
|
-
"| `40_KNOWLEDGE/` | Conclusions worth keeping: how things work, why they were decided |",
|
|
3304
|
-
"| `50_SKILLS/` | Procedures \u2014 the steps for doing a thing again |",
|
|
3305
|
-
"| `60_TEMPLATES/` | Shapes for notes you write often |",
|
|
3306
|
-
"| `70_SOURCES/` | Where something came from: links, papers, transcripts |",
|
|
3307
|
-
"| `80_SHAREABLE/` | Notes you would be willing to publish |",
|
|
3308
|
-
"| `90_ARCHIVE/` | Kept, but no longer current |",
|
|
3309
|
-
""
|
|
3310
|
-
].join("\n"),
|
|
3311
|
-
"01_BASE/CRITICAL_FACTS.md": base(
|
|
3312
|
-
"critical-facts",
|
|
3313
|
-
"Critical facts",
|
|
3314
|
-
"The few things that must never be wrong.",
|
|
3315
|
-
[
|
|
3316
|
-
"Keep this short enough to read every session. Anything that can be",
|
|
3317
|
-
"looked up when needed belongs in a note instead.",
|
|
3318
|
-
"",
|
|
3319
|
-
"- "
|
|
3320
|
-
].join("\n")
|
|
3321
|
-
),
|
|
3322
|
-
"01_BASE/USER.md": base(
|
|
3323
|
-
"user",
|
|
3324
|
-
"About me",
|
|
3325
|
-
"Who I am and what context I work in.",
|
|
3326
|
-
[
|
|
3327
|
-
"Background, role, what you are working on, what an agent should assume",
|
|
3328
|
-
"you already know.",
|
|
3329
|
-
"",
|
|
3330
|
-
"- "
|
|
3331
|
-
].join("\n")
|
|
3332
|
-
),
|
|
3333
|
-
"01_BASE/PREFERENCES.md": base(
|
|
3334
|
-
"preferences",
|
|
3335
|
-
"Preferences",
|
|
3336
|
-
"How I like answers, code and decisions handled.",
|
|
3337
|
-
["Style, verbosity, how much to check in, how you like code written.", "", "- "].join("\n")
|
|
3338
|
-
),
|
|
3339
|
-
"01_BASE/BOUNDARIES.md": base(
|
|
3340
|
-
"boundaries",
|
|
3341
|
-
"Boundaries",
|
|
3342
|
-
"What an agent must not do without asking me first.",
|
|
3343
|
-
[
|
|
3344
|
-
"Privacy, spending, anything irreversible, anything that leaves this",
|
|
3345
|
-
"machine. Be specific \u2014 a boundary that has to be interpreted is one",
|
|
3346
|
-
"that will be interpreted generously.",
|
|
3347
|
-
"",
|
|
3348
|
-
"- "
|
|
3349
|
-
].join("\n")
|
|
3350
|
-
),
|
|
3351
|
-
"01_BASE/TOOLING.md": base(
|
|
3352
|
-
"tooling",
|
|
3353
|
-
"Tooling",
|
|
3354
|
-
"The tools, machines and commands I actually use.",
|
|
3355
|
-
[
|
|
3356
|
-
"Editors, languages, deploy commands, where things run. Enough that an",
|
|
3357
|
-
"agent does not have to guess or discover it again each time.",
|
|
3358
|
-
"",
|
|
3359
|
-
"- "
|
|
3360
|
-
].join("\n")
|
|
3361
|
-
)
|
|
3362
|
-
};
|
|
3363
|
-
}
|
|
3364
|
-
var OBSERVATIONS_DIR, FOLDERS;
|
|
3365
|
-
var init_init = __esm({
|
|
3366
|
-
"src/commands/init.ts"() {
|
|
3404
|
+
var DEFAULT_OBSERVATIONS_DIR;
|
|
3405
|
+
var init_link = __esm({
|
|
3406
|
+
"src/commands/link.ts"() {
|
|
3367
3407
|
"use strict";
|
|
3368
3408
|
init_adapter();
|
|
3409
|
+
init_rules_file();
|
|
3410
|
+
init_src();
|
|
3369
3411
|
init_errors();
|
|
3370
3412
|
init_config();
|
|
3371
3413
|
init_index_build();
|
|
3372
3414
|
init_vault();
|
|
3373
3415
|
init_ui();
|
|
3374
|
-
|
|
3375
|
-
FOLDERS = [
|
|
3376
|
-
"00_HUB",
|
|
3377
|
-
"01_BASE",
|
|
3378
|
-
"10_ENTITIES",
|
|
3379
|
-
"20_TIMELINE",
|
|
3380
|
-
"20_TIMELINE/observations",
|
|
3381
|
-
"30_ACTIONS",
|
|
3382
|
-
"40_KNOWLEDGE",
|
|
3383
|
-
"50_SKILLS",
|
|
3384
|
-
"60_TEMPLATES",
|
|
3385
|
-
"70_SOURCES",
|
|
3386
|
-
"80_SHAREABLE",
|
|
3387
|
-
"90_ARCHIVE"
|
|
3388
|
-
];
|
|
3416
|
+
DEFAULT_OBSERVATIONS_DIR = "_observations";
|
|
3389
3417
|
}
|
|
3390
3418
|
});
|
|
3391
3419
|
|
|
@@ -4102,10 +4130,11 @@ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
|
4102
4130
|
import { readdir as readdir6, stat as stat3 } from "fs/promises";
|
|
4103
4131
|
import { createServer } from "http";
|
|
4104
4132
|
import { homedir as homedir3 } from "os";
|
|
4105
|
-
import { basename, isAbsolute as isAbsolute2, join as join14, resolve as resolve7 } from "path";
|
|
4133
|
+
import { basename, isAbsolute as isAbsolute2, join as join14, relative as relative2, resolve as resolve7 } from "path";
|
|
4106
4134
|
async function startSetupWebServer(opts) {
|
|
4107
4135
|
const token = randomBytes2(32).toString("hex");
|
|
4108
4136
|
const idleMs = opts.idleMs ?? IDLE_TIMEOUT_MS;
|
|
4137
|
+
const maxMs = opts.maxMs ?? MAX_LIFETIME_MS;
|
|
4109
4138
|
const lingerMs = opts.lingerMs ?? APPLY_LINGER_MS;
|
|
4110
4139
|
const sockets = /* @__PURE__ */ new Set();
|
|
4111
4140
|
const server = createServer();
|
|
@@ -4134,6 +4163,7 @@ async function startSetupWebServer(opts) {
|
|
|
4134
4163
|
if (stopped) return;
|
|
4135
4164
|
stopped = true;
|
|
4136
4165
|
if (idleTimer) clearTimeout(idleTimer);
|
|
4166
|
+
clearTimeout(maxTimer);
|
|
4137
4167
|
process.off("SIGINT", onInterrupt);
|
|
4138
4168
|
await new Promise((resolve10) => {
|
|
4139
4169
|
server.close(() => resolve10());
|
|
@@ -4147,14 +4177,15 @@ async function startSetupWebServer(opts) {
|
|
|
4147
4177
|
idleTimer = setTimeout(() => void stop("idle"), idleMs);
|
|
4148
4178
|
idleTimer.unref();
|
|
4149
4179
|
};
|
|
4180
|
+
const maxTimer = setTimeout(() => void stop("idle"), maxMs);
|
|
4181
|
+
maxTimer.unref();
|
|
4150
4182
|
function onInterrupt() {
|
|
4151
4183
|
void stop("interrupted");
|
|
4152
4184
|
}
|
|
4153
4185
|
process.once("SIGINT", onInterrupt);
|
|
4154
4186
|
const state = { detected: opts.detected, token, ownOrigins, applied: false };
|
|
4155
4187
|
server.on("request", (req, res) => {
|
|
4156
|
-
touch()
|
|
4157
|
-
void handle(req, res, state, origin).then((outcome) => {
|
|
4188
|
+
void handle(req, res, state, origin, touch).then((outcome) => {
|
|
4158
4189
|
if (outcome === "applied") setTimeout(() => void stop("applied"), lingerMs).unref();
|
|
4159
4190
|
if (outcome === "cancelled") setTimeout(() => void stop("cancelled"), 50).unref();
|
|
4160
4191
|
}).catch((err) => {
|
|
@@ -4209,9 +4240,20 @@ async function runSetupWeb(opts) {
|
|
|
4209
4240
|
else printHuman(`
|
|
4210
4241
|
${reason === "applied" ? c.green(ending[reason]) : c.gray(ending[reason])}`);
|
|
4211
4242
|
}
|
|
4212
|
-
async function handle(req, res, state, origin) {
|
|
4243
|
+
async function handle(req, res, state, origin, touch = () => {
|
|
4244
|
+
}) {
|
|
4213
4245
|
const url = new URL(req.url ?? "/", origin);
|
|
4214
4246
|
const sender = req.headers.origin;
|
|
4247
|
+
if (sender === void 0 && req.method !== "GET" && req.method !== "HEAD") {
|
|
4248
|
+
sendJson(res, 403, {
|
|
4249
|
+
error: {
|
|
4250
|
+
code: "forbidden_origin",
|
|
4251
|
+
message: "a request that changes something must say where it came from",
|
|
4252
|
+
hint: "This screen answers the page it opened on this machine. If you are driving it directly, send `Origin` matching the URL `memgineering setup --web` printed."
|
|
4253
|
+
}
|
|
4254
|
+
});
|
|
4255
|
+
return "served";
|
|
4256
|
+
}
|
|
4215
4257
|
if (sender !== void 0 && !state.ownOrigins.has(sender)) {
|
|
4216
4258
|
sendJson(res, 403, {
|
|
4217
4259
|
error: {
|
|
@@ -4232,6 +4274,7 @@ async function handle(req, res, state, origin) {
|
|
|
4232
4274
|
});
|
|
4233
4275
|
return "served";
|
|
4234
4276
|
}
|
|
4277
|
+
touch();
|
|
4235
4278
|
if (req.method === "GET" && url.pathname === "/") {
|
|
4236
4279
|
sendHtml(res, renderSetupPage());
|
|
4237
4280
|
return "served";
|
|
@@ -4249,7 +4292,7 @@ async function handle(req, res, state, origin) {
|
|
|
4249
4292
|
return "served";
|
|
4250
4293
|
}
|
|
4251
4294
|
if (req.method === "POST" && url.pathname === "/api/inspect") {
|
|
4252
|
-
return inspectFromScreen(req, res);
|
|
4295
|
+
return inspectFromScreen(req, res, state);
|
|
4253
4296
|
}
|
|
4254
4297
|
if (req.method === "POST" && url.pathname === "/api/apply") {
|
|
4255
4298
|
return applyFromScreen(req, res, state);
|
|
@@ -4326,7 +4369,24 @@ async function holdsNotes(dir) {
|
|
|
4326
4369
|
}
|
|
4327
4370
|
return false;
|
|
4328
4371
|
}
|
|
4329
|
-
|
|
4372
|
+
function isSystemPath(path) {
|
|
4373
|
+
return SYSTEM_ROOTS.some((root) => {
|
|
4374
|
+
if (root === "/") return path === "/";
|
|
4375
|
+
const rel = relative2(root, path);
|
|
4376
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
4377
|
+
});
|
|
4378
|
+
}
|
|
4379
|
+
async function inspectFromScreen(req, res, state) {
|
|
4380
|
+
if (state.applied) {
|
|
4381
|
+
sendJson(res, 409, {
|
|
4382
|
+
error: {
|
|
4383
|
+
code: "already_applied",
|
|
4384
|
+
message: "setup is finished \u2014 this screen no longer reads folders",
|
|
4385
|
+
hint: "Run `memgineering link <folder>` to connect another set of notes, or `memgineering setup --web` again."
|
|
4386
|
+
}
|
|
4387
|
+
});
|
|
4388
|
+
return "served";
|
|
4389
|
+
}
|
|
4330
4390
|
const parsed = await readJsonBody(req, res);
|
|
4331
4391
|
if (parsed === void 0) return "served";
|
|
4332
4392
|
const asked = parsed.path;
|
|
@@ -4340,7 +4400,17 @@ async function inspectFromScreen(req, res) {
|
|
|
4340
4400
|
});
|
|
4341
4401
|
return "served";
|
|
4342
4402
|
}
|
|
4343
|
-
const path = expandHome(asked.trim());
|
|
4403
|
+
const path = resolve7(expandHome(asked.trim()));
|
|
4404
|
+
if (isSystemPath(path)) {
|
|
4405
|
+
sendJson(res, 400, {
|
|
4406
|
+
error: {
|
|
4407
|
+
code: "invalid_input",
|
|
4408
|
+
message: `that is a system folder, not a notes folder: ${path}`,
|
|
4409
|
+
hint: "Point this at the FOLDER holding your notes \u2014 somewhere you write, like `~/Documents/Notes`."
|
|
4410
|
+
}
|
|
4411
|
+
});
|
|
4412
|
+
return "served";
|
|
4413
|
+
}
|
|
4344
4414
|
const info = await stat3(path).catch(() => null);
|
|
4345
4415
|
if (info === null) {
|
|
4346
4416
|
sendJson(res, 200, {
|
|
@@ -4362,7 +4432,8 @@ async function inspectFromScreen(req, res) {
|
|
|
4362
4432
|
}
|
|
4363
4433
|
try {
|
|
4364
4434
|
const target = await openLinkTarget(path, { quiet: true });
|
|
4365
|
-
const rows = await target.preview(
|
|
4435
|
+
const rows = await target.preview(PREVIEW_CAP);
|
|
4436
|
+
const capped = target.notes.length > PREVIEW_CAP;
|
|
4366
4437
|
const cfg = await loadConfig();
|
|
4367
4438
|
sendJson(res, 200, {
|
|
4368
4439
|
path,
|
|
@@ -4373,6 +4444,10 @@ async function inspectFromScreen(req, res) {
|
|
|
4373
4444
|
// number is the one a person is agreeing to; the first is why it moved.
|
|
4374
4445
|
scanned: target.notes.length,
|
|
4375
4446
|
would_index: rows.length,
|
|
4447
|
+
// Named, so a number that stopped early is never read as a total. The
|
|
4448
|
+
// screen shows what a person is agreeing to, and "5,000" presented as the
|
|
4449
|
+
// whole of a 24,000-note folder is a worse answer than a slow one.
|
|
4450
|
+
...capped ? { would_index_capped: PREVIEW_CAP } : {},
|
|
4376
4451
|
excluded: target.scan.excluded.length,
|
|
4377
4452
|
skipped_symlinks: target.scan.skippedSymlinks.length,
|
|
4378
4453
|
refused: [
|
|
@@ -4402,14 +4477,18 @@ async function applyFromScreen(req, res, state) {
|
|
|
4402
4477
|
});
|
|
4403
4478
|
return "served";
|
|
4404
4479
|
}
|
|
4480
|
+
state.applied = true;
|
|
4405
4481
|
const parsed = await readJsonBody(req, res);
|
|
4406
|
-
if (parsed === void 0)
|
|
4482
|
+
if (parsed === void 0) {
|
|
4483
|
+
state.applied = false;
|
|
4484
|
+
return "served";
|
|
4485
|
+
}
|
|
4407
4486
|
const choice = readChoice(parsed, state.detected);
|
|
4408
4487
|
if ("error" in choice) {
|
|
4488
|
+
state.applied = false;
|
|
4409
4489
|
sendJson(res, 400, { error: choice.error });
|
|
4410
4490
|
return "served";
|
|
4411
4491
|
}
|
|
4412
|
-
state.applied = true;
|
|
4413
4492
|
const changes = await applySetup(choice.value, false);
|
|
4414
4493
|
const brain = await connectBrain(choice.brain);
|
|
4415
4494
|
const cfg = await loadConfig();
|
|
@@ -4628,17 +4707,34 @@ function sendHtml(res, html) {
|
|
|
4628
4707
|
// The page is one self-contained file by design; say so, so that a future
|
|
4629
4708
|
// edit that reaches for a CDN font fails visibly here instead of quietly
|
|
4630
4709
|
// making a local setup screen talk to the internet.
|
|
4631
|
-
|
|
4710
|
+
// `frame-ancestors 'none'` because a hostile page could and did load this
|
|
4711
|
+
// screen in an iframe. Nothing is exploitable through it while the token
|
|
4712
|
+
// stands between an attacker and the API — but the consent screen for
|
|
4713
|
+
// installing into somebody's agent should not be embeddable, and the day
|
|
4714
|
+
// the token moves out of the URL into a cookie it would become the whole
|
|
4715
|
+
// attack rather than a curiosity.
|
|
4716
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; form-action 'none'; base-uri 'none'; frame-ancestors 'none'",
|
|
4717
|
+
// The header the older browsers read. Same statement, twice.
|
|
4718
|
+
"x-frame-options": "DENY"
|
|
4632
4719
|
});
|
|
4633
4720
|
res.end(html);
|
|
4634
4721
|
}
|
|
4635
4722
|
async function openBrowser(url) {
|
|
4636
4723
|
const { file, args } = browserOpener(url);
|
|
4637
4724
|
return new Promise((resolve10) => {
|
|
4725
|
+
let settled = false;
|
|
4726
|
+
const finish = (opened) => {
|
|
4727
|
+
if (settled) return;
|
|
4728
|
+
settled = true;
|
|
4729
|
+
resolve10(opened);
|
|
4730
|
+
};
|
|
4638
4731
|
const child = spawn(file, args, { detached: true, stdio: "ignore" });
|
|
4639
|
-
child.on("error", () =>
|
|
4732
|
+
child.on("error", () => finish(false));
|
|
4733
|
+
child.on("exit", (code) => {
|
|
4734
|
+
if (code !== null && code !== 0) finish(false);
|
|
4735
|
+
});
|
|
4640
4736
|
child.unref();
|
|
4641
|
-
setTimeout(() =>
|
|
4737
|
+
setTimeout(() => finish(true), 300).unref();
|
|
4642
4738
|
});
|
|
4643
4739
|
}
|
|
4644
4740
|
function browserOpener(url) {
|
|
@@ -4646,7 +4742,7 @@ function browserOpener(url) {
|
|
|
4646
4742
|
if (process.platform === "win32") return { file: "cmd", args: ["/c", "start", "", url] };
|
|
4647
4743
|
return { file: "xdg-open", args: [url] };
|
|
4648
4744
|
}
|
|
4649
|
-
var IDLE_TIMEOUT_MS, APPLY_LINGER_MS, MAX_BODY_BYTES, MAX_CANDIDATES;
|
|
4745
|
+
var IDLE_TIMEOUT_MS, MAX_LIFETIME_MS, APPLY_LINGER_MS, MAX_BODY_BYTES, MAX_CANDIDATES, PREVIEW_CAP, SYSTEM_ROOTS;
|
|
4650
4746
|
var init_setup_web = __esm({
|
|
4651
4747
|
"src/commands/setup-web.ts"() {
|
|
4652
4748
|
"use strict";
|
|
@@ -4658,9 +4754,24 @@ var init_setup_web = __esm({
|
|
|
4658
4754
|
init_setup();
|
|
4659
4755
|
init_setup_web_page();
|
|
4660
4756
|
IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
4757
|
+
MAX_LIFETIME_MS = 60 * 60 * 1e3;
|
|
4661
4758
|
APPLY_LINGER_MS = 1e3;
|
|
4662
4759
|
MAX_BODY_BYTES = 64 * 1024;
|
|
4663
4760
|
MAX_CANDIDATES = 8;
|
|
4761
|
+
PREVIEW_CAP = 5e3;
|
|
4762
|
+
SYSTEM_ROOTS = [
|
|
4763
|
+
"/",
|
|
4764
|
+
"/usr",
|
|
4765
|
+
"/bin",
|
|
4766
|
+
"/sbin",
|
|
4767
|
+
"/etc",
|
|
4768
|
+
"/dev",
|
|
4769
|
+
"/proc",
|
|
4770
|
+
"/sys",
|
|
4771
|
+
"/System",
|
|
4772
|
+
"/Library",
|
|
4773
|
+
"/opt"
|
|
4774
|
+
];
|
|
4664
4775
|
}
|
|
4665
4776
|
});
|
|
4666
4777
|
|
|
@@ -5424,7 +5535,11 @@ function unlinkCommand() {
|
|
|
5424
5535
|
const { brain } = await resolveBrain(path);
|
|
5425
5536
|
const cfg = await loadConfig();
|
|
5426
5537
|
const remaining = cfg.brain.brains.filter((b) => b.root !== brain.root);
|
|
5427
|
-
|
|
5538
|
+
const nextDefault = cfg.brain.default === brain.root ? remaining[0]?.root : cfg.brain.default;
|
|
5539
|
+
await saveConfig({
|
|
5540
|
+
...cfg,
|
|
5541
|
+
brain: { brains: remaining, ...nextDefault ? { default: nextDefault } : {} }
|
|
5542
|
+
});
|
|
5428
5543
|
if (opts.purge) {
|
|
5429
5544
|
await clearCache(cacheDirFor(brain.root, brandHome()));
|
|
5430
5545
|
await rm5(stateDirFor(brain.root), { recursive: true, force: true });
|
|
@@ -5515,8 +5630,8 @@ async function readLines(root, name) {
|
|
|
5515
5630
|
// src/commands/curation.ts
|
|
5516
5631
|
init_src();
|
|
5517
5632
|
import { stat as stat2 } from "fs/promises";
|
|
5518
|
-
import { resolve as
|
|
5519
|
-
import { Command as
|
|
5633
|
+
import { resolve as resolve6 } from "path";
|
|
5634
|
+
import { Command as Command6 } from "commander";
|
|
5520
5635
|
|
|
5521
5636
|
// ../../packages/storage-adapter/src/local-file/ignore-file.ts
|
|
5522
5637
|
init_atomic();
|
|
@@ -5964,7 +6079,7 @@ init_ui();
|
|
|
5964
6079
|
// src/commands/open.ts
|
|
5965
6080
|
init_src();
|
|
5966
6081
|
init_errors();
|
|
5967
|
-
import { Command as
|
|
6082
|
+
import { Command as Command4 } from "commander";
|
|
5968
6083
|
|
|
5969
6084
|
// src/lib/events.ts
|
|
5970
6085
|
import { appendFile as appendFile2, mkdir as mkdir7, readFile as readFile12, readdir as readdir4 } from "fs/promises";
|
|
@@ -6037,6 +6152,96 @@ async function readEvents(vaultRoot) {
|
|
|
6037
6152
|
// src/commands/open.ts
|
|
6038
6153
|
init_index_build();
|
|
6039
6154
|
|
|
6155
|
+
// src/lib/base-files.ts
|
|
6156
|
+
init_init();
|
|
6157
|
+
var BASE_FILES = [
|
|
6158
|
+
{
|
|
6159
|
+
path: "01_BASE/USER.md",
|
|
6160
|
+
title: "Who they are",
|
|
6161
|
+
asks: "who they are, what they work on, and what they are trying to get done",
|
|
6162
|
+
openers: [
|
|
6163
|
+
"What should I know about you before we work together?",
|
|
6164
|
+
"What are you building, or what do you spend most of your time on?"
|
|
6165
|
+
]
|
|
6166
|
+
},
|
|
6167
|
+
{
|
|
6168
|
+
path: "01_BASE/PREFERENCES.md",
|
|
6169
|
+
title: "How they want you to work",
|
|
6170
|
+
asks: "how they like answers, code and decisions \u2014 length, tone, how much checking in",
|
|
6171
|
+
openers: [
|
|
6172
|
+
"When I answer you, do you want the short version or the reasoning too?",
|
|
6173
|
+
"How much should I check in before doing something, versus just doing it?"
|
|
6174
|
+
]
|
|
6175
|
+
},
|
|
6176
|
+
{
|
|
6177
|
+
path: "01_BASE/BOUNDARIES.md",
|
|
6178
|
+
title: "What to ask before doing",
|
|
6179
|
+
asks: "what you must never do without asking first",
|
|
6180
|
+
openers: [
|
|
6181
|
+
"What should I never do without asking you first?",
|
|
6182
|
+
"Has an assistant ever done something for you that you wish it had asked about?"
|
|
6183
|
+
]
|
|
6184
|
+
},
|
|
6185
|
+
{
|
|
6186
|
+
path: "01_BASE/CRITICAL_FACTS.md",
|
|
6187
|
+
title: "What is expensive to get wrong",
|
|
6188
|
+
asks: "the facts where being wrong costs real money, data, or trust",
|
|
6189
|
+
openers: [
|
|
6190
|
+
"What is the thing I most need to not get wrong \u2014 deploys, money, someone else\u2019s data?",
|
|
6191
|
+
"Is there anything that looks routine but is actually irreversible?"
|
|
6192
|
+
]
|
|
6193
|
+
},
|
|
6194
|
+
{
|
|
6195
|
+
path: "01_BASE/TOOLING.md",
|
|
6196
|
+
title: "What they actually run",
|
|
6197
|
+
asks: "the editors, machines, languages and commands they really use",
|
|
6198
|
+
openers: [
|
|
6199
|
+
"What do you actually run this on \u2014 editor, machine, language, deploy command?",
|
|
6200
|
+
"Anything I would guess wrong about your setup if I did not ask?"
|
|
6201
|
+
]
|
|
6202
|
+
}
|
|
6203
|
+
];
|
|
6204
|
+
var HUB_PATH = "00_HUB/HUB.md";
|
|
6205
|
+
function bodyOf(content) {
|
|
6206
|
+
const stripped = content.replace(/^/, "");
|
|
6207
|
+
const fence = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
|
|
6208
|
+
return (fence ? stripped.slice(fence[0].length) : stripped).trim();
|
|
6209
|
+
}
|
|
6210
|
+
function bodyStillTemplate(path, content) {
|
|
6211
|
+
const template = files("brain")[path];
|
|
6212
|
+
if (template === void 0) return false;
|
|
6213
|
+
const body = bodyOf(content);
|
|
6214
|
+
return body !== "" && body === bodyOf(template);
|
|
6215
|
+
}
|
|
6216
|
+
async function scaffoldingAmong(adapter, notes) {
|
|
6217
|
+
const basePaths = new Set(BASE_FILES.map((f) => f.path));
|
|
6218
|
+
const found = await untouchedBase(
|
|
6219
|
+
adapter,
|
|
6220
|
+
notes.map((n) => n.path).filter((path) => basePaths.has(path))
|
|
6221
|
+
);
|
|
6222
|
+
const hub = notes.find((n) => n.path === HUB_PATH);
|
|
6223
|
+
if (hub && await untouchedHub(adapter, hub.title)) found.add(HUB_PATH);
|
|
6224
|
+
return found;
|
|
6225
|
+
}
|
|
6226
|
+
async function untouchedHub(adapter, title) {
|
|
6227
|
+
const actual = await adapter.read(HUB_PATH).then((r) => r.content).catch(() => null);
|
|
6228
|
+
if (actual === null) return false;
|
|
6229
|
+
return actual.trimEnd() === (files(title)[HUB_PATH] ?? "").trimEnd();
|
|
6230
|
+
}
|
|
6231
|
+
async function untouchedBase(adapter, paths) {
|
|
6232
|
+
const templates = files("brain");
|
|
6233
|
+
const out = /* @__PURE__ */ new Set();
|
|
6234
|
+
await Promise.all(
|
|
6235
|
+
paths.map(async (path) => {
|
|
6236
|
+
const template = templates[path];
|
|
6237
|
+
if (template === void 0) return;
|
|
6238
|
+
const actual = await adapter.read(path).then((r) => r.content).catch(() => null);
|
|
6239
|
+
if (actual !== null && actual.trimEnd() === template.trimEnd()) out.add(path);
|
|
6240
|
+
})
|
|
6241
|
+
);
|
|
6242
|
+
return out;
|
|
6243
|
+
}
|
|
6244
|
+
|
|
6040
6245
|
// src/lib/stages.ts
|
|
6041
6246
|
init_sections();
|
|
6042
6247
|
init_errors();
|
|
@@ -6082,7 +6287,7 @@ async function assembleStage(adapter, entry, detail, sectionName) {
|
|
|
6082
6287
|
init_vault();
|
|
6083
6288
|
init_ui();
|
|
6084
6289
|
function openCommand() {
|
|
6085
|
-
return new
|
|
6290
|
+
return new Command4("open").description("open one memory, as deeply as you ask").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--detail <level>", "card | summary | chunks | full (default: card)").option("--section <name>", "one section (implies --detail chunks)").action(async (ref, opts) => {
|
|
6086
6291
|
const detail = parseDetail(opts.detail, opts.section ? "chunks" : "card");
|
|
6087
6292
|
const { brain } = await resolveBrain(opts.vault);
|
|
6088
6293
|
const adapter = await openBrainOrExplain(brain.root);
|
|
@@ -6101,12 +6306,14 @@ function openCommand() {
|
|
|
6101
6306
|
entry.memory.id,
|
|
6102
6307
|
built.entries.map((e) => e.memory.id)
|
|
6103
6308
|
);
|
|
6309
|
+
const scaffolding = (await scaffoldingAmong(adapter, [{ path: entry.path, title: entry.memory.title }])).has(entry.path);
|
|
6104
6310
|
printDual({
|
|
6105
6311
|
json: {
|
|
6106
6312
|
id: entry.memory.id,
|
|
6107
6313
|
handle: handle2,
|
|
6108
6314
|
path: entry.path,
|
|
6109
6315
|
detail,
|
|
6316
|
+
scaffolding,
|
|
6110
6317
|
memory: entry.memory,
|
|
6111
6318
|
claims,
|
|
6112
6319
|
relations: entry.relations,
|
|
@@ -6115,9 +6322,15 @@ function openCommand() {
|
|
|
6115
6322
|
human: () => {
|
|
6116
6323
|
printHuman(`## ${entry.memory.title}
|
|
6117
6324
|
`);
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6325
|
+
if (scaffolding) {
|
|
6326
|
+
printHuman(
|
|
6327
|
+
c.yellow("not filled in yet") + c.gray(" \u2014 still the template `init` wrote")
|
|
6328
|
+
);
|
|
6329
|
+
} else {
|
|
6330
|
+
printHuman(
|
|
6331
|
+
`${confidenceOf(entry.memory, claims)} \xB7 ${freshnessOf(entry.memory, claims, now)} \xB7 ${entry.memory.provenance.mapping}`
|
|
6332
|
+
);
|
|
6333
|
+
}
|
|
6121
6334
|
printHuman(`
|
|
6122
6335
|
\`id: ${entry.memory.id}\` \xB7 \`${entry.path}\`
|
|
6123
6336
|
`);
|
|
@@ -6126,7 +6339,19 @@ function openCommand() {
|
|
|
6126
6339
|
printHuman(`- ${claim.text}`);
|
|
6127
6340
|
printHuman(c.gray(` ${claim.confidence} \xB7 ${claim.status}`));
|
|
6128
6341
|
}
|
|
6129
|
-
if (claims.length === 0) {
|
|
6342
|
+
if (claims.length === 0 && scaffolding) {
|
|
6343
|
+
printHuman(
|
|
6344
|
+
c.gray(
|
|
6345
|
+
`_Nobody has written this yet. The line above is the prompt \`init\` left, and the
|
|
6346
|
+
body is its instructions \u2014 neither is something the user said, so do not answer
|
|
6347
|
+
from them._
|
|
6348
|
+
|
|
6349
|
+
This one is filled in by asking. \`memgineering onboard\` prints the questions;
|
|
6350
|
+
write the body, then record the conclusion:
|
|
6351
|
+
memgineering revise ${handle2} --claim "<what they told you>" --summary "<one line>"`
|
|
6352
|
+
)
|
|
6353
|
+
);
|
|
6354
|
+
} else if (claims.length === 0) {
|
|
6130
6355
|
printHuman(
|
|
6131
6356
|
c.gray(
|
|
6132
6357
|
entry.memory.provenance.mapping === "explicit" ? "_The title and summary above came from this note, but it declares no `claim` \u2014 so nothing here states a conclusion. That is what `stated` above refers to: where the fields came from, not that a conclusion exists._" : "_No claim declared \u2014 this card was inferred from the note._"
|
|
@@ -6223,7 +6448,7 @@ function fold2(value) {
|
|
|
6223
6448
|
// src/commands/curation.ts
|
|
6224
6449
|
init_link();
|
|
6225
6450
|
function lifecycleCommand(action) {
|
|
6226
|
-
return new
|
|
6451
|
+
return new Command6(action).description(
|
|
6227
6452
|
action === "retire" ? "mark a memory as no longer current \u2014 the note itself is not changed" : "mark a retired memory as current again"
|
|
6228
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) => {
|
|
6229
6454
|
const rationale = sanitizeRationale(opts.reason);
|
|
@@ -6327,7 +6552,7 @@ Undo: \`${undoHint(record.op_id, opts.vault)}\``));
|
|
|
6327
6552
|
});
|
|
6328
6553
|
}
|
|
6329
6554
|
function ignoreCommand(mode) {
|
|
6330
|
-
return new
|
|
6555
|
+
return new Command6(mode).description(
|
|
6331
6556
|
mode === "exclude" ? "stop reading a note entirely \u2014 the note itself is not changed" : "read a note again after excluding it"
|
|
6332
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) => {
|
|
6333
6558
|
const rationale = sanitizeRationale(opts.reason);
|
|
@@ -6385,7 +6610,7 @@ Undo: \`${undoHint(record.op_id, opts.vault)}\``));
|
|
|
6385
6610
|
}
|
|
6386
6611
|
async function mustMatchAFile(root, adapter, path) {
|
|
6387
6612
|
const wanted = path.normalize("NFC").replace(/^\/+/, "");
|
|
6388
|
-
const absolute =
|
|
6613
|
+
const absolute = resolve6(root, wanted);
|
|
6389
6614
|
if (isInside(absolute, root)) {
|
|
6390
6615
|
const found = await stat2(absolute).catch(() => null);
|
|
6391
6616
|
if (found?.isFile()) return wanted;
|
|
@@ -6403,12 +6628,12 @@ var excludeCommand = () => ignoreCommand("exclude");
|
|
|
6403
6628
|
var unexcludeCommand = () => ignoreCommand("unexclude");
|
|
6404
6629
|
|
|
6405
6630
|
// src/commands/evidence.ts
|
|
6406
|
-
import { Command as
|
|
6631
|
+
import { Command as Command7 } from "commander";
|
|
6407
6632
|
init_index_build();
|
|
6408
6633
|
init_vault();
|
|
6409
6634
|
init_ui();
|
|
6410
6635
|
function evidenceCommand() {
|
|
6411
|
-
return new
|
|
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) => {
|
|
6412
6637
|
const { brain } = await resolveBrain(opts.vault);
|
|
6413
6638
|
const adapter = await openBrainOrExplain(brain.root);
|
|
6414
6639
|
const built = await openIndex(adapter, brain.root);
|
|
@@ -6673,71 +6898,6 @@ async function readOrNull(adapter, path) {
|
|
|
6673
6898
|
|
|
6674
6899
|
// src/commands/onboard.ts
|
|
6675
6900
|
import { Command as Command9 } from "commander";
|
|
6676
|
-
|
|
6677
|
-
// src/lib/base-files.ts
|
|
6678
|
-
init_init();
|
|
6679
|
-
var BASE_FILES = [
|
|
6680
|
-
{
|
|
6681
|
-
path: "01_BASE/USER.md",
|
|
6682
|
-
title: "Who they are",
|
|
6683
|
-
asks: "who they are, what they work on, and what they are trying to get done",
|
|
6684
|
-
openers: [
|
|
6685
|
-
"What should I know about you before we work together?",
|
|
6686
|
-
"What are you building, or what do you spend most of your time on?"
|
|
6687
|
-
]
|
|
6688
|
-
},
|
|
6689
|
-
{
|
|
6690
|
-
path: "01_BASE/PREFERENCES.md",
|
|
6691
|
-
title: "How they want you to work",
|
|
6692
|
-
asks: "how they like answers, code and decisions \u2014 length, tone, how much checking in",
|
|
6693
|
-
openers: [
|
|
6694
|
-
"When I answer you, do you want the short version or the reasoning too?",
|
|
6695
|
-
"How much should I check in before doing something, versus just doing it?"
|
|
6696
|
-
]
|
|
6697
|
-
},
|
|
6698
|
-
{
|
|
6699
|
-
path: "01_BASE/BOUNDARIES.md",
|
|
6700
|
-
title: "What to ask before doing",
|
|
6701
|
-
asks: "what you must never do without asking first",
|
|
6702
|
-
openers: [
|
|
6703
|
-
"What should I never do without asking you first?",
|
|
6704
|
-
"Has an assistant ever done something for you that you wish it had asked about?"
|
|
6705
|
-
]
|
|
6706
|
-
},
|
|
6707
|
-
{
|
|
6708
|
-
path: "01_BASE/CRITICAL_FACTS.md",
|
|
6709
|
-
title: "What is expensive to get wrong",
|
|
6710
|
-
asks: "the facts where being wrong costs real money, data, or trust",
|
|
6711
|
-
openers: [
|
|
6712
|
-
"What is the thing I most need to not get wrong \u2014 deploys, money, someone else\u2019s data?",
|
|
6713
|
-
"Is there anything that looks routine but is actually irreversible?"
|
|
6714
|
-
]
|
|
6715
|
-
},
|
|
6716
|
-
{
|
|
6717
|
-
path: "01_BASE/TOOLING.md",
|
|
6718
|
-
title: "What they actually run",
|
|
6719
|
-
asks: "the editors, machines, languages and commands they really use",
|
|
6720
|
-
openers: [
|
|
6721
|
-
"What do you actually run this on \u2014 editor, machine, language, deploy command?",
|
|
6722
|
-
"Anything I would guess wrong about your setup if I did not ask?"
|
|
6723
|
-
]
|
|
6724
|
-
}
|
|
6725
|
-
];
|
|
6726
|
-
async function untouchedBase(adapter, paths) {
|
|
6727
|
-
const templates = files("brain");
|
|
6728
|
-
const out = /* @__PURE__ */ new Set();
|
|
6729
|
-
await Promise.all(
|
|
6730
|
-
paths.map(async (path) => {
|
|
6731
|
-
const template = templates[path];
|
|
6732
|
-
if (template === void 0) return;
|
|
6733
|
-
const actual = await adapter.read(path).then((r) => r.content).catch(() => null);
|
|
6734
|
-
if (actual !== null && actual.trimEnd() === template.trimEnd()) out.add(path);
|
|
6735
|
-
})
|
|
6736
|
-
);
|
|
6737
|
-
return out;
|
|
6738
|
-
}
|
|
6739
|
-
|
|
6740
|
-
// src/commands/onboard.ts
|
|
6741
6901
|
init_vault();
|
|
6742
6902
|
init_ui();
|
|
6743
6903
|
function onboardCommand() {
|
|
@@ -6836,6 +6996,14 @@ function recallCommand() {
|
|
|
6836
6996
|
stage: await stageFor(adapter, byId.get(card.id), detail)
|
|
6837
6997
|
}))
|
|
6838
6998
|
);
|
|
6999
|
+
const scaffolding = await scaffoldingAmong(
|
|
7000
|
+
adapter,
|
|
7001
|
+
rows.map(({ card }) => {
|
|
7002
|
+
const entry = byId.get(card.id);
|
|
7003
|
+
return { path: entry?.path ?? "", title: entry?.memory.title ?? "" };
|
|
7004
|
+
})
|
|
7005
|
+
);
|
|
7006
|
+
const isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
|
|
6839
7007
|
const reasons = result.conflicts.length > 0 ? await conflictReasons(brain.root, result.conflicts, byId) : /* @__PURE__ */ new Map();
|
|
6840
7008
|
printDual({
|
|
6841
7009
|
json: {
|
|
@@ -6845,6 +7013,10 @@ function recallCommand() {
|
|
|
6845
7013
|
...card,
|
|
6846
7014
|
handle: handle2,
|
|
6847
7015
|
path: byId.get(card.id)?.path ?? null,
|
|
7016
|
+
// A fact about the file, not a judgement about the match: this
|
|
7017
|
+
// note is still exactly what `init` wrote, so nobody has answered
|
|
7018
|
+
// it yet.
|
|
7019
|
+
scaffolding: isScaffolding(card),
|
|
6848
7020
|
...stage
|
|
6849
7021
|
})),
|
|
6850
7022
|
related: result.related,
|
|
@@ -6855,9 +7027,7 @@ function recallCommand() {
|
|
|
6855
7027
|
}))
|
|
6856
7028
|
},
|
|
6857
7029
|
human: () => {
|
|
6858
|
-
const reachedSomethingWritten = rows.some(
|
|
6859
|
-
({ card }) => !(byId.get(card.id)?.path ?? "").startsWith("01_BASE/")
|
|
6860
|
-
);
|
|
7030
|
+
const reachedSomethingWritten = rows.some(({ card }) => !isScaffolding(card));
|
|
6861
7031
|
const otherScript = reachedSomethingWritten ? null : unreachableScript(query, built.entries);
|
|
6862
7032
|
const sayWhy = () => {
|
|
6863
7033
|
printHuman(
|
|
@@ -6888,10 +7058,20 @@ function recallCommand() {
|
|
|
6888
7058
|
}
|
|
6889
7059
|
printHuman(`## recall: ${query} (${rows.length} cards)
|
|
6890
7060
|
`);
|
|
7061
|
+
if (!reachedSomethingWritten) {
|
|
7062
|
+
printHuman(
|
|
7063
|
+
c.yellow("\u26A0 Nothing you have written matched this.") + c.gray(
|
|
7064
|
+
"\n Every card below is still the template `init` wrote \u2014 nobody has answered\n these yet, so none of them is evidence of anything.\n `memgineering onboard` prints what to ask to fill them in.\n"
|
|
7065
|
+
)
|
|
7066
|
+
);
|
|
7067
|
+
}
|
|
6891
7068
|
if (otherScript) sayWhy();
|
|
6892
|
-
const glossOn = rows.findIndex(
|
|
7069
|
+
const glossOn = rows.findIndex(
|
|
7070
|
+
({ card }) => card.confidence === "stated" && !isScaffolding(card)
|
|
7071
|
+
);
|
|
6893
7072
|
for (const [i, { card, handle: handle2, stage }] of rows.entries()) {
|
|
6894
|
-
|
|
7073
|
+
const mark = reachedSomethingWritten && isScaffolding(card);
|
|
7074
|
+
printHuman(renderCard(i + 1, card, handle2, detail, stage, i === glossOn, mark));
|
|
6895
7075
|
}
|
|
6896
7076
|
if (result.conflicts.length > 0) {
|
|
6897
7077
|
printHuman(`### \u26A0 contradictions (${result.conflicts.length})
|
|
@@ -6947,13 +7127,19 @@ async function stageFor(adapter, entry, detail) {
|
|
|
6947
7127
|
if (!entry) return {};
|
|
6948
7128
|
return assembleStage(adapter, entry, detail);
|
|
6949
7129
|
}
|
|
6950
|
-
function renderCard(n, card, handle2, detail, stage, glossHere = false) {
|
|
7130
|
+
function renderCard(n, card, handle2, detail, stage, glossHere = false, scaffolding = false) {
|
|
6951
7131
|
const out = [];
|
|
6952
7132
|
const standing = card.freshness === "current" ? "" : ` \xB7 ${card.freshness}`;
|
|
6953
7133
|
const age = card.age_days === null ? "" : ` \xB7 ${ago2(card.age_days)}`;
|
|
6954
7134
|
out.push(`### ${n}. ${card.title}`);
|
|
6955
|
-
|
|
6956
|
-
|
|
7135
|
+
if (scaffolding) {
|
|
7136
|
+
out.push(c.yellow("not filled in yet") + c.gray(" \u2014 still the template `init` wrote"));
|
|
7137
|
+
} else {
|
|
7138
|
+
out.push(`${card.confidence}${standing}${age}`);
|
|
7139
|
+
}
|
|
7140
|
+
if (glossHere && !scaffolding) {
|
|
7141
|
+
out.push(c.gray("stated = these fields came from the note, not from inference"));
|
|
7142
|
+
}
|
|
6957
7143
|
if (detail !== "title") {
|
|
6958
7144
|
if (card.summary) out.push("", quoteBlock(card.summary));
|
|
6959
7145
|
out.push("", `\`open: ${handle2}\``);
|
|
@@ -7280,7 +7466,22 @@ function reviseCommand() {
|
|
|
7280
7466
|
return new Command13("revise").description("change a memory's conclusion, and record how to undo it").argument("<ref>", "handle, id, path, or exact title").option("--claim <text>", "what is now true").option("--summary <text>", "the one line recall will show (defaults to the claim)").option("--title <text>", "the heading recall shows above it (unchanged when omitted)").option(
|
|
7281
7467
|
"--valid-from <iso>",
|
|
7282
7468
|
"when this became true (defaults: supersede = now, otherwise unchanged)"
|
|
7283
|
-
).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").
|
|
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(
|
|
7470
|
+
"after",
|
|
7471
|
+
[
|
|
7472
|
+
"",
|
|
7473
|
+
"Which --action:",
|
|
7474
|
+
" supersede the conclusion CHANGED. Stamps valid_from with now, because a",
|
|
7475
|
+
" replaced conclusion starts now. The default.",
|
|
7476
|
+
" reinforce the SAME conclusion, with new support. Leaves valid_from alone \u2014",
|
|
7477
|
+
" use this when nothing about the fact changed, only your evidence.",
|
|
7478
|
+
" conflict two memories DISAGREE. Records the edge and leaves both standing;",
|
|
7479
|
+
" the claim you pass is deliberately not written.",
|
|
7480
|
+
"",
|
|
7481
|
+
"Filling in a blank 01_BASE file for the first time is `reinforce`: the fact was",
|
|
7482
|
+
"always true, you have only just been told it. `supersede` would date it today."
|
|
7483
|
+
].join("\n")
|
|
7484
|
+
).action(async (ref, opts) => {
|
|
7284
7485
|
const { brain } = await resolveBrain(opts.vault);
|
|
7285
7486
|
await openBrainOrExplain(brain.root);
|
|
7286
7487
|
const adapter = await LocalFileCasAdapter.openVault(brain.root, {
|
|
@@ -7294,9 +7495,23 @@ function reviseCommand() {
|
|
|
7294
7495
|
);
|
|
7295
7496
|
const result = commit(asSingleMatch(entry), input);
|
|
7296
7497
|
if (result.action === "ignore") {
|
|
7297
|
-
throw memgError(
|
|
7498
|
+
throw memgError(
|
|
7499
|
+
"invalid_input",
|
|
7500
|
+
"nothing to revise",
|
|
7501
|
+
"Pass --claim, or --input with one."
|
|
7502
|
+
);
|
|
7298
7503
|
}
|
|
7299
7504
|
const proposal = result.proposal;
|
|
7505
|
+
if (proposal.action !== "conflict" && rationale === null && saysNothingNew(entry, input)) {
|
|
7506
|
+
throw memgError(
|
|
7507
|
+
"invalid_input",
|
|
7508
|
+
`${entry.memory.title} already says this \u2014 nothing was written`,
|
|
7509
|
+
`The claim, summary and title all match what the note already holds.
|
|
7510
|
+
If you meant to record that it still holds, say why:
|
|
7511
|
+
memgineering revise ${ref} --action reinforce --claim "\u2026" --reason "<what confirms it>"
|
|
7512
|
+
To see what is there now: \`memgineering open ` + ref + "`"
|
|
7513
|
+
);
|
|
7514
|
+
}
|
|
7300
7515
|
const preview = await previewProposal(adapter, proposal);
|
|
7301
7516
|
if (preview.refusal) {
|
|
7302
7517
|
throw Object.assign(preview.refusal, {
|
|
@@ -7305,7 +7520,12 @@ function reviseCommand() {
|
|
|
7305
7520
|
}
|
|
7306
7521
|
if (opts.dryRun) {
|
|
7307
7522
|
printDual({
|
|
7308
|
-
json: {
|
|
7523
|
+
json: {
|
|
7524
|
+
dryRun: true,
|
|
7525
|
+
target: entry.path,
|
|
7526
|
+
before: preview.before,
|
|
7527
|
+
after: preview.after
|
|
7528
|
+
},
|
|
7309
7529
|
human: () => {
|
|
7310
7530
|
printHuman(`## revise (dry run): ${entry.memory.title}
|
|
7311
7531
|
`);
|
|
@@ -7353,6 +7573,7 @@ function reviseCommand() {
|
|
|
7353
7573
|
return { record, content: outcome.content };
|
|
7354
7574
|
});
|
|
7355
7575
|
const drift = proposal.action === "supersede" ? bodyStillAsserts(applied.content, previousClaim(entry), proposal.claim?.text ?? null) : null;
|
|
7576
|
+
const unwrittenBody = bodyStillTemplate(entry.path, applied.content);
|
|
7356
7577
|
printDual({
|
|
7357
7578
|
json: {
|
|
7358
7579
|
target: entry.path,
|
|
@@ -7360,7 +7581,8 @@ function reviseCommand() {
|
|
|
7360
7581
|
op_id: applied.record.op_id,
|
|
7361
7582
|
before: preview.before,
|
|
7362
7583
|
after: preview.after,
|
|
7363
|
-
body_drift: drift
|
|
7584
|
+
body_drift: drift,
|
|
7585
|
+
body_unwritten: unwrittenBody
|
|
7364
7586
|
},
|
|
7365
7587
|
human: () => {
|
|
7366
7588
|
if (proposal.action === "conflict") {
|
|
@@ -7388,6 +7610,14 @@ function reviseCommand() {
|
|
|
7388
7610
|
)
|
|
7389
7611
|
);
|
|
7390
7612
|
}
|
|
7613
|
+
if (unwrittenBody) {
|
|
7614
|
+
printHuman(
|
|
7615
|
+
c.yellow(`
|
|
7616
|
+
\u26A0 the body of this note is still the template \`init\` wrote`) + c.gray(
|
|
7617
|
+
"\n \u2014 the conclusion above it is theirs, the prose under it is instructions for\n writing one. Anyone opening the file reads the form, not the answer.\n Replace the body with what they actually said, in their words."
|
|
7618
|
+
)
|
|
7619
|
+
);
|
|
7620
|
+
}
|
|
7391
7621
|
if (isCritical(entry)) {
|
|
7392
7622
|
printHuman(
|
|
7393
7623
|
c.yellow(
|
|
@@ -7497,14 +7727,26 @@ function previousClaim(entry) {
|
|
|
7497
7727
|
const current = entry.claims.find((claim) => claim.id === entry.memory.current_claim) ?? entry.claims[0];
|
|
7498
7728
|
return current?.text ?? null;
|
|
7499
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
|
+
}
|
|
7500
7742
|
function bodyStillAsserts(content, oldClaim, newClaim) {
|
|
7501
7743
|
if (oldClaim === null) return null;
|
|
7502
7744
|
const line = stripControl(oldClaim.split("\n")[0] ?? "").trim();
|
|
7503
7745
|
if (line === "") return null;
|
|
7504
7746
|
if (line === stripControl(newClaim?.split("\n")[0] ?? "").trim()) return null;
|
|
7505
|
-
return
|
|
7747
|
+
return bodyOf2(content).includes(line) ? line : null;
|
|
7506
7748
|
}
|
|
7507
|
-
function
|
|
7749
|
+
function bodyOf2(content) {
|
|
7508
7750
|
const stripped = content.replace(/^/, "");
|
|
7509
7751
|
const fence = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
|
|
7510
7752
|
return fence ? stripped.slice(fence[0].length) : stripped;
|
|
@@ -8092,16 +8334,20 @@ init_errors();
|
|
|
8092
8334
|
init_config();
|
|
8093
8335
|
init_vault();
|
|
8094
8336
|
init_ui();
|
|
8095
|
-
import { isAbsolute as isAbsolute3, relative as
|
|
8337
|
+
import { isAbsolute as isAbsolute3, relative as relative3, resolve as resolve9 } from "path";
|
|
8096
8338
|
import { Command as Command17 } from "commander";
|
|
8097
8339
|
var VIA_MEANS = {
|
|
8098
8340
|
flag: "because --vault named it",
|
|
8099
8341
|
pointer: `because a \`${BRAND.pointerFileName}\` file here points at it`,
|
|
8100
8342
|
cwd: "because this folder is inside it",
|
|
8101
|
-
only: "because it is the only brain linked"
|
|
8343
|
+
only: "because it is the only brain linked",
|
|
8344
|
+
default: "because it is your default brain (`memgineering use --default <path>` changes it)"
|
|
8102
8345
|
};
|
|
8103
8346
|
function useCommand() {
|
|
8104
|
-
return new Command17("use").description("bind this folder (and everything under it) to one brain").argument("[brain]", "path to the brain \u2014 omit to show what this folder resolves to").option("--here <dir>", "write the pointer in this directory instead of the cwd").
|
|
8347
|
+
return new Command17("use").description("bind this folder (and everything under it) to one brain").argument("[brain]", "path to the brain \u2014 omit to show what this folder resolves to").option("--here <dir>", "write the pointer in this directory instead of the cwd").option(
|
|
8348
|
+
"--default",
|
|
8349
|
+
"make this the brain used anywhere no pointer applies, instead of binding one folder"
|
|
8350
|
+
).action(async (brainPath, opts) => {
|
|
8105
8351
|
const where = await canonicalize(resolve9(opts.here ?? process.cwd()));
|
|
8106
8352
|
if (!brainPath) {
|
|
8107
8353
|
const pointer = await readPointer(where);
|
|
@@ -8149,7 +8395,23 @@ ${hint}`));
|
|
|
8149
8395
|
A pointer to an unlinked folder would fail on every command instead of at this one.`
|
|
8150
8396
|
);
|
|
8151
8397
|
}
|
|
8152
|
-
|
|
8398
|
+
if (opts.default) {
|
|
8399
|
+
await saveConfig({ ...cfg, brain: { ...cfg.brain, default: root } });
|
|
8400
|
+
printDual({
|
|
8401
|
+
json: { default: root },
|
|
8402
|
+
human: () => {
|
|
8403
|
+
printHuman(`Default brain is now \`${root}\`.
|
|
8404
|
+
`);
|
|
8405
|
+
printHuman(
|
|
8406
|
+
c.gray(
|
|
8407
|
+
"Used wherever no `.memgineering` pointer applies and you are not inside\nanother brain \u2014 so linking a second brain no longer makes every command\nambiguous. `--vault <path>` still wins for a single run."
|
|
8408
|
+
)
|
|
8409
|
+
);
|
|
8410
|
+
}
|
|
8411
|
+
});
|
|
8412
|
+
return;
|
|
8413
|
+
}
|
|
8414
|
+
const value = isInside(root, where) ? relative3(where, root) || "." : root;
|
|
8153
8415
|
const file = resolve9(where, BRAND.pointerFileName);
|
|
8154
8416
|
await writeThenRename(file, `${value}
|
|
8155
8417
|
`);
|