memgineering 0.25.1 → 0.25.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -1065,22 +1065,6 @@ function mapNoteToMemory(input) {
|
|
|
1065
1065
|
issues
|
|
1066
1066
|
};
|
|
1067
1067
|
}
|
|
1068
|
-
function isUntouchedBase(path, content) {
|
|
1069
|
-
if (!/^01_BASE\//.test(path)) return false;
|
|
1070
|
-
const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/, "");
|
|
1071
|
-
const lines2 = body.split(/\r?\n/).map((l) => l.trimEnd());
|
|
1072
|
-
let seenHeading = false;
|
|
1073
|
-
for (const line of lines2) {
|
|
1074
|
-
if (line.trim() === "") continue;
|
|
1075
|
-
if (!seenHeading && line.startsWith("# ")) {
|
|
1076
|
-
seenHeading = true;
|
|
1077
|
-
continue;
|
|
1078
|
-
}
|
|
1079
|
-
if (/^-\s*$/.test(line)) continue;
|
|
1080
|
-
if (/^[-*+]\s+\S/.test(line) || /^#{1,6}\s/.test(line)) return false;
|
|
1081
|
-
}
|
|
1082
|
-
return seenHeading;
|
|
1083
|
-
}
|
|
1084
1068
|
var NAMESPACE, isRecord, asString, asStringArray, AUTHORABLE;
|
|
1085
1069
|
var init_markdown_mapping = __esm({
|
|
1086
1070
|
"../../packages/memory-engine/src/markdown-mapping.ts"() {
|
|
@@ -2441,11 +2425,18 @@ var init_planner = __esm({
|
|
|
2441
2425
|
}
|
|
2442
2426
|
});
|
|
2443
2427
|
|
|
2444
|
-
// ../../packages/memory-engine/src/
|
|
2445
|
-
|
|
2446
|
-
|
|
2428
|
+
// ../../packages/memory-engine/src/note-body.ts
|
|
2429
|
+
function bodyOf(content) {
|
|
2430
|
+
const stripped = content.startsWith(BOM) ? content.slice(BOM.length) : content;
|
|
2431
|
+
const fence2 = FENCE.exec(stripped);
|
|
2432
|
+
return fence2 ? stripped.slice(fence2[0].length) : stripped;
|
|
2433
|
+
}
|
|
2434
|
+
var BOM, FENCE;
|
|
2435
|
+
var init_note_body = __esm({
|
|
2436
|
+
"../../packages/memory-engine/src/note-body.ts"() {
|
|
2447
2437
|
"use strict";
|
|
2448
|
-
|
|
2438
|
+
BOM = String.fromCharCode(65279);
|
|
2439
|
+
FENCE = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/;
|
|
2449
2440
|
}
|
|
2450
2441
|
});
|
|
2451
2442
|
|
|
@@ -2469,10 +2460,31 @@ function base(id, title, summary, body) {
|
|
|
2469
2460
|
function ownEntry(record, key2) {
|
|
2470
2461
|
return Object.hasOwn(record, key2) ? record[key2] : void 0;
|
|
2471
2462
|
}
|
|
2472
|
-
|
|
2463
|
+
function wordingsFor(path) {
|
|
2464
|
+
const current = ownEntry(BASE_TEMPLATES, path);
|
|
2465
|
+
const retired = ownEntry(RETIRED_BASE_TEMPLATES, path) ?? [];
|
|
2466
|
+
return current === void 0 ? retired : [current, ...retired];
|
|
2467
|
+
}
|
|
2468
|
+
function isExactUntouchedBase(path, content) {
|
|
2469
|
+
const saved = asSaved(content);
|
|
2470
|
+
return wordingsFor(path).some((t) => asSaved(t) === saved);
|
|
2471
|
+
}
|
|
2472
|
+
function isTemplateBody(path, content) {
|
|
2473
|
+
const lines2 = bodyLines(content).join("\n");
|
|
2474
|
+
return wordingsFor(path).some((t) => bodyLines(t).join("\n") === lines2);
|
|
2475
|
+
}
|
|
2476
|
+
function isUnwrittenBody(path, content) {
|
|
2477
|
+
if (isTemplateBody(path, content)) return true;
|
|
2478
|
+
const [first, ...rest] = bodyLines(content);
|
|
2479
|
+
if (first === void 0) return false;
|
|
2480
|
+
const headings = wordingsFor(path).map((t) => bodyLines(t)[0]);
|
|
2481
|
+
return headings.includes(first) && rest.every((line) => /^-\s*$/.test(line));
|
|
2482
|
+
}
|
|
2483
|
+
var BASE_TEMPLATES, RETIRED_BASE_TEMPLATES, BOM2, asSaved, bodyLines;
|
|
2473
2484
|
var init_base_templates = __esm({
|
|
2474
2485
|
"../../packages/memory-engine/src/base-templates.ts"() {
|
|
2475
2486
|
"use strict";
|
|
2487
|
+
init_note_body();
|
|
2476
2488
|
BASE_TEMPLATES = {
|
|
2477
2489
|
"01_BASE/CRITICAL_FACTS.md": base(
|
|
2478
2490
|
"critical-facts",
|
|
@@ -2553,6 +2565,17 @@ var init_base_templates = __esm({
|
|
|
2553
2565
|
].join("\n")
|
|
2554
2566
|
]
|
|
2555
2567
|
};
|
|
2568
|
+
BOM2 = String.fromCharCode(65279);
|
|
2569
|
+
asSaved = (text) => (text.startsWith(BOM2) ? text.slice(BOM2.length) : text).replace(/\r\n/g, "\n").trimEnd();
|
|
2570
|
+
bodyLines = (content) => bodyOf(content).split("\n").map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
|
|
2571
|
+
}
|
|
2572
|
+
});
|
|
2573
|
+
|
|
2574
|
+
// ../../packages/memory-engine/src/recall/semantic.ts
|
|
2575
|
+
var init_semantic = __esm({
|
|
2576
|
+
"../../packages/memory-engine/src/recall/semantic.ts"() {
|
|
2577
|
+
"use strict";
|
|
2578
|
+
init_base_templates();
|
|
2556
2579
|
}
|
|
2557
2580
|
});
|
|
2558
2581
|
|
|
@@ -2577,6 +2600,13 @@ var init_unfilled = __esm({
|
|
|
2577
2600
|
}
|
|
2578
2601
|
});
|
|
2579
2602
|
|
|
2603
|
+
// ../../packages/memory-engine/src/setup-progress.ts
|
|
2604
|
+
var init_setup_progress = __esm({
|
|
2605
|
+
"../../packages/memory-engine/src/setup-progress.ts"() {
|
|
2606
|
+
"use strict";
|
|
2607
|
+
}
|
|
2608
|
+
});
|
|
2609
|
+
|
|
2580
2610
|
// ../../packages/memory-engine/src/recall/query-prefix.ts
|
|
2581
2611
|
var init_query_prefix = __esm({
|
|
2582
2612
|
"../../packages/memory-engine/src/recall/query-prefix.ts"() {
|
|
@@ -4043,11 +4073,6 @@ function bodyStillAsserts(content, oldClaim, newClaim, normalize) {
|
|
|
4043
4073
|
if (line === normalize(newClaim?.split("\n")[0] ?? "").trim()) return null;
|
|
4044
4074
|
return bodyOf(content).includes(line) ? line : null;
|
|
4045
4075
|
}
|
|
4046
|
-
function bodyOf(content) {
|
|
4047
|
-
const stripped = content.replace(/^/, "");
|
|
4048
|
-
const fence2 = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
|
|
4049
|
-
return fence2 ? stripped.slice(fence2[0].length) : stripped;
|
|
4050
|
-
}
|
|
4051
4076
|
function previousClaim(entry) {
|
|
4052
4077
|
const current = entry.claims.find((claim) => claim.id === entry.memory.current_claim) ?? entry.claims[0];
|
|
4053
4078
|
return current?.text ?? null;
|
|
@@ -4065,6 +4090,7 @@ var init_curate = __esm({
|
|
|
4065
4090
|
init_planner();
|
|
4066
4091
|
init_schema();
|
|
4067
4092
|
init_rule_budget();
|
|
4093
|
+
init_note_body();
|
|
4068
4094
|
NO_REASON_GIVEN = "revised directly";
|
|
4069
4095
|
noLifecycleReason = (action) => `${action}d directly`;
|
|
4070
4096
|
}
|
|
@@ -4905,6 +4931,7 @@ var init_src = __esm({
|
|
|
4905
4931
|
init_planner();
|
|
4906
4932
|
init_semantic();
|
|
4907
4933
|
init_unfilled();
|
|
4934
|
+
init_setup_progress();
|
|
4908
4935
|
init_query_prefix();
|
|
4909
4936
|
init_base_templates();
|
|
4910
4937
|
init_scope_spread();
|
|
@@ -5507,6 +5534,13 @@ var init_config = __esm({
|
|
|
5507
5534
|
* answer as "older than this CLI" and wants the same one command.
|
|
5508
5535
|
*/
|
|
5509
5536
|
guidanceVersion: z8.string().optional(),
|
|
5537
|
+
/**
|
|
5538
|
+
* The person's answer to "arm the hooks?", as they gave it — not whether
|
|
5539
|
+
* this machine's tools could take one. A flag-less `setup` reads it back,
|
|
5540
|
+
* so a refresh keeps what they chose instead of setup's default (0.25.2).
|
|
5541
|
+
* Absent on a machine set up before it was recorded.
|
|
5542
|
+
*/
|
|
5543
|
+
hook: z8.enum(["on", "off"]).optional().catch(void 0),
|
|
5510
5544
|
/**
|
|
5511
5545
|
* Notes `setup --web` was asked to upload, not yet sent.
|
|
5512
5546
|
*
|
|
@@ -7864,13 +7898,208 @@ var init_setup_signin = __esm({
|
|
|
7864
7898
|
}
|
|
7865
7899
|
});
|
|
7866
7900
|
|
|
7901
|
+
// src/lib/next-step.ts
|
|
7902
|
+
import { mkdir as mkdir15, readFile as readFile24, writeFile as writeFile13 } from "fs/promises";
|
|
7903
|
+
import { join as join23 } from "path";
|
|
7904
|
+
function isSetUp(cfg) {
|
|
7905
|
+
return Object.keys(cfg.system.installed).length > 0;
|
|
7906
|
+
}
|
|
7907
|
+
function connectedMarker(at) {
|
|
7908
|
+
return join23(brandHome(), `setup-connected-shown-${at.replace(/[^0-9A-Za-z]/g, "")}`);
|
|
7909
|
+
}
|
|
7910
|
+
async function connectedNoticeShown(at) {
|
|
7911
|
+
return readFile24(connectedMarker(at), "utf8").then(
|
|
7912
|
+
() => true,
|
|
7913
|
+
() => false
|
|
7914
|
+
);
|
|
7915
|
+
}
|
|
7916
|
+
async function markConnectedNoticeShown(at) {
|
|
7917
|
+
try {
|
|
7918
|
+
await mkdir15(brandHome(), { recursive: true });
|
|
7919
|
+
await writeFile13(connectedMarker(at), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
7920
|
+
`);
|
|
7921
|
+
} catch {
|
|
7922
|
+
}
|
|
7923
|
+
}
|
|
7924
|
+
async function claimFullSetupNotice() {
|
|
7925
|
+
const marker = join23(brandHome(), "setup-notice-shown");
|
|
7926
|
+
try {
|
|
7927
|
+
await readFile24(marker, "utf8");
|
|
7928
|
+
return false;
|
|
7929
|
+
} catch {
|
|
7930
|
+
}
|
|
7931
|
+
try {
|
|
7932
|
+
await mkdir15(brandHome(), { recursive: true });
|
|
7933
|
+
await writeFile13(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
7934
|
+
`);
|
|
7935
|
+
} catch {
|
|
7936
|
+
}
|
|
7937
|
+
return true;
|
|
7938
|
+
}
|
|
7939
|
+
function shellQuote(value) {
|
|
7940
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7941
|
+
}
|
|
7942
|
+
function confirmUploadNotice(p) {
|
|
7943
|
+
const who = p.account_email ?? "the account `setup --web` connected";
|
|
7944
|
+
const brain = p.brain_id && UUID.test(p.brain_id) ? ` --brain ${p.brain_id}` : "";
|
|
7945
|
+
return `## Notes are waiting to be uploaded \u2014 after a check.
|
|
7946
|
+
|
|
7947
|
+
\`memgineering setup --web\` chose to upload \`${p.path}\` to a brain on **${who}**.
|
|
7948
|
+
This machine has not sent them: an upload decided on the setup screen waits
|
|
7949
|
+
until somebody here has seen which account would receive the notes.
|
|
7950
|
+
|
|
7951
|
+
If that is your user's account, this uploads them:
|
|
7952
|
+
|
|
7953
|
+
\`\`\`bash
|
|
7954
|
+
memgineering push --vault ${shellQuote(p.path)}${brain}
|
|
7955
|
+
\`\`\`
|
|
7956
|
+
|
|
7957
|
+
If it is not, \`memgineering logout\` disconnects this machine from that account
|
|
7958
|
+
and nothing is uploaded. If you are not sure which it is, ask them.`;
|
|
7959
|
+
}
|
|
7960
|
+
function setupConnectedNotice(c2) {
|
|
7961
|
+
const who = c2.email ?? "an account with no email on record";
|
|
7962
|
+
return `## This machine is signed in as **${who}**.
|
|
7963
|
+
|
|
7964
|
+
\`memgineering setup --web\` connected it to that account. If that is not your user's account,
|
|
7965
|
+
\`memgineering logout\` disconnects it, and nothing they write reaches that brain.`;
|
|
7966
|
+
}
|
|
7967
|
+
function nextStepFor(cfg, signedIn = false, seen = {}) {
|
|
7968
|
+
if (cfg.system.pendingUpload) {
|
|
7969
|
+
return { id: "confirm_upload", markdown: confirmUploadNotice(cfg.system.pendingUpload) };
|
|
7970
|
+
}
|
|
7971
|
+
if (cfg.system.setupConnected && !seen.connected) {
|
|
7972
|
+
return { id: "setup_connected", markdown: setupConnectedNotice(cfg.system.setupConnected) };
|
|
7973
|
+
}
|
|
7974
|
+
if (signedIn && cfg.brain.cloud === void 0) {
|
|
7975
|
+
return { id: "point_at_a_brain", markdown: POINT_AT_A_BRAIN };
|
|
7976
|
+
}
|
|
7977
|
+
if (isSetUp(cfg) && cfg.system.guidanceVersion !== VERSION) {
|
|
7978
|
+
return { id: "refresh_guidance", markdown: REFRESH_GUIDANCE };
|
|
7979
|
+
}
|
|
7980
|
+
if (cfg.brain.cloud !== void 0 && cfg.brain.cloud.api_url === apiUrl()) return null;
|
|
7981
|
+
if (!isSetUp(cfg)) return { id: "finish_setup", markdown: FINISH_SETUP };
|
|
7982
|
+
if (cfg.brain.brains.length === 0 && cfg.brain.cloud === void 0) {
|
|
7983
|
+
return { id: "connect_a_brain", markdown: CONNECT_A_BRAIN };
|
|
7984
|
+
}
|
|
7985
|
+
return null;
|
|
7986
|
+
}
|
|
7987
|
+
async function currentNextStep() {
|
|
7988
|
+
try {
|
|
7989
|
+
const stored = await readCredentials().catch(() => null);
|
|
7990
|
+
const cfg = await loadConfig();
|
|
7991
|
+
const connected = cfg.system.setupConnected ? await connectedNoticeShown(cfg.system.setupConnected.at) : false;
|
|
7992
|
+
return nextStepFor(cfg, stored !== null && stored.api_url === apiUrl(), { connected });
|
|
7993
|
+
} catch {
|
|
7994
|
+
return null;
|
|
7995
|
+
}
|
|
7996
|
+
}
|
|
7997
|
+
var FINISH_SETUP, SETUP_NUDGE, REFRESH_GUIDANCE, CONNECT_A_BRAIN, POINT_AT_A_BRAIN, UUID;
|
|
7998
|
+
var init_next_step = __esm({
|
|
7999
|
+
"src/lib/next-step.ts"() {
|
|
8000
|
+
"use strict";
|
|
8001
|
+
init_brand();
|
|
8002
|
+
init_api_client();
|
|
8003
|
+
init_credentials();
|
|
8004
|
+
init_config();
|
|
8005
|
+
init_version();
|
|
8006
|
+
FINISH_SETUP = `## memgineering is installed, but not set up yet.
|
|
8007
|
+
|
|
8008
|
+
Nothing is connected: there is no brain to read, and the agent tools on this
|
|
8009
|
+
machine have no memgineering guidance yet. Setup is one command:
|
|
8010
|
+
|
|
8011
|
+
\`\`\`bash
|
|
8012
|
+
memgineering setup --web
|
|
8013
|
+
\`\`\`
|
|
8014
|
+
|
|
8015
|
+
It does NOT wait for them. It returns in a few seconds \u2014 measured \u2014 and leaves
|
|
8016
|
+
a setup screen open in their browser at memgineering.com, where they choose
|
|
8017
|
+
everything: signing in, where the memory lives, which notes. Nothing is
|
|
8018
|
+
installed until they finish there, and this machine picks it up on its own.
|
|
8019
|
+
|
|
8020
|
+
If they have not already asked you to set it up, ask them first \u2014 it opens a
|
|
8021
|
+
browser window on their computer. Once you run it, tell them in their language
|
|
8022
|
+
that the setup screen is open. It prints a short code first: show it to them,
|
|
8023
|
+
because the screen asks them to compare it.
|
|
8024
|
+
|
|
8025
|
+
No browser on this machine (a server, a container, a sandbox)? Run
|
|
8026
|
+
\`memgineering setup --web --print-url\` and give them the link to open on the
|
|
8027
|
+
computer they are actually sitting at.
|
|
8028
|
+
|
|
8029
|
+
\`setup --agent\` answers every one of those questions on their behalf, and
|
|
8030
|
+
\`--no-login\` answers the biggest one \u2014 no account, so the memory cannot follow
|
|
8031
|
+
them to another machine. Use it only when they have said they would rather you
|
|
8032
|
+
decided, or when this machine cannot reach a browser and \`--print-url\` is no
|
|
8033
|
+
use to them either.
|
|
8034
|
+
|
|
8035
|
+
Setup's output then says how to connect their brain \u2014 create one, pick one, or
|
|
8036
|
+
move existing notes up.
|
|
8037
|
+
|
|
8038
|
+
The \`memgineering-setup\` skill covers what comes after.`;
|
|
8039
|
+
SETUP_NUDGE = "memgineering is installed, but not set up yet. `memgineering setup --web` opens a setup screen in their browser and does NOT wait for them \u2014 ask them first unless they asked you to set it up.\nThe full notes: run `memgineering` on its own.";
|
|
8040
|
+
REFRESH_GUIDANCE = `## The guidance in your agent tools is older than this memgineering.
|
|
8041
|
+
|
|
8042
|
+
The hub and skills this CLI installs into your tool's home were written by an
|
|
8043
|
+
earlier release. One command rewrites them:
|
|
8044
|
+
|
|
8045
|
+
\`\`\`bash
|
|
8046
|
+
memgineering setup
|
|
8047
|
+
\`\`\`
|
|
8048
|
+
|
|
8049
|
+
It does not change which brain this machine reads, or any note. Two things it
|
|
8050
|
+
does change, both worth knowing before it runs:
|
|
8051
|
+
|
|
8052
|
+
- A setup-owned file your user edited is overwritten; setup names it afterwards.
|
|
8053
|
+
- Run with no flags where there is no terminal (an agent's shell), it keeps the
|
|
8054
|
+
auto-update and hook choices recorded on this machine, and installs into
|
|
8055
|
+
every agent tool it finds \u2014 \`--tools\` naming every tool they use keeps it to
|
|
8056
|
+
those (a tool left out keeps its old guidance, and this notice stops
|
|
8057
|
+
appearing anyway). On a machine not signed in, it starts a sign-in and prints
|
|
8058
|
+
a code; \`--no-login\` declines that for a machine they keep local.
|
|
8059
|
+
|
|
8060
|
+
If you are not sure what they chose, ask them. If you run it,
|
|
8061
|
+
mention it to your user in one line: it is upkeep for a tool they already set
|
|
8062
|
+
up, and they should not have to discover it later.
|
|
8063
|
+
|
|
8064
|
+
Guidance files are read at the start of a session, so what changes today
|
|
8065
|
+
arrives in the next one.`;
|
|
8066
|
+
CONNECT_A_BRAIN = `## memgineering is set up, and no brain is connected.
|
|
8067
|
+
|
|
8068
|
+
A brain is a folder of markdown notes. Until one is connected there is nothing
|
|
8069
|
+
to recall and nowhere to remember. Which folder is your user's choice; the
|
|
8070
|
+
command is one you can run for them:
|
|
8071
|
+
|
|
8072
|
+
\`\`\`bash
|
|
8073
|
+
memgineering link <their-notes-folder> # they already keep notes
|
|
8074
|
+
memgineering init <path> # they do not \u2014 creates one, laid out
|
|
8075
|
+
\`\`\`
|
|
8076
|
+
|
|
8077
|
+
\`link\` reports what would be stored before anything is indexed. An existing
|
|
8078
|
+
folder needs no index and no frontmatter of its own \u2014 plain markdown is a brain.
|
|
8079
|
+
|
|
8080
|
+
Then run \`memgineering onboard\`: it prints what to ask so the brain knows whose
|
|
8081
|
+
it is. A brain whose \`01_BASE/\` is still template text answers almost nothing.
|
|
8082
|
+
|
|
8083
|
+
Read \`memgineering-setup\` for accounts and which of their tools this reaches.
|
|
8084
|
+
|
|
8085
|
+
Synced instead? \`memgineering login\`, then \`memgineering create <name>\`.`;
|
|
8086
|
+
POINT_AT_A_BRAIN = `## Signed in, and this machine reads no brain yet.
|
|
8087
|
+
|
|
8088
|
+
\`memgineering brains\` lists what is on the account.
|
|
8089
|
+
\`memgineering use --brain <name>\` points this machine at one.
|
|
8090
|
+
\`memgineering create <name>\` makes one and points here, when the account has none yet.
|
|
8091
|
+
`;
|
|
8092
|
+
UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8093
|
+
}
|
|
8094
|
+
});
|
|
8095
|
+
|
|
7867
8096
|
// src/lib/provenance.ts
|
|
7868
|
-
import { execFile } from "child_process";
|
|
8097
|
+
import { execFile, spawn as spawn2 } from "child_process";
|
|
7869
8098
|
import { realpath as realpath3 } from "fs/promises";
|
|
7870
|
-
import { dirname as dirname8, join as
|
|
8099
|
+
import { dirname as dirname8, join as join24, resolve as resolve14 } from "path";
|
|
7871
8100
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7872
8101
|
import { promisify } from "util";
|
|
7873
|
-
async function detectProvenance(selfPath) {
|
|
8102
|
+
async function detectProvenance(selfPath, opts = {}) {
|
|
7874
8103
|
const entry = selfPath ?? fileURLToPath2(import.meta.url);
|
|
7875
8104
|
const selfDir = await realpath3(dirname8(entry)).catch(() => dirname8(entry));
|
|
7876
8105
|
if (/[\\/]_npx[\\/]|[\\/]\.npm[\\/]_cacache[\\/]/.test(selfDir)) {
|
|
@@ -7882,9 +8111,11 @@ async function detectProvenance(selfPath) {
|
|
|
7882
8111
|
reason: "this is a one-off npx run, not an installed copy"
|
|
7883
8112
|
};
|
|
7884
8113
|
}
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
8114
|
+
const deadline = opts.deadlineMs;
|
|
8115
|
+
const answers = deadline === void 0 ? null : await probeAllWithin(MANAGERS.map((m) => [m.bin, m.prefixArgs]), deadline);
|
|
8116
|
+
for (const [i, manager] of MANAGERS.entries()) {
|
|
8117
|
+
const prefix = answers === null ? await prefixOf(manager.bin, manager.prefixArgs) : answers.prefixes[i];
|
|
8118
|
+
if (prefix === null || prefix === void 0) continue;
|
|
7888
8119
|
if (isInside2(selfDir, manager.moduleRoot(prefix))) {
|
|
7889
8120
|
return {
|
|
7890
8121
|
kind: manager.kind,
|
|
@@ -7895,6 +8126,15 @@ async function detectProvenance(selfPath) {
|
|
|
7895
8126
|
};
|
|
7896
8127
|
}
|
|
7897
8128
|
}
|
|
8129
|
+
if (answers?.timedOut === true) {
|
|
8130
|
+
return {
|
|
8131
|
+
kind: "undetermined",
|
|
8132
|
+
selfDir,
|
|
8133
|
+
prefix: null,
|
|
8134
|
+
command: "npm i -g memgineering@latest",
|
|
8135
|
+
reason: "the package managers did not answer in time to tell which one installed this copy"
|
|
8136
|
+
};
|
|
8137
|
+
}
|
|
7898
8138
|
const local = selfDir.includes(`${"node_modules"}`);
|
|
7899
8139
|
return {
|
|
7900
8140
|
kind: local ? "local" : "unknown",
|
|
@@ -7907,6 +8147,62 @@ async function detectProvenance(selfPath) {
|
|
|
7907
8147
|
function canSelfUpdate(p) {
|
|
7908
8148
|
return p.kind === "npm" || p.kind === "pnpm" || p.kind === "bun";
|
|
7909
8149
|
}
|
|
8150
|
+
async function probeAllWithin(probes, deadlineMs) {
|
|
8151
|
+
const controller = new AbortController();
|
|
8152
|
+
const timer = setTimeout(() => controller.abort(), deadlineMs);
|
|
8153
|
+
timer.unref();
|
|
8154
|
+
try {
|
|
8155
|
+
const prefixes = await Promise.all(
|
|
8156
|
+
probes.map(([bin, args]) => probeOnce(bin, args, controller.signal))
|
|
8157
|
+
);
|
|
8158
|
+
return { prefixes, timedOut: controller.signal.aborted };
|
|
8159
|
+
} finally {
|
|
8160
|
+
clearTimeout(timer);
|
|
8161
|
+
}
|
|
8162
|
+
}
|
|
8163
|
+
function probeOnce(bin, args, signal) {
|
|
8164
|
+
return new Promise((settle) => {
|
|
8165
|
+
let child;
|
|
8166
|
+
try {
|
|
8167
|
+
child = spawn2(bin, args, { detached: true, stdio: ["ignore", "pipe", "ignore"] });
|
|
8168
|
+
} catch {
|
|
8169
|
+
settle(null);
|
|
8170
|
+
return;
|
|
8171
|
+
}
|
|
8172
|
+
let out = "";
|
|
8173
|
+
let done = false;
|
|
8174
|
+
const finish = (value) => {
|
|
8175
|
+
if (done) return;
|
|
8176
|
+
done = true;
|
|
8177
|
+
signal.removeEventListener("abort", stop);
|
|
8178
|
+
settle(value);
|
|
8179
|
+
};
|
|
8180
|
+
function stop() {
|
|
8181
|
+
try {
|
|
8182
|
+
if (process.platform === "win32" || child.pid === void 0) child.kill("SIGKILL");
|
|
8183
|
+
else process.kill(-child.pid, "SIGKILL");
|
|
8184
|
+
} catch {
|
|
8185
|
+
}
|
|
8186
|
+
child.stdout?.destroy();
|
|
8187
|
+
child.unref();
|
|
8188
|
+
finish(null);
|
|
8189
|
+
}
|
|
8190
|
+
child.stdout?.on("data", (chunk) => {
|
|
8191
|
+
out += chunk.toString("utf8");
|
|
8192
|
+
});
|
|
8193
|
+
child.on("error", () => finish(null));
|
|
8194
|
+
child.on("close", (code) => {
|
|
8195
|
+
const value = out.trim();
|
|
8196
|
+
if (code !== 0 || value === "") {
|
|
8197
|
+
finish(null);
|
|
8198
|
+
return;
|
|
8199
|
+
}
|
|
8200
|
+
void realpath3(value).catch(() => value).then(finish);
|
|
8201
|
+
});
|
|
8202
|
+
if (signal.aborted) stop();
|
|
8203
|
+
else signal.addEventListener("abort", stop, { once: true });
|
|
8204
|
+
});
|
|
8205
|
+
}
|
|
7910
8206
|
async function prefixOf(bin, args) {
|
|
7911
8207
|
try {
|
|
7912
8208
|
const { stdout } = await run(bin, args, { timeout: 5e3 });
|
|
@@ -7928,7 +8224,7 @@ async function unreachableByName(selfPath) {
|
|
|
7928
8224
|
const p = await detectProvenance(entry);
|
|
7929
8225
|
return p.kind === "local" || p.kind === "npx" ? p.reason : null;
|
|
7930
8226
|
}
|
|
7931
|
-
var run, MANAGERS;
|
|
8227
|
+
var run, MANAGERS, BACKGROUND_PROBE_DEADLINE_MS;
|
|
7932
8228
|
var init_provenance = __esm({
|
|
7933
8229
|
"src/lib/provenance.ts"() {
|
|
7934
8230
|
"use strict";
|
|
@@ -7938,7 +8234,7 @@ var init_provenance = __esm({
|
|
|
7938
8234
|
kind: "npm",
|
|
7939
8235
|
bin: "npm",
|
|
7940
8236
|
prefixArgs: ["prefix", "-g"],
|
|
7941
|
-
moduleRoot: (prefix) => process.platform === "win32" ?
|
|
8237
|
+
moduleRoot: (prefix) => process.platform === "win32" ? join24(prefix, "node_modules") : join24(prefix, "lib", "node_modules"),
|
|
7942
8238
|
install: "npm i -g memgineering@latest"
|
|
7943
8239
|
},
|
|
7944
8240
|
{
|
|
@@ -7957,6 +8253,7 @@ var init_provenance = __esm({
|
|
|
7957
8253
|
install: "bun add -g memgineering"
|
|
7958
8254
|
}
|
|
7959
8255
|
];
|
|
8256
|
+
BACKGROUND_PROBE_DEADLINE_MS = 1500;
|
|
7960
8257
|
}
|
|
7961
8258
|
});
|
|
7962
8259
|
|
|
@@ -7972,24 +8269,29 @@ __export(setup_cloud_exports, {
|
|
|
7972
8269
|
runSetupCloudChild: () => runSetupCloudChild,
|
|
7973
8270
|
waitAndApply: () => waitAndApply
|
|
7974
8271
|
});
|
|
7975
|
-
import { spawn as
|
|
8272
|
+
import { spawn as spawn3 } from "child_process";
|
|
7976
8273
|
import { realpathSync as realpathSync2 } from "fs";
|
|
7977
8274
|
import { readdir as readdir7, stat as stat6 } from "fs/promises";
|
|
7978
8275
|
import { homedir as homedir5 } from "os";
|
|
7979
|
-
import { basename as basename3, dirname as dirname9, join as
|
|
7980
|
-
async function report2(setupToken, state, detail) {
|
|
8276
|
+
import { basename as basename3, dirname as dirname9, join as join25, resolve as resolve15, sep as sep4 } from "path";
|
|
8277
|
+
async function report2(setupToken, state, detail, code) {
|
|
7981
8278
|
try {
|
|
7982
8279
|
await apiRequest({
|
|
7983
8280
|
method: "POST",
|
|
7984
8281
|
path: "/v1/setup/session/report",
|
|
7985
|
-
body: {
|
|
8282
|
+
body: {
|
|
8283
|
+
setup_token: setupToken,
|
|
8284
|
+
state,
|
|
8285
|
+
...detail === void 0 ? {} : { detail },
|
|
8286
|
+
...code === void 0 ? {} : { code }
|
|
8287
|
+
}
|
|
7986
8288
|
});
|
|
7987
8289
|
} catch {
|
|
7988
8290
|
}
|
|
7989
8291
|
}
|
|
7990
8292
|
async function folderCandidates() {
|
|
7991
8293
|
const home = homedir5();
|
|
7992
|
-
const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) =>
|
|
8294
|
+
const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) => join25(home, d));
|
|
7993
8295
|
const found = [];
|
|
7994
8296
|
const countNotes = async (dir) => {
|
|
7995
8297
|
let count = 0;
|
|
@@ -7998,7 +8300,7 @@ async function folderCandidates() {
|
|
|
7998
8300
|
if (entry.name.startsWith(".")) continue;
|
|
7999
8301
|
if (entry.isFile() && /\.(md|markdown)$/i.test(entry.name)) count += 1;
|
|
8000
8302
|
else if (entry.isDirectory()) {
|
|
8001
|
-
const inner = await readdir7(
|
|
8303
|
+
const inner = await readdir7(join25(dir, entry.name)).catch(() => []);
|
|
8002
8304
|
count += inner.filter((n) => /\.(md|markdown)$/i.test(n)).length;
|
|
8003
8305
|
}
|
|
8004
8306
|
if (count > 999) break;
|
|
@@ -8058,17 +8360,24 @@ async function applyAnswers(setupToken, claim, detected, guards = { candidates:
|
|
|
8058
8360
|
...cfg,
|
|
8059
8361
|
system: {
|
|
8060
8362
|
...cfg.system,
|
|
8061
|
-
setupConnected: {
|
|
8363
|
+
setupConnected: {
|
|
8364
|
+
account_id: account.id,
|
|
8365
|
+
email: account.email,
|
|
8366
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8367
|
+
}
|
|
8062
8368
|
}
|
|
8063
8369
|
});
|
|
8064
8370
|
}
|
|
8065
8371
|
}
|
|
8066
|
-
await report2(setupToken, "applying", "\uC124\uC815\uC744 \uC4F0\uB294 \uC911\uC774\uC5D0\uC694.");
|
|
8372
|
+
await report2(setupToken, "applying", "\uC124\uC815\uC744 \uC4F0\uB294 \uC911\uC774\uC5D0\uC694.", "writing_config");
|
|
8067
8373
|
const choice = {
|
|
8068
8374
|
tools: toolsFromAnswers(answers, detected),
|
|
8069
8375
|
autoUpdate: answers.auto_update ? "on" : "off",
|
|
8070
8376
|
errorTelemetry: answers.error_telemetry === null ? "undecided" : answers.error_telemetry ? "on" : "off",
|
|
8071
|
-
hook: answers.hook
|
|
8377
|
+
hook: answers.hook,
|
|
8378
|
+
// The screen always shows the hook switch, so this is the person's answer,
|
|
8379
|
+
// and a later flag-less refresh keeps it only if it is recorded as one.
|
|
8380
|
+
hookAnswer: answers.hook ? "on" : "off"
|
|
8072
8381
|
};
|
|
8073
8382
|
await applySetup(choice, false);
|
|
8074
8383
|
const parts = [];
|
|
@@ -8096,7 +8405,7 @@ async function applyAnswers(setupToken, claim, detected, guards = { candidates:
|
|
|
8096
8405
|
const email = account?.email ?? null;
|
|
8097
8406
|
const yes = guards.confirmUpload ? await guards.confirmUpload({ path, email }) : null;
|
|
8098
8407
|
if (yes === true) {
|
|
8099
|
-
await report2(setupToken, "applying", "\uB178\uD2B8\uB97C \uC62C\uB9AC\uB294 \uC911\uC774\uC5D0\uC694.");
|
|
8408
|
+
await report2(setupToken, "applying", "\uB178\uD2B8\uB97C \uC62C\uB9AC\uB294 \uC911\uC774\uC5D0\uC694.", "uploading");
|
|
8100
8409
|
const { pushCommand: pushCommand2 } = await Promise.resolve().then(() => (init_push(), push_exports));
|
|
8101
8410
|
await pushCommand2().parseAsync(
|
|
8102
8411
|
["--vault", path, ...claim.brain?.id ? ["--brain", claim.brain.id] : []],
|
|
@@ -8119,7 +8428,12 @@ async function applyAnswers(setupToken, claim, detected, guards = { candidates:
|
|
|
8119
8428
|
}
|
|
8120
8429
|
}
|
|
8121
8430
|
});
|
|
8122
|
-
await report2(
|
|
8431
|
+
await report2(
|
|
8432
|
+
setupToken,
|
|
8433
|
+
"applying",
|
|
8434
|
+
"\uB178\uD2B8 \uC62C\uB9AC\uAE30\uB294 \uC774 \uCEF4\uD4E8\uD130\uC5D0\uC11C \uD655\uC778\uD55C \uB4A4\uC5D0 \uC2DC\uC791\uB3FC\uC694.",
|
|
8435
|
+
"upload_waits_for_check"
|
|
8436
|
+
);
|
|
8123
8437
|
parts.push(`upload of ${path} waits for a check on this machine`);
|
|
8124
8438
|
} else {
|
|
8125
8439
|
parts.push(`not uploaded: ${path}`);
|
|
@@ -8189,11 +8503,12 @@ async function waitAndApply(setupToken, opts) {
|
|
|
8189
8503
|
candidates: offered,
|
|
8190
8504
|
confirmUpload: opts.confirmUpload ?? null
|
|
8191
8505
|
});
|
|
8192
|
-
await report2(setupToken, "done", "\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694.");
|
|
8506
|
+
await report2(setupToken, "done", "\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694.", "done");
|
|
8193
8507
|
return done;
|
|
8194
8508
|
} catch (err) {
|
|
8195
8509
|
const message = err instanceof Error ? err.message : String(err);
|
|
8196
|
-
|
|
8510
|
+
const code = err.code === "conflict" ? "different_account" : "failed";
|
|
8511
|
+
await report2(setupToken, "failed", message.slice(0, 400), code);
|
|
8197
8512
|
throw err;
|
|
8198
8513
|
}
|
|
8199
8514
|
}
|
|
@@ -8205,7 +8520,7 @@ async function waitAndApply(setupToken, opts) {
|
|
|
8205
8520
|
}).catch(() => null);
|
|
8206
8521
|
if (state?.body?.folders_requested === true) {
|
|
8207
8522
|
sentFolders = true;
|
|
8208
|
-
opts.onProgress?.("
|
|
8523
|
+
opts.onProgress?.("Looking for note folders on this machine\u2026");
|
|
8209
8524
|
const candidates = await folderCandidates();
|
|
8210
8525
|
offered = candidates.map((c2) => c2.path);
|
|
8211
8526
|
await apiRequest({
|
|
@@ -8236,7 +8551,7 @@ async function startSession(detected) {
|
|
|
8236
8551
|
return body;
|
|
8237
8552
|
}
|
|
8238
8553
|
function detachChild(setupToken) {
|
|
8239
|
-
const child =
|
|
8554
|
+
const child = spawn3(process.execPath, [process.argv[1] ?? "", "setup", "--web"], {
|
|
8240
8555
|
detached: true,
|
|
8241
8556
|
stdio: ["pipe", "ignore", "ignore"],
|
|
8242
8557
|
env: { ...process.env, [SETUP_CHILD_ENV]: "1" }
|
|
@@ -8255,7 +8570,7 @@ async function runSetupCloud(opts) {
|
|
|
8255
8570
|
const session = await startSession(opts.detected);
|
|
8256
8571
|
const intervalMs = Math.max(1, session.interval ?? 5) * 1e3;
|
|
8257
8572
|
printHuman(`
|
|
8258
|
-
|
|
8573
|
+
Setup code: ${c.bold(session.user_code)}
|
|
8259
8574
|
`);
|
|
8260
8575
|
const opened = opts.printUrl ? false : await openBrowser(session.setup_url);
|
|
8261
8576
|
const foreground = opts.wait || process.stdout.isTTY === true && process.stderr.isTTY === true;
|
|
@@ -8271,14 +8586,14 @@ async function runSetupCloud(opts) {
|
|
|
8271
8586
|
waiting: foreground
|
|
8272
8587
|
},
|
|
8273
8588
|
human: () => {
|
|
8274
|
-
if (opened) printHuman("
|
|
8589
|
+
if (opened) printHuman("Opened the setup screen in the browser.");
|
|
8275
8590
|
else {
|
|
8276
|
-
printHuman("
|
|
8591
|
+
printHuman("Open this link in a browser to finish the setup:\n");
|
|
8277
8592
|
printHuman(` ${session.setup_url}`);
|
|
8278
8593
|
}
|
|
8279
8594
|
printHuman(
|
|
8280
8595
|
c.gray(
|
|
8281
|
-
foreground ? "\n
|
|
8596
|
+
foreground ? "\n When the screen is done, the install continues here. Ctrl-C stops waiting." : "\n When the screen is done, this machine installs on its own. This window can be closed."
|
|
8282
8597
|
)
|
|
8283
8598
|
);
|
|
8284
8599
|
}
|
|
@@ -8296,7 +8611,7 @@ async function runSetupCloud(opts) {
|
|
|
8296
8611
|
confirmUpload: interactive ? askBeforeUpload : null
|
|
8297
8612
|
});
|
|
8298
8613
|
if (outcome === "expired") {
|
|
8299
|
-
const line = "
|
|
8614
|
+
const line = "The setup screen expired and nothing was changed. `memgineering setup --web` opens a new one.";
|
|
8300
8615
|
if (isJsonMode()) notice(JSON.stringify({ setup_web: "expired" }));
|
|
8301
8616
|
else printHuman(c.gray(`
|
|
8302
8617
|
${line}`));
|
|
@@ -8304,10 +8619,10 @@ ${line}`));
|
|
|
8304
8619
|
}
|
|
8305
8620
|
if (isJsonMode()) notice(JSON.stringify({ setup_web: "applied" }));
|
|
8306
8621
|
else {
|
|
8307
|
-
printHuman(c.green("\
|
|
8622
|
+
printHuman(c.green("\nSetup finished."));
|
|
8308
8623
|
printHuman(
|
|
8309
8624
|
c.gray(
|
|
8310
|
-
"
|
|
8625
|
+
" recall and remember work now.\n The newly installed guidance is read from the next conversation on."
|
|
8311
8626
|
)
|
|
8312
8627
|
);
|
|
8313
8628
|
}
|
|
@@ -8342,7 +8657,7 @@ async function askBeforeUpload(q) {
|
|
|
8342
8657
|
const { yes } = await prompts6({
|
|
8343
8658
|
type: "confirm",
|
|
8344
8659
|
name: "yes",
|
|
8345
|
-
message:
|
|
8660
|
+
message: `Upload the notes in ${q.path} to the brain on ${q.email ?? "this account"}?`,
|
|
8346
8661
|
initial: false
|
|
8347
8662
|
});
|
|
8348
8663
|
return yes === true;
|
|
@@ -8377,8 +8692,8 @@ var init_setup_cloud = __esm({
|
|
|
8377
8692
|
|
|
8378
8693
|
// src/commands/setup.ts
|
|
8379
8694
|
import { createHash as createHash9 } from "crypto";
|
|
8380
|
-
import { access as access2, mkdir as
|
|
8381
|
-
import { dirname as dirname10, join as
|
|
8695
|
+
import { access as access2, mkdir as mkdir16, readFile as readFile25 } from "fs/promises";
|
|
8696
|
+
import { dirname as dirname10, join as join26 } from "path";
|
|
8382
8697
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8383
8698
|
import { Command as Command40 } from "commander";
|
|
8384
8699
|
import prompts5 from "prompts";
|
|
@@ -8403,10 +8718,11 @@ function setupCommand() {
|
|
|
8403
8718
|
"after",
|
|
8404
8719
|
[
|
|
8405
8720
|
"",
|
|
8406
|
-
"Chat apps with no shell
|
|
8407
|
-
`
|
|
8408
|
-
"
|
|
8409
|
-
"
|
|
8721
|
+
"Chat apps with no shell need no setup. They read and write the brain on your",
|
|
8722
|
+
`account (not a folder on this machine) at ${HOSTED_CONNECTOR_URL}:`,
|
|
8723
|
+
" ChatGPT: Settings \u2192 Developer mode \u2192 add an MCP server with that URL, sign in.",
|
|
8724
|
+
" Claude.ai, Claude Desktop, Cowork: Customize \u2192 Connectors \u2192 Add custom",
|
|
8725
|
+
" connector with that URL, sign in."
|
|
8410
8726
|
].join("\n")
|
|
8411
8727
|
).action(async (opts) => {
|
|
8412
8728
|
const detected = await detectTools();
|
|
@@ -8441,16 +8757,18 @@ function setupCommand() {
|
|
|
8441
8757
|
dryRun: opts.dryRun === true
|
|
8442
8758
|
});
|
|
8443
8759
|
const mode = await resolveMode(opts);
|
|
8444
|
-
const
|
|
8760
|
+
const recorded = mode.kind === "human" ? {} : await recordedChoices(detected);
|
|
8761
|
+
const choice = mode.kind === "human" ? await askHuman(detected, opts) : fromFlags(detected, opts, recorded);
|
|
8445
8762
|
const defaultsTaken = !mode.chosen ? [
|
|
8446
|
-
|
|
8763
|
+
// Only what nobody chose, here or on an earlier run.
|
|
8764
|
+
...opts.autoUpdate === void 0 && recorded.autoUpdate === void 0 ? ["auto-update: on"] : [],
|
|
8447
8765
|
// Every callback this flag arms, named — on Claude Code and Codex
|
|
8448
8766
|
// that is two (note summaries at session start, the user's own
|
|
8449
8767
|
// rules before an edit), and saying only the first would be
|
|
8450
8768
|
// reporting half of what was decided on their behalf. On Grok it
|
|
8451
8769
|
// is one, at the end of a turn, and naming the other two there
|
|
8452
8770
|
// reported callbacks that were never installed.
|
|
8453
|
-
...opts.hook === void 0 && choice?.hook ? hooksFor(choice.tools).map((h) => `${h.what}: on`) : []
|
|
8771
|
+
...opts.hook === void 0 && recorded.hook === void 0 && choice?.hook ? hooksFor(choice.tools).map((h) => `${h.what}: on`) : []
|
|
8454
8772
|
] : [];
|
|
8455
8773
|
if (choice === null) {
|
|
8456
8774
|
printHuman(c.yellow("Cancelled \u2014 nothing was changed."));
|
|
@@ -8716,6 +9034,7 @@ async function applySetup(choice, dryRun) {
|
|
|
8716
9034
|
system: {
|
|
8717
9035
|
...cfg.system,
|
|
8718
9036
|
autoUpdate: choice.autoUpdate,
|
|
9037
|
+
...choice.hookAnswer === void 0 ? {} : { hook: choice.hookAnswer },
|
|
8719
9038
|
installed,
|
|
8720
9039
|
// Stamped whether or not anything was rewritten. A run that finds every
|
|
8721
9040
|
// file already current is exactly the run that must clear the notice —
|
|
@@ -8741,7 +9060,7 @@ async function applySetup(choice, dryRun) {
|
|
|
8741
9060
|
}
|
|
8742
9061
|
async function write2() {
|
|
8743
9062
|
for (const tool of choice.tools) {
|
|
8744
|
-
const hubPath =
|
|
9063
|
+
const hubPath = join26(agentHome(), tool.hubFile);
|
|
8745
9064
|
const written = await injectBlock(
|
|
8746
9065
|
hubPath,
|
|
8747
9066
|
hub + tailFor(tool, choice.hook),
|
|
@@ -8756,8 +9075,8 @@ async function applySetup(choice, dryRun) {
|
|
|
8756
9075
|
});
|
|
8757
9076
|
if (!dryRun && written.after !== null) mark(hubPath, written.after);
|
|
8758
9077
|
for (const skill of BRAND.subSkills) {
|
|
8759
|
-
const content = await loadAsset(
|
|
8760
|
-
const path =
|
|
9078
|
+
const content = await loadAsset(join26(skill, "SKILL.md"));
|
|
9079
|
+
const path = join26(agentHome(), tool.skillsDir, skill, "SKILL.md");
|
|
8761
9080
|
const out = await writeIfDifferent(path, content, dryRun);
|
|
8762
9081
|
results.push({
|
|
8763
9082
|
tool: tool.label,
|
|
@@ -8830,6 +9149,23 @@ function globalInstallHint(reason, hooks) {
|
|
|
8830
9149
|
return `${hooks ? "The hooks and the guide call" : "The guide tells your agent to run"} \`memgineering\` by name, and ${reason} \u2014
|
|
8831
9150
|
so nothing reaches it until a global install exists: \`npm i -g memgineering\`. Nothing needs re-running after that.`;
|
|
8832
9151
|
}
|
|
9152
|
+
async function recordedChoices(detected) {
|
|
9153
|
+
const cfg = await loadConfig();
|
|
9154
|
+
if (!isSetUp(cfg)) return {};
|
|
9155
|
+
const hook = cfg.system.hook ?? await hooksArmed(detected);
|
|
9156
|
+
return { autoUpdate: cfg.system.autoUpdate, ...hook === void 0 ? {} : { hook } };
|
|
9157
|
+
}
|
|
9158
|
+
async function hooksArmed(tools) {
|
|
9159
|
+
const capable = tools.filter((t) => t.hooks && t.hooksFile);
|
|
9160
|
+
if (capable.length === 0) return void 0;
|
|
9161
|
+
for (const tool of capable) {
|
|
9162
|
+
const raw = await readFile25(join26(agentHome(), ...(tool.hooksFile ?? "").split("/")), "utf8").catch(
|
|
9163
|
+
() => ""
|
|
9164
|
+
);
|
|
9165
|
+
if (hooksFor([tool]).some((spec) => raw.includes(spec.signature))) return "on";
|
|
9166
|
+
}
|
|
9167
|
+
return "off";
|
|
9168
|
+
}
|
|
8833
9169
|
async function resolveMode(opts) {
|
|
8834
9170
|
if (opts.agent) return { kind: "agent", chosen: true };
|
|
8835
9171
|
if (opts.human) {
|
|
@@ -8863,11 +9199,11 @@ async function detectTools() {
|
|
|
8863
9199
|
const home = agentHome();
|
|
8864
9200
|
const found = [];
|
|
8865
9201
|
for (const tool of TOOLS) {
|
|
8866
|
-
if (await exists2(
|
|
9202
|
+
if (await exists2(join26(home, tool.dir))) found.push(tool);
|
|
8867
9203
|
}
|
|
8868
9204
|
return found;
|
|
8869
9205
|
}
|
|
8870
|
-
function fromFlags(detected, opts) {
|
|
9206
|
+
function fromFlags(detected, opts, recorded = {}) {
|
|
8871
9207
|
const tools = opts.tools ? opts.tools.split(",").map((t) => t.trim().toLowerCase()).map((id) => {
|
|
8872
9208
|
const tool = detected.find((t) => t.id === id);
|
|
8873
9209
|
if (tool) return tool;
|
|
@@ -8886,14 +9222,16 @@ function fromFlags(detected, opts) {
|
|
|
8886
9222
|
`${known.label} creates ~/${known.dir} the first time it runs. If it is installed, open it once, then run setup again. ${detectedList}`
|
|
8887
9223
|
);
|
|
8888
9224
|
}) : detected;
|
|
9225
|
+
const hookAnswer = parseChoice(opts.hook, recorded.hook ?? "on", "--hook");
|
|
8889
9226
|
return {
|
|
8890
9227
|
tools,
|
|
8891
|
-
autoUpdate: parseChoice(opts.autoUpdate, "on", "--auto-update"),
|
|
9228
|
+
autoUpdate: parseChoice(opts.autoUpdate, recorded.autoUpdate ?? "on", "--auto-update"),
|
|
8892
9229
|
errorTelemetry: opts.errorTelemetry === void 0 ? "undecided" : parseChoice(opts.errorTelemetry, "off", "--error-telemetry"),
|
|
8893
9230
|
// Only Claude Code can run something at session start. For the others the
|
|
8894
9231
|
// skill tells the agent to call resurface itself, which is why asking for
|
|
8895
9232
|
// a hook there is not an error — it simply has nowhere to go.
|
|
8896
|
-
hook:
|
|
9233
|
+
hook: hookAnswer === "on" && tools.some((t) => t.hooks),
|
|
9234
|
+
hookAnswer
|
|
8897
9235
|
};
|
|
8898
9236
|
}
|
|
8899
9237
|
function parseChoice(value, fallback, flag2) {
|
|
@@ -8952,17 +9290,19 @@ async function askHuman(detected, opts) {
|
|
|
8952
9290
|
autoUpdate: answers.autoUpdate === false ? "off" : parseChoice(opts.autoUpdate, "on", "--auto-update"),
|
|
8953
9291
|
// An aborted prompt is not an answer — undecided, never a silent yes.
|
|
8954
9292
|
errorTelemetry: answers.errorTelemetry === void 0 ? "undecided" : answers.errorTelemetry ? "on" : "off",
|
|
8955
|
-
hook: answers.hook !== false
|
|
9293
|
+
hook: answers.hook !== false,
|
|
9294
|
+
// Only a question that was put is an answer to record.
|
|
9295
|
+
...answers.hook === void 0 ? {} : { hookAnswer: answers.hook ? "on" : "off" }
|
|
8956
9296
|
};
|
|
8957
9297
|
}
|
|
8958
9298
|
async function injectBlock(filePath, content, dryRun, weWroteThis) {
|
|
8959
9299
|
const block = `${BRAND.hubMarkerStart}
|
|
8960
9300
|
${content.trimEnd()}
|
|
8961
9301
|
${BRAND.hubMarkerEnd}`;
|
|
8962
|
-
const existing = await
|
|
9302
|
+
const existing = await readFile25(filePath, "utf8").catch(() => null);
|
|
8963
9303
|
if (existing === null) {
|
|
8964
9304
|
if (!dryRun) {
|
|
8965
|
-
await
|
|
9305
|
+
await mkdir16(dirname10(filePath), { recursive: true });
|
|
8966
9306
|
await writeThenRename(filePath, `${block}
|
|
8967
9307
|
`);
|
|
8968
9308
|
}
|
|
@@ -9002,11 +9342,11 @@ function looksLikeOurGuide(text) {
|
|
|
9002
9342
|
return text.includes("memgineering recall") && text.includes("memgineering remember");
|
|
9003
9343
|
}
|
|
9004
9344
|
async function writeIfDifferent(filePath, content, dryRun) {
|
|
9005
|
-
const existing = await
|
|
9345
|
+
const existing = await readFile25(filePath, "utf8").catch(() => null);
|
|
9006
9346
|
const after = fingerprint(content);
|
|
9007
9347
|
if (existing === content) return { outcome: "unchanged", before: existing, after };
|
|
9008
9348
|
if (!dryRun) {
|
|
9009
|
-
await
|
|
9349
|
+
await mkdir16(dirname10(filePath), { recursive: true });
|
|
9010
9350
|
await writeThenRename(filePath, content);
|
|
9011
9351
|
}
|
|
9012
9352
|
return { outcome: existing === null ? "injected" : "updated", before: existing, after };
|
|
@@ -9017,8 +9357,8 @@ function hooksFor(tools) {
|
|
|
9017
9357
|
);
|
|
9018
9358
|
}
|
|
9019
9359
|
async function installHook(spec, tool, dryRun) {
|
|
9020
|
-
const settingsPath =
|
|
9021
|
-
const raw = await
|
|
9360
|
+
const settingsPath = join26(agentHome(), ...(tool.hooksFile ?? "").split("/"));
|
|
9361
|
+
const raw = await readFile25(settingsPath, "utf8").catch(() => null);
|
|
9022
9362
|
let settings = {};
|
|
9023
9363
|
if (raw !== null) {
|
|
9024
9364
|
try {
|
|
@@ -9060,15 +9400,15 @@ async function installHook(spec, tool, dryRun) {
|
|
|
9060
9400
|
}
|
|
9061
9401
|
const next = { ...settings, hooks: { ...hooks, [spec.event]: existingGroups } };
|
|
9062
9402
|
if (!dryRun) {
|
|
9063
|
-
await
|
|
9403
|
+
await mkdir16(dirname10(settingsPath), { recursive: true });
|
|
9064
9404
|
await writeThenRename(settingsPath, `${JSON.stringify(next, null, 2)}
|
|
9065
9405
|
`);
|
|
9066
9406
|
}
|
|
9067
9407
|
return raw === null ? "injected" : "updated";
|
|
9068
9408
|
}
|
|
9069
9409
|
async function removeHook(spec, tool, dryRun) {
|
|
9070
|
-
const settingsPath =
|
|
9071
|
-
const raw = await
|
|
9410
|
+
const settingsPath = join26(agentHome(), ...(tool.hooksFile ?? "").split("/"));
|
|
9411
|
+
const raw = await readFile25(settingsPath, "utf8").catch(() => null);
|
|
9072
9412
|
if (raw === null) return "unchanged";
|
|
9073
9413
|
let parsed;
|
|
9074
9414
|
try {
|
|
@@ -9092,7 +9432,7 @@ async function removeHook(spec, tool, dryRun) {
|
|
|
9092
9432
|
return "removed";
|
|
9093
9433
|
}
|
|
9094
9434
|
async function loadAsset(relPath) {
|
|
9095
|
-
const raw = await
|
|
9435
|
+
const raw = await readFile25(join26(await assetRoot(), relPath), "utf8");
|
|
9096
9436
|
if (relPath === BRAND.hubFileName && raw.startsWith("---")) {
|
|
9097
9437
|
const end = raw.indexOf("\n---", 4);
|
|
9098
9438
|
if (end !== -1) return raw.slice(end + 4).trimStart();
|
|
@@ -9101,12 +9441,12 @@ async function loadAsset(relPath) {
|
|
|
9101
9441
|
}
|
|
9102
9442
|
async function assetRoot() {
|
|
9103
9443
|
const here = dirname10(fileURLToPath3(import.meta.url));
|
|
9104
|
-
const bundled =
|
|
9105
|
-
if (await exists2(
|
|
9444
|
+
const bundled = join26(here, "..", "assets");
|
|
9445
|
+
if (await exists2(join26(bundled, BRAND.hubFileName))) return bundled;
|
|
9106
9446
|
let dir = here;
|
|
9107
9447
|
for (; ; ) {
|
|
9108
|
-
const candidate =
|
|
9109
|
-
if (await exists2(
|
|
9448
|
+
const candidate = join26(dir, "packages", "skills");
|
|
9449
|
+
if (await exists2(join26(candidate, BRAND.hubFileName))) return candidate;
|
|
9110
9450
|
const parent = dirname10(dir);
|
|
9111
9451
|
if (parent === dir) break;
|
|
9112
9452
|
dir = parent;
|
|
@@ -9136,6 +9476,7 @@ var init_setup = __esm({
|
|
|
9136
9476
|
init_setup_brain();
|
|
9137
9477
|
init_setup_signin();
|
|
9138
9478
|
init_config();
|
|
9479
|
+
init_next_step();
|
|
9139
9480
|
init_provenance();
|
|
9140
9481
|
init_ui();
|
|
9141
9482
|
init_version();
|
|
@@ -13040,34 +13381,21 @@ var BASE_FILES = [
|
|
|
13040
13381
|
];
|
|
13041
13382
|
var HUB_PATH = "00_HUB/HUB.md";
|
|
13042
13383
|
var TEMPLATE_SLOTS = 2;
|
|
13043
|
-
function capTemplates(cards, isTemplate) {
|
|
13384
|
+
function capTemplates(cards, isTemplate, inConflict = () => false) {
|
|
13044
13385
|
const kept = [];
|
|
13045
13386
|
const withheld = [];
|
|
13046
13387
|
let templates = 0;
|
|
13047
13388
|
for (const card of cards) {
|
|
13048
|
-
if (isTemplate(card) && ++templates > TEMPLATE_SLOTS) withheld.push(card);
|
|
13389
|
+
if (isTemplate(card) && !inConflict(card) && ++templates > TEMPLATE_SLOTS) withheld.push(card);
|
|
13049
13390
|
else kept.push(card);
|
|
13050
13391
|
}
|
|
13051
13392
|
return { kept, withheld };
|
|
13052
13393
|
}
|
|
13053
|
-
function bodyOf2(content) {
|
|
13054
|
-
const stripped = content.replace(/^/, "");
|
|
13055
|
-
const fence2 = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
|
|
13056
|
-
return (fence2 ? stripped.slice(fence2[0].length) : stripped).trim();
|
|
13057
|
-
}
|
|
13058
|
-
function templatesFor(path, current) {
|
|
13059
|
-
const retired = ownEntry(RETIRED_BASE_TEMPLATES, path) ?? [];
|
|
13060
|
-
return current === void 0 ? retired : [current, ...retired];
|
|
13061
|
-
}
|
|
13062
13394
|
function isBaseFilePath(path) {
|
|
13063
13395
|
return path.startsWith("01_BASE/");
|
|
13064
13396
|
}
|
|
13065
13397
|
function bodyStillTemplate(path, content) {
|
|
13066
|
-
|
|
13067
|
-
if (template === void 0) return false;
|
|
13068
|
-
const body = bodyOf2(content);
|
|
13069
|
-
if (body === "") return false;
|
|
13070
|
-
return templatesFor(path, template).some((t) => body === bodyOf2(t));
|
|
13398
|
+
return isTemplateBody(path, content);
|
|
13071
13399
|
}
|
|
13072
13400
|
async function scaffoldingAmong(adapter, notes) {
|
|
13073
13401
|
const basePaths = new Set(BASE_FILES.map((f) => f.path));
|
|
@@ -13082,19 +13410,18 @@ async function scaffoldingAmong(adapter, notes) {
|
|
|
13082
13410
|
async function untouchedHub(adapter, title) {
|
|
13083
13411
|
const actual = await adapter.read(HUB_PATH).then((r) => r.content).catch(() => null);
|
|
13084
13412
|
if (actual === null) return false;
|
|
13085
|
-
return actual
|
|
13413
|
+
return asSaved2(actual) === asSaved2(files(title)[HUB_PATH] ?? "");
|
|
13414
|
+
}
|
|
13415
|
+
function asSaved2(text) {
|
|
13416
|
+
const BOM3 = String.fromCharCode(65279);
|
|
13417
|
+
return (text.startsWith(BOM3) ? text.slice(BOM3.length) : text).replace(/\r\n/g, "\n").trimEnd();
|
|
13086
13418
|
}
|
|
13087
13419
|
async function untouchedBase(adapter, paths) {
|
|
13088
|
-
const templates = files("brain");
|
|
13089
13420
|
const out = /* @__PURE__ */ new Set();
|
|
13090
13421
|
await Promise.all(
|
|
13091
13422
|
paths.map(async (path) => {
|
|
13092
|
-
const template = ownEntry(templates, path);
|
|
13093
|
-
if (template === void 0) return;
|
|
13094
13423
|
const actual = await adapter.read(path).then((r) => r.content).catch(() => null);
|
|
13095
|
-
if (actual
|
|
13096
|
-
const trimmed = actual.trimEnd();
|
|
13097
|
-
if (templatesFor(path, template).some((t) => trimmed === t.trimEnd())) out.add(path);
|
|
13424
|
+
if (actual !== null && isExactUntouchedBase(path, actual)) out.add(path);
|
|
13098
13425
|
})
|
|
13099
13426
|
);
|
|
13100
13427
|
return out;
|
|
@@ -18239,7 +18566,7 @@ function onboardCommand() {
|
|
|
18239
18566
|
...survey.missing.length === 0 ? {} : { missing: survey.missing, missing_hint: MISSING_HINT },
|
|
18240
18567
|
// Spelled out in the machine-readable form too, because an agent
|
|
18241
18568
|
// parsing this is the one that has to do the asking.
|
|
18242
|
-
how_to_write: 'Write the body with your own file tools, then `memgineering revise <path> --action reinforce --claim "<one line>" --summary "<what recall should show>"`. This is the one place editing a brain file directly is right, and only while
|
|
18569
|
+
how_to_write: 'Write the body with your own file tools, then `memgineering revise <path> --action reinforce --claim "<one line>" --summary "<what recall should show>"`. This is the one place editing a brain file directly is right, and only while its body has not been written yet.'
|
|
18243
18570
|
},
|
|
18244
18571
|
human: () => {
|
|
18245
18572
|
if (survey.unfilled.length === 0 && survey.missing.length === 0) {
|
|
@@ -18253,7 +18580,7 @@ function onboardCommand() {
|
|
|
18253
18580
|
);
|
|
18254
18581
|
return;
|
|
18255
18582
|
}
|
|
18256
|
-
printHuman(`${survey.heading} (${survey.unfilled.length} of 5 still
|
|
18583
|
+
printHuman(`${survey.heading} (${survey.unfilled.length} of 5 still to write)
|
|
18257
18584
|
`);
|
|
18258
18585
|
if (survey.unfilled.length > 0) {
|
|
18259
18586
|
printHuman(
|
|
@@ -18294,21 +18621,23 @@ function onboardCommand() {
|
|
|
18294
18621
|
var MISSING_HINT = "A note that does not exist cannot be revised into \u2014 `revise` answers `ref_not_found`. `memgineering create <name>` writes the base notes into a brain that holds nothing but its own scaffolding. A brain that already holds other notes takes them from a folder: `memgineering init <folder>` then `memgineering push --brain <name>`, which skips every path already up there.";
|
|
18295
18622
|
async function localSurvey(where) {
|
|
18296
18623
|
const adapter = await openBrainOrExplain(where.brain.root);
|
|
18297
|
-
const
|
|
18298
|
-
|
|
18299
|
-
BASE_FILES.map((f) =>
|
|
18624
|
+
const unwritten = /* @__PURE__ */ new Set();
|
|
18625
|
+
await Promise.all(
|
|
18626
|
+
BASE_FILES.map(async (f) => {
|
|
18627
|
+
const content = await adapter.read(f.path).then((r) => r.content).catch(() => null);
|
|
18628
|
+
if (content !== null && isUnwrittenBody(f.path, content)) unwritten.add(f.path);
|
|
18629
|
+
})
|
|
18300
18630
|
);
|
|
18301
18631
|
return {
|
|
18302
18632
|
identity: { brain: where.brain.root },
|
|
18303
18633
|
heading: "## onboard",
|
|
18304
|
-
unfilled: BASE_FILES.filter((f) =>
|
|
18305
|
-
// A file that is not there reads as written here
|
|
18306
|
-
//
|
|
18307
|
-
//
|
|
18308
|
-
//
|
|
18309
|
-
//
|
|
18310
|
-
|
|
18311
|
-
filled: BASE_FILES.filter((f) => !untouched.has(f.path)).map((f) => f.path),
|
|
18634
|
+
unfilled: BASE_FILES.filter((f) => unwritten.has(f.path)),
|
|
18635
|
+
// A file that is not there reads as written here. That is a choice kept
|
|
18636
|
+
// from before, not something this side cannot tell: `init` writes all
|
|
18637
|
+
// five, so the state does not arise on a folder brain, and asking about a
|
|
18638
|
+
// file that is gone would send an agent to revise a ref that does not
|
|
18639
|
+
// exist. The hosted half CAN arrive without them, and says so (`missing`).
|
|
18640
|
+
filled: BASE_FILES.filter((f) => !unwritten.has(f.path)).map((f) => f.path),
|
|
18312
18641
|
missing: []
|
|
18313
18642
|
};
|
|
18314
18643
|
}
|
|
@@ -18317,7 +18646,7 @@ async function hostedSurvey(where) {
|
|
|
18317
18646
|
const state = (file) => {
|
|
18318
18647
|
const content = contents.get(file.path);
|
|
18319
18648
|
if (content === void 0) return "missing";
|
|
18320
|
-
return
|
|
18649
|
+
return isUnwrittenBody(file.path, content) ? "unfilled" : "filled";
|
|
18321
18650
|
};
|
|
18322
18651
|
return {
|
|
18323
18652
|
identity: { brain: where.name, brain_id: where.brainId },
|
|
@@ -22198,7 +22527,8 @@ function recallCommand() {
|
|
|
22198
22527
|
})
|
|
22199
22528
|
);
|
|
22200
22529
|
isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
|
|
22201
|
-
const
|
|
22530
|
+
const sides = new Set(local.conflicts.flatMap((conflict) => conflict.between));
|
|
22531
|
+
const capped2 = capTemplates(local.memory_cards, isScaffolding, (card) => sides.has(card.id));
|
|
22202
22532
|
templatesWithheld = capped2.withheld.length;
|
|
22203
22533
|
result = { ...local, memory_cards: capped2.kept };
|
|
22204
22534
|
entries = built.entries;
|
|
@@ -22393,7 +22723,7 @@ ${unsearchedLocal.map(
|
|
|
22393
22723
|
if (templatesWithheld > 0) {
|
|
22394
22724
|
printHuman(
|
|
22395
22725
|
c.gray(
|
|
22396
|
-
` ${templatesWithheld} more untouched template${templatesWithheld === 1 ? "" : "s"} matched and ${templatesWithheld === 1 ? "is" : "are"} not shown \u2014 at most ${TEMPLATE_SLOTS} per answer.
|
|
22726
|
+
` ${templatesWithheld} more untouched template${templatesWithheld === 1 ? "" : "s"} matched and ${templatesWithheld === 1 ? "is" : "are"} not shown \u2014 at most ${TEMPLATE_SLOTS} per answer, plus any that is one side of a conflict.
|
|
22397
22727
|
`
|
|
22398
22728
|
)
|
|
22399
22729
|
);
|
|
@@ -23024,205 +23354,14 @@ init_setup();
|
|
|
23024
23354
|
init_brand();
|
|
23025
23355
|
init_version();
|
|
23026
23356
|
init_config();
|
|
23027
|
-
|
|
23357
|
+
init_next_step();
|
|
23358
|
+
init_provenance();
|
|
23359
|
+
import { spawn as spawn4 } from "child_process";
|
|
23028
23360
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
23029
|
-
import { mkdir as mkdir18, open, readFile as readFile27, stat as stat7, unlink as unlink4 } from "fs/promises";
|
|
23361
|
+
import { mkdir as mkdir18, open, readFile as readFile27, rename as rename4, rm as rm8, stat as stat7, unlink as unlink4, writeFile as writeFile15 } from "fs/promises";
|
|
23030
23362
|
import { join as join28 } from "path";
|
|
23031
23363
|
import { Command as Command41 } from "commander";
|
|
23032
23364
|
|
|
23033
|
-
// src/lib/next-step.ts
|
|
23034
|
-
init_brand();
|
|
23035
|
-
init_api_client();
|
|
23036
|
-
init_credentials();
|
|
23037
|
-
init_config();
|
|
23038
|
-
init_version();
|
|
23039
|
-
import { mkdir as mkdir16, readFile as readFile25, writeFile as writeFile13 } from "fs/promises";
|
|
23040
|
-
import { join as join26 } from "path";
|
|
23041
|
-
function isSetUp(cfg) {
|
|
23042
|
-
return Object.keys(cfg.system.installed).length > 0;
|
|
23043
|
-
}
|
|
23044
|
-
var FINISH_SETUP = `## memgineering is installed, but not set up yet.
|
|
23045
|
-
|
|
23046
|
-
Nothing is connected: there is no brain to read, and the agent tools on this
|
|
23047
|
-
machine have no memgineering guidance yet. Setup is one command:
|
|
23048
|
-
|
|
23049
|
-
\`\`\`bash
|
|
23050
|
-
memgineering setup --web
|
|
23051
|
-
\`\`\`
|
|
23052
|
-
|
|
23053
|
-
It does NOT wait for them. It returns in a few seconds \u2014 measured \u2014 and leaves
|
|
23054
|
-
a setup screen open in their browser at memgineering.com, where they choose
|
|
23055
|
-
everything: signing in, where the memory lives, which notes. Nothing is
|
|
23056
|
-
installed until they finish there, and this machine picks it up on its own.
|
|
23057
|
-
|
|
23058
|
-
If they have not already asked you to set it up, ask them first \u2014 it opens a
|
|
23059
|
-
browser window on their computer. Once you run it, tell them in their language
|
|
23060
|
-
that the setup screen is open. It prints a short code first: show it to them,
|
|
23061
|
-
because the screen asks them to compare it.
|
|
23062
|
-
|
|
23063
|
-
No browser on this machine (a server, a container, a sandbox)? Run
|
|
23064
|
-
\`memgineering setup --web --print-url\` and give them the link to open on the
|
|
23065
|
-
computer they are actually sitting at.
|
|
23066
|
-
|
|
23067
|
-
\`setup --agent\` answers every one of those questions on their behalf, and
|
|
23068
|
-
\`--no-login\` answers the biggest one \u2014 no account, so the memory cannot follow
|
|
23069
|
-
them to another machine. Use it only when they have said they would rather you
|
|
23070
|
-
decided, or when this machine cannot reach a browser and \`--print-url\` is no
|
|
23071
|
-
use to them either.
|
|
23072
|
-
|
|
23073
|
-
Setup's output then says how to connect their brain \u2014 create one, pick one, or
|
|
23074
|
-
move existing notes up.
|
|
23075
|
-
|
|
23076
|
-
The \`memgineering-setup\` skill covers what comes after.`;
|
|
23077
|
-
var SETUP_NUDGE = "memgineering is installed, but not set up yet. `memgineering setup --web` opens a setup screen in their browser and does NOT wait for them \u2014 ask them first unless they asked you to set it up.\nThe full notes: run `memgineering` on its own.";
|
|
23078
|
-
function connectedMarker(at) {
|
|
23079
|
-
return join26(brandHome(), `setup-connected-shown-${at.replace(/[^0-9A-Za-z]/g, "")}`);
|
|
23080
|
-
}
|
|
23081
|
-
async function connectedNoticeShown(at) {
|
|
23082
|
-
return readFile25(connectedMarker(at), "utf8").then(
|
|
23083
|
-
() => true,
|
|
23084
|
-
() => false
|
|
23085
|
-
);
|
|
23086
|
-
}
|
|
23087
|
-
async function markConnectedNoticeShown(at) {
|
|
23088
|
-
try {
|
|
23089
|
-
await mkdir16(brandHome(), { recursive: true });
|
|
23090
|
-
await writeFile13(connectedMarker(at), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
23091
|
-
`);
|
|
23092
|
-
} catch {
|
|
23093
|
-
}
|
|
23094
|
-
}
|
|
23095
|
-
async function claimFullSetupNotice() {
|
|
23096
|
-
const marker = join26(brandHome(), "setup-notice-shown");
|
|
23097
|
-
try {
|
|
23098
|
-
await readFile25(marker, "utf8");
|
|
23099
|
-
return false;
|
|
23100
|
-
} catch {
|
|
23101
|
-
}
|
|
23102
|
-
try {
|
|
23103
|
-
await mkdir16(brandHome(), { recursive: true });
|
|
23104
|
-
await writeFile13(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
23105
|
-
`);
|
|
23106
|
-
} catch {
|
|
23107
|
-
}
|
|
23108
|
-
return true;
|
|
23109
|
-
}
|
|
23110
|
-
var REFRESH_GUIDANCE = `## The guidance in your agent tools is older than this memgineering.
|
|
23111
|
-
|
|
23112
|
-
The hub and skills this CLI installs into your tool's home were written by an
|
|
23113
|
-
earlier release. One command rewrites them:
|
|
23114
|
-
|
|
23115
|
-
\`\`\`bash
|
|
23116
|
-
memgineering setup
|
|
23117
|
-
\`\`\`
|
|
23118
|
-
|
|
23119
|
-
It does not change which brain this machine reads, or any note. Two things it
|
|
23120
|
-
does change, both worth knowing before it runs:
|
|
23121
|
-
|
|
23122
|
-
- A setup-owned file your user edited is overwritten; setup names it afterwards.
|
|
23123
|
-
- Run with no flags where there is no terminal (an agent's shell), it applies
|
|
23124
|
-
setup's defaults over whatever they chose before: auto-update on, its hooks
|
|
23125
|
-
on, every agent tool it finds \u2014 and on a machine not signed in, it starts a
|
|
23126
|
-
sign-in and prints a code. To keep what they chose, pass it:
|
|
23127
|
-
\`--auto-update off\`, \`--hook off\`, \`--no-login\` for a machine they keep
|
|
23128
|
-
local, and \`--tools\` naming every tool they use (a tool left out keeps its
|
|
23129
|
-
old guidance, and this notice stops appearing anyway).
|
|
23130
|
-
|
|
23131
|
-
If you are not sure what they chose, ask them. If you run it,
|
|
23132
|
-
mention it to your user in one line: it is upkeep for a tool they already set
|
|
23133
|
-
up, and they should not have to discover it later.
|
|
23134
|
-
|
|
23135
|
-
Guidance files are read at the start of a session, so what changes today
|
|
23136
|
-
arrives in the next one.`;
|
|
23137
|
-
var CONNECT_A_BRAIN = `## memgineering is set up, and no brain is connected.
|
|
23138
|
-
|
|
23139
|
-
A brain is a folder of markdown notes. Until one is connected there is nothing
|
|
23140
|
-
to recall and nowhere to remember. Which folder is your user's choice; the
|
|
23141
|
-
command is one you can run for them:
|
|
23142
|
-
|
|
23143
|
-
\`\`\`bash
|
|
23144
|
-
memgineering link <their-notes-folder> # they already keep notes
|
|
23145
|
-
memgineering init <path> # they do not \u2014 creates one, laid out
|
|
23146
|
-
\`\`\`
|
|
23147
|
-
|
|
23148
|
-
\`link\` reports what would be stored before anything is indexed. An existing
|
|
23149
|
-
folder needs no index and no frontmatter of its own \u2014 plain markdown is a brain.
|
|
23150
|
-
|
|
23151
|
-
Then run \`memgineering onboard\`: it prints what to ask so the brain knows whose
|
|
23152
|
-
it is. A brain whose \`01_BASE/\` is still template text answers almost nothing.
|
|
23153
|
-
|
|
23154
|
-
Read \`memgineering-setup\` for accounts and which of their tools this reaches.
|
|
23155
|
-
|
|
23156
|
-
Synced instead? \`memgineering login\`, then \`memgineering create <name>\`.`;
|
|
23157
|
-
var POINT_AT_A_BRAIN = `## Signed in, and this machine reads no brain yet.
|
|
23158
|
-
|
|
23159
|
-
\`memgineering brains\` lists what is on the account.
|
|
23160
|
-
\`memgineering use --brain <name>\` points this machine at one.
|
|
23161
|
-
\`memgineering create <name>\` makes one and points here, when the account has none yet.
|
|
23162
|
-
`;
|
|
23163
|
-
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
23164
|
-
function shellQuote(value) {
|
|
23165
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
23166
|
-
}
|
|
23167
|
-
function confirmUploadNotice(p) {
|
|
23168
|
-
const who = p.account_email ?? "the account `setup --web` connected";
|
|
23169
|
-
const brain = p.brain_id && UUID.test(p.brain_id) ? ` --brain ${p.brain_id}` : "";
|
|
23170
|
-
return `## Notes are waiting to be uploaded \u2014 after a check.
|
|
23171
|
-
|
|
23172
|
-
\`memgineering setup --web\` chose to upload \`${p.path}\` to a brain on **${who}**.
|
|
23173
|
-
This machine has not sent them: an upload decided on the setup screen waits
|
|
23174
|
-
until somebody here has seen which account would receive the notes.
|
|
23175
|
-
|
|
23176
|
-
If that is your user's account, this uploads them:
|
|
23177
|
-
|
|
23178
|
-
\`\`\`bash
|
|
23179
|
-
memgineering push --vault ${shellQuote(p.path)}${brain}
|
|
23180
|
-
\`\`\`
|
|
23181
|
-
|
|
23182
|
-
If it is not, \`memgineering logout\` disconnects this machine from that account
|
|
23183
|
-
and nothing is uploaded. If you are not sure which it is, ask them.`;
|
|
23184
|
-
}
|
|
23185
|
-
function setupConnectedNotice(c2) {
|
|
23186
|
-
const who = c2.email ?? "an account with no email on record";
|
|
23187
|
-
return `## This machine is signed in as **${who}**.
|
|
23188
|
-
|
|
23189
|
-
\`memgineering setup --web\` connected it to that account. If that is not your user's account,
|
|
23190
|
-
\`memgineering logout\` disconnects it, and nothing they write reaches that brain.`;
|
|
23191
|
-
}
|
|
23192
|
-
function nextStepFor(cfg, signedIn = false, seen = {}) {
|
|
23193
|
-
if (cfg.system.pendingUpload) {
|
|
23194
|
-
return { id: "confirm_upload", markdown: confirmUploadNotice(cfg.system.pendingUpload) };
|
|
23195
|
-
}
|
|
23196
|
-
if (cfg.system.setupConnected && !seen.connected) {
|
|
23197
|
-
return { id: "setup_connected", markdown: setupConnectedNotice(cfg.system.setupConnected) };
|
|
23198
|
-
}
|
|
23199
|
-
if (signedIn && cfg.brain.cloud === void 0) {
|
|
23200
|
-
return { id: "point_at_a_brain", markdown: POINT_AT_A_BRAIN };
|
|
23201
|
-
}
|
|
23202
|
-
if (isSetUp(cfg) && cfg.system.guidanceVersion !== VERSION) {
|
|
23203
|
-
return { id: "refresh_guidance", markdown: REFRESH_GUIDANCE };
|
|
23204
|
-
}
|
|
23205
|
-
if (cfg.brain.cloud !== void 0 && cfg.brain.cloud.api_url === apiUrl()) return null;
|
|
23206
|
-
if (!isSetUp(cfg)) return { id: "finish_setup", markdown: FINISH_SETUP };
|
|
23207
|
-
if (cfg.brain.brains.length === 0 && cfg.brain.cloud === void 0) {
|
|
23208
|
-
return { id: "connect_a_brain", markdown: CONNECT_A_BRAIN };
|
|
23209
|
-
}
|
|
23210
|
-
return null;
|
|
23211
|
-
}
|
|
23212
|
-
async function currentNextStep() {
|
|
23213
|
-
try {
|
|
23214
|
-
const stored = await readCredentials().catch(() => null);
|
|
23215
|
-
const cfg = await loadConfig();
|
|
23216
|
-
const connected = cfg.system.setupConnected ? await connectedNoticeShown(cfg.system.setupConnected.at) : false;
|
|
23217
|
-
return nextStepFor(cfg, stored !== null && stored.api_url === apiUrl(), { connected });
|
|
23218
|
-
} catch {
|
|
23219
|
-
return null;
|
|
23220
|
-
}
|
|
23221
|
-
}
|
|
23222
|
-
|
|
23223
|
-
// src/commands/update.ts
|
|
23224
|
-
init_provenance();
|
|
23225
|
-
|
|
23226
23365
|
// src/lib/update-check.ts
|
|
23227
23366
|
init_atomic();
|
|
23228
23367
|
init_brand();
|
|
@@ -23432,6 +23571,18 @@ async function runUpdate(opts, inherited) {
|
|
|
23432
23571
|
return;
|
|
23433
23572
|
}
|
|
23434
23573
|
const held = await holdUpdateSlot(inherited);
|
|
23574
|
+
if (held === null && await blockedBy() === "stuck") {
|
|
23575
|
+
const lock = join28(brandHome(), RECLAIM_LOCK);
|
|
23576
|
+
const hint = `A previous update did not finish, and clearing what it left is blocked by ${lock}. If no memgineering update is running, delete that folder and run \`memgineering update\` again.`;
|
|
23577
|
+
printDual({
|
|
23578
|
+
json: { ...status, provenance: provenance.kind, updated: false, reason: "update_claim_stuck", hint },
|
|
23579
|
+
human: () => {
|
|
23580
|
+
printHuman(`memgineering ${status.latest} is available, but a previous update's leftover claim could not be cleared.`);
|
|
23581
|
+
printHuman(c.gray(hint));
|
|
23582
|
+
}
|
|
23583
|
+
});
|
|
23584
|
+
return;
|
|
23585
|
+
}
|
|
23435
23586
|
if (held === null) {
|
|
23436
23587
|
const started = (await claimOwner())?.split(" ")[0];
|
|
23437
23588
|
const hint = "Another memgineering update is installing right now. Run `memgineering update` again in a few minutes \u2014 it reports whether that install worked.";
|
|
@@ -23487,10 +23638,10 @@ async function backgroundUpdateNotice(deps = {}) {
|
|
|
23487
23638
|
const status = await (deps.check ?? checkForUpdate)(VERSION);
|
|
23488
23639
|
await reportPreviousAttempt();
|
|
23489
23640
|
if (status.updateAvailable && cfg.system.autoUpdate === "on" && isSetUp(cfg) && !process.env["CI"]) {
|
|
23490
|
-
const provenance = await (deps.provenance ?? detectProvenance)();
|
|
23641
|
+
const provenance = await (deps.provenance ?? (() => detectProvenance(void 0, { deadlineMs: BACKGROUND_PROBE_DEADLINE_MS })))();
|
|
23491
23642
|
const token = newClaimToken();
|
|
23492
23643
|
if (canSelfUpdate(provenance) && await claimUpdateSlot(token)) {
|
|
23493
|
-
(deps.startUpdate ?? ((t) => startDetachedUpdate(
|
|
23644
|
+
(deps.startUpdate ?? ((t) => startDetachedUpdate(spawn4, t)))(token);
|
|
23494
23645
|
notice(
|
|
23495
23646
|
c.gray(
|
|
23496
23647
|
`memgineering ${status.latest} is available \u2014 installing it in the background (auto-update is on). The next run uses it.`
|
|
@@ -23513,7 +23664,7 @@ async function backgroundUpdateNotice(deps = {}) {
|
|
|
23513
23664
|
} catch {
|
|
23514
23665
|
}
|
|
23515
23666
|
}
|
|
23516
|
-
function startDetachedUpdate(spawnImpl =
|
|
23667
|
+
function startDetachedUpdate(spawnImpl = spawn4, token) {
|
|
23517
23668
|
const child = spawnImpl(process.execPath, [process.argv[1] ?? "", "update"], {
|
|
23518
23669
|
detached: true,
|
|
23519
23670
|
stdio: "ignore",
|
|
@@ -23554,7 +23705,7 @@ async function holdUpdateSlot(inherited) {
|
|
|
23554
23705
|
}
|
|
23555
23706
|
var HOOK_VERBS = /* @__PURE__ */ new Set(["guard", "resurface"]);
|
|
23556
23707
|
var UPDATE_CLAIM_MS = 15 * 6e4;
|
|
23557
|
-
async function claimUpdateSlot(token = newClaimToken()) {
|
|
23708
|
+
async function claimUpdateSlot(token = newClaimToken(), hooks = {}) {
|
|
23558
23709
|
const path = join28(brandHome(), "update-running");
|
|
23559
23710
|
try {
|
|
23560
23711
|
await mkdir18(brandHome(), { recursive: true });
|
|
@@ -23567,17 +23718,86 @@ async function claimUpdateSlot(token = newClaimToken()) {
|
|
|
23567
23718
|
if (err?.code !== "EEXIST") return false;
|
|
23568
23719
|
}
|
|
23569
23720
|
try {
|
|
23570
|
-
if (Date.now() - (await stat7(path)).mtimeMs >= UPDATE_CLAIM_MS)
|
|
23721
|
+
if (Date.now() - (await stat7(path)).mtimeMs >= UPDATE_CLAIM_MS) {
|
|
23722
|
+
await hooks.afterObserve?.();
|
|
23723
|
+
await withReclaimLock(async () => {
|
|
23724
|
+
if (Date.now() - (await stat7(path)).mtimeMs < UPDATE_CLAIM_MS) return;
|
|
23725
|
+
await hooks.afterRecheck?.();
|
|
23726
|
+
await unlink4(path);
|
|
23727
|
+
});
|
|
23728
|
+
}
|
|
23571
23729
|
} catch {
|
|
23572
23730
|
}
|
|
23573
23731
|
return false;
|
|
23574
23732
|
}
|
|
23733
|
+
var RECLAIM_LOCK = "update-running.reclaim";
|
|
23734
|
+
var RECLAIM_LOCK_MAX_MS = 10 * 6e4;
|
|
23735
|
+
async function withReclaimLock(fn) {
|
|
23736
|
+
const home = brandHome();
|
|
23737
|
+
const lock = join28(home, RECLAIM_LOCK);
|
|
23738
|
+
const staging = join28(home, `${RECLAIM_LOCK}.${process.pid}.${randomBytes2(4).toString("hex")}`);
|
|
23739
|
+
await mkdir18(staging, { recursive: true });
|
|
23740
|
+
await writeFile15(join28(staging, "owner"), `${process.pid} ${Date.now()}
|
|
23741
|
+
`);
|
|
23742
|
+
try {
|
|
23743
|
+
await rename4(staging, lock);
|
|
23744
|
+
} catch {
|
|
23745
|
+
await rm8(staging, { recursive: true, force: true }).catch(() => void 0);
|
|
23746
|
+
await breakAbandonedLock(lock);
|
|
23747
|
+
return null;
|
|
23748
|
+
}
|
|
23749
|
+
try {
|
|
23750
|
+
return await fn();
|
|
23751
|
+
} finally {
|
|
23752
|
+
const aside = `${lock}.done.${randomBytes2(4).toString("hex")}`;
|
|
23753
|
+
await rename4(lock, aside).then(() => rm8(aside, { recursive: true, force: true })).catch(() => void 0);
|
|
23754
|
+
}
|
|
23755
|
+
}
|
|
23756
|
+
async function breakAbandonedLock(lock) {
|
|
23757
|
+
try {
|
|
23758
|
+
const seen = (await readFile27(join28(lock, "owner"), "utf8")).trim();
|
|
23759
|
+
if (!lockAbandoned(seen)) return;
|
|
23760
|
+
const aside = `${lock}.dead.${randomBytes2(4).toString("hex")}`;
|
|
23761
|
+
await rename4(lock, aside);
|
|
23762
|
+
const moved = (await readFile27(join28(aside, "owner"), "utf8").catch(() => "")).trim();
|
|
23763
|
+
if (moved !== seen) {
|
|
23764
|
+
await rename4(aside, lock).catch(() => void 0);
|
|
23765
|
+
return;
|
|
23766
|
+
}
|
|
23767
|
+
await rm8(aside, { recursive: true, force: true });
|
|
23768
|
+
} catch {
|
|
23769
|
+
}
|
|
23770
|
+
}
|
|
23771
|
+
function lockAbandoned(owner, now = Date.now()) {
|
|
23772
|
+
const [pidText, atText] = owner.split(" ");
|
|
23773
|
+
const pid = Number(pidText);
|
|
23774
|
+
const at = Number(atText);
|
|
23775
|
+
if (!Number.isFinite(at) || now - at > RECLAIM_LOCK_MAX_MS) return true;
|
|
23776
|
+
if (!Number.isInteger(pid) || pid <= 0) return true;
|
|
23777
|
+
try {
|
|
23778
|
+
process.kill(pid, 0);
|
|
23779
|
+
return false;
|
|
23780
|
+
} catch (err) {
|
|
23781
|
+
return err?.code !== "EPERM";
|
|
23782
|
+
}
|
|
23783
|
+
}
|
|
23784
|
+
async function blockedBy() {
|
|
23785
|
+
try {
|
|
23786
|
+
const old = Date.now() - (await stat7(join28(brandHome(), "update-running"))).mtimeMs >= UPDATE_CLAIM_MS;
|
|
23787
|
+
if (old) {
|
|
23788
|
+
await stat7(join28(brandHome(), RECLAIM_LOCK));
|
|
23789
|
+
return "stuck";
|
|
23790
|
+
}
|
|
23791
|
+
} catch {
|
|
23792
|
+
}
|
|
23793
|
+
return "in_progress";
|
|
23794
|
+
}
|
|
23575
23795
|
async function reportPreviousAttempt() {
|
|
23576
23796
|
const previous = await readUpdateState();
|
|
23577
23797
|
if (!previous || previous.ok) return;
|
|
23578
23798
|
notice(
|
|
23579
23799
|
c.yellow(`\u26A0 The last update attempt did not complete: ${previous.message}`) + c.gray(
|
|
23580
|
-
"\n `memgineering update` shows why. `memgineering setup --agent --auto-update off
|
|
23800
|
+
"\n `memgineering update` shows why. `memgineering setup --agent --auto-update off` stops\n these attempts; the hooks stay as this machine recorded them."
|
|
23581
23801
|
)
|
|
23582
23802
|
);
|
|
23583
23803
|
}
|
|
@@ -23597,7 +23817,7 @@ function reportBelowMinimum(below) {
|
|
|
23597
23817
|
|
|
23598
23818
|
// src/commands/undo.ts
|
|
23599
23819
|
init_src();
|
|
23600
|
-
import { readFile as readFile28, rm as
|
|
23820
|
+
import { readFile as readFile28, rm as rm9 } from "fs/promises";
|
|
23601
23821
|
import { join as join29 } from "path";
|
|
23602
23822
|
import { Command as Command42 } from "commander";
|
|
23603
23823
|
init_atomic();
|
|
@@ -23767,7 +23987,7 @@ async function reverse(brain, op, current) {
|
|
|
23767
23987
|
"Nothing to take back. `memgineering log` shows what this brain recorded."
|
|
23768
23988
|
);
|
|
23769
23989
|
}
|
|
23770
|
-
await
|
|
23990
|
+
await rm9(absolute, { force: true });
|
|
23771
23991
|
return null;
|
|
23772
23992
|
}
|
|
23773
23993
|
if (op.verb === "exclude" || op.verb === "unexclude") {
|
|
@@ -24301,6 +24521,7 @@ function whoamiCommand() {
|
|
|
24301
24521
|
|
|
24302
24522
|
// src/program.ts
|
|
24303
24523
|
init_config();
|
|
24524
|
+
init_next_step();
|
|
24304
24525
|
init_ui();
|
|
24305
24526
|
init_brand();
|
|
24306
24527
|
|
|
@@ -24491,8 +24712,14 @@ async function run3(argv) {
|
|
|
24491
24712
|
process.stderr.isTTY === true ? "person" : cleaned[2] === void 0 ? "full" : "brief"
|
|
24492
24713
|
);
|
|
24493
24714
|
}
|
|
24715
|
+
let ran;
|
|
24716
|
+
program.hook("preAction", (_root, action) => {
|
|
24717
|
+
let command = action;
|
|
24718
|
+
while (command.parent && command.parent !== program) command = command.parent;
|
|
24719
|
+
ran = command === program ? void 0 : command.name();
|
|
24720
|
+
});
|
|
24494
24721
|
await program.parseAsync(cleaned);
|
|
24495
|
-
if (
|
|
24722
|
+
if (ran !== "update") await backgroundUpdateNotice(ran === void 0 ? {} : { verb: ran });
|
|
24496
24723
|
}
|
|
24497
24724
|
async function handleError(err) {
|
|
24498
24725
|
const failure = adaptRegistryError(err) ?? err;
|