memgineering 0.25.0 → 0.25.2
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 +105 -0
- package/assets/MEMGINEERING.md +1 -1
- package/assets/memgineering-recall/SKILL.md +3 -2
- package/assets/memgineering-rules/SKILL.md +14 -0
- package/assets/memgineering-setup/SKILL.md +14 -2
- package/dist/index.js +736 -267
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1341,8 +1341,8 @@ var init_exclude = __esm({
|
|
|
1341
1341
|
[...ALWAYS_SKIP_DIRS].map((d) => fold(d))
|
|
1342
1342
|
);
|
|
1343
1343
|
ALWAYS_SKIP_SEGMENTS_FOLDED = ALWAYS_SKIP_SEGMENTS.map((s) => fold(s));
|
|
1344
|
-
ALWAYS_SKIP_FILE = (
|
|
1345
|
-
const b = fold(
|
|
1344
|
+
ALWAYS_SKIP_FILE = (basename4) => {
|
|
1345
|
+
const b = fold(basename4);
|
|
1346
1346
|
return b.startsWith(".env") || b === ".ds_store" || b === ".memgdeny" || b === ".memgallow" || b === ".memgignore";
|
|
1347
1347
|
};
|
|
1348
1348
|
EMPTY_IGNORE_SET = { rules: [], unsupported: [] };
|
|
@@ -2441,14 +2441,6 @@ var init_planner = __esm({
|
|
|
2441
2441
|
}
|
|
2442
2442
|
});
|
|
2443
2443
|
|
|
2444
|
-
// ../../packages/memory-engine/src/recall/semantic.ts
|
|
2445
|
-
var init_semantic = __esm({
|
|
2446
|
-
"../../packages/memory-engine/src/recall/semantic.ts"() {
|
|
2447
|
-
"use strict";
|
|
2448
|
-
init_markdown_mapping();
|
|
2449
|
-
}
|
|
2450
|
-
});
|
|
2451
|
-
|
|
2452
2444
|
// ../../packages/memory-engine/src/base-templates.ts
|
|
2453
2445
|
function base(id, title, summary, body) {
|
|
2454
2446
|
return [
|
|
@@ -2556,6 +2548,14 @@ var init_base_templates = __esm({
|
|
|
2556
2548
|
}
|
|
2557
2549
|
});
|
|
2558
2550
|
|
|
2551
|
+
// ../../packages/memory-engine/src/recall/semantic.ts
|
|
2552
|
+
var init_semantic = __esm({
|
|
2553
|
+
"../../packages/memory-engine/src/recall/semantic.ts"() {
|
|
2554
|
+
"use strict";
|
|
2555
|
+
init_base_templates();
|
|
2556
|
+
}
|
|
2557
|
+
});
|
|
2558
|
+
|
|
2559
2559
|
// ../../packages/memory-engine/src/recall/unfilled.ts
|
|
2560
2560
|
var UNFILLED_FIELDS;
|
|
2561
2561
|
var init_unfilled = __esm({
|
|
@@ -2577,6 +2577,13 @@ var init_unfilled = __esm({
|
|
|
2577
2577
|
}
|
|
2578
2578
|
});
|
|
2579
2579
|
|
|
2580
|
+
// ../../packages/memory-engine/src/setup-progress.ts
|
|
2581
|
+
var init_setup_progress = __esm({
|
|
2582
|
+
"../../packages/memory-engine/src/setup-progress.ts"() {
|
|
2583
|
+
"use strict";
|
|
2584
|
+
}
|
|
2585
|
+
});
|
|
2586
|
+
|
|
2580
2587
|
// ../../packages/memory-engine/src/recall/query-prefix.ts
|
|
2581
2588
|
var init_query_prefix = __esm({
|
|
2582
2589
|
"../../packages/memory-engine/src/recall/query-prefix.ts"() {
|
|
@@ -4030,6 +4037,7 @@ function saysNothingNew(entry, input, normalize) {
|
|
|
4030
4037
|
if (norm(claim.text) !== norm(previousClaim(entry))) return false;
|
|
4031
4038
|
if (claim.summary != null && norm(claim.summary) !== norm(entry.memory.summary)) return false;
|
|
4032
4039
|
if (claim.title != null && norm(claim.title) !== norm(entry.memory.title)) return false;
|
|
4040
|
+
if (claim.binding != null && claim.binding !== (entry.memory.binding === true)) return false;
|
|
4033
4041
|
const declared = new Set(
|
|
4034
4042
|
entry.relations.filter((r) => r.type === "contradicts").map((r) => r.to)
|
|
4035
4043
|
);
|
|
@@ -4904,6 +4912,7 @@ var init_src = __esm({
|
|
|
4904
4912
|
init_planner();
|
|
4905
4913
|
init_semantic();
|
|
4906
4914
|
init_unfilled();
|
|
4915
|
+
init_setup_progress();
|
|
4907
4916
|
init_query_prefix();
|
|
4908
4917
|
init_base_templates();
|
|
4909
4918
|
init_scope_spread();
|
|
@@ -5137,7 +5146,7 @@ async function apiRequest(args) {
|
|
|
5137
5146
|
if (cause.retryable && attempts < MAX_CONNECT_RETRIES) {
|
|
5138
5147
|
const backoff = args.backoffMs ?? CONNECT_BACKOFF_MS;
|
|
5139
5148
|
const wait = backoff[Math.min(attempts, backoff.length - 1)] ?? 0;
|
|
5140
|
-
await new Promise((
|
|
5149
|
+
await new Promise((resolve17) => setTimeout(resolve17, wait));
|
|
5141
5150
|
return apiRequest({ ...args, connectRetries: attempts + 1 });
|
|
5142
5151
|
}
|
|
5143
5152
|
throw memgError(
|
|
@@ -5161,7 +5170,7 @@ async function apiRequest(args) {
|
|
|
5161
5170
|
if (res.status === 429 && (args.retries ?? MAX_RATE_LIMIT_RETRIES) > 0) {
|
|
5162
5171
|
const wait = retryAfterMs(res.headers.get("retry-after"));
|
|
5163
5172
|
args.onWait?.(wait);
|
|
5164
|
-
await new Promise((
|
|
5173
|
+
await new Promise((resolve17) => setTimeout(resolve17, wait));
|
|
5165
5174
|
return apiRequest({ ...args, retries: (args.retries ?? MAX_RATE_LIMIT_RETRIES) - 1 });
|
|
5166
5175
|
}
|
|
5167
5176
|
const envelope = parsed?.error;
|
|
@@ -5348,6 +5357,25 @@ function findBrain(cfg, root) {
|
|
|
5348
5357
|
const target = resolve(root);
|
|
5349
5358
|
return cfg.brain.brains.find((b) => resolve(b.root) === target);
|
|
5350
5359
|
}
|
|
5360
|
+
async function clearSetupFollowUps(onlyPath) {
|
|
5361
|
+
try {
|
|
5362
|
+
const cfg = await loadConfig();
|
|
5363
|
+
const pending = cfg.system.pendingUpload;
|
|
5364
|
+
const dropUpload = pending !== void 0 && (onlyPath === void 0 || resolve(pending.path) === resolve(onlyPath));
|
|
5365
|
+
const dropConnected = onlyPath === void 0 && cfg.system.setupConnected !== void 0;
|
|
5366
|
+
if (!dropUpload && !dropConnected) return;
|
|
5367
|
+
const { pendingUpload, setupConnected, ...rest } = cfg.system;
|
|
5368
|
+
await saveConfig({
|
|
5369
|
+
...cfg,
|
|
5370
|
+
system: {
|
|
5371
|
+
...rest,
|
|
5372
|
+
...dropUpload ? {} : pendingUpload ? { pendingUpload } : {},
|
|
5373
|
+
...dropConnected ? {} : setupConnected ? { setupConnected } : {}
|
|
5374
|
+
}
|
|
5375
|
+
});
|
|
5376
|
+
} catch {
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5351
5379
|
var BrainSchema, ConfigSchema, EMPTY_CONFIG;
|
|
5352
5380
|
var init_config = __esm({
|
|
5353
5381
|
"src/lib/config.ts"() {
|
|
@@ -5486,7 +5514,38 @@ var init_config = __esm({
|
|
|
5486
5514
|
* Absent means "written before this field existed", which is the same
|
|
5487
5515
|
* answer as "older than this CLI" and wants the same one command.
|
|
5488
5516
|
*/
|
|
5489
|
-
guidanceVersion: z8.string().optional()
|
|
5517
|
+
guidanceVersion: z8.string().optional(),
|
|
5518
|
+
/**
|
|
5519
|
+
* The person's answer to "arm the hooks?", as they gave it — not whether
|
|
5520
|
+
* this machine's tools could take one. A flag-less `setup` reads it back,
|
|
5521
|
+
* so a refresh keeps what they chose instead of setup's default (0.25.2).
|
|
5522
|
+
* Absent on a machine set up before it was recorded.
|
|
5523
|
+
*/
|
|
5524
|
+
hook: z8.enum(["on", "off"]).optional().catch(void 0),
|
|
5525
|
+
/**
|
|
5526
|
+
* Notes `setup --web` was asked to upload, not yet sent.
|
|
5527
|
+
*
|
|
5528
|
+
* The detached half of the setup never uploads on its own (security
|
|
5529
|
+
* audit H-3): whoever confirmed the setup decided which account receives
|
|
5530
|
+
* the notes, and that is checked by somebody on THIS machine first. The
|
|
5531
|
+
* next command's notice names the path and the receiving account; a
|
|
5532
|
+
* `push` of that path, or `logout`, clears it.
|
|
5533
|
+
*/
|
|
5534
|
+
pendingUpload: z8.object({
|
|
5535
|
+
path: z8.string().min(1),
|
|
5536
|
+
brain: z8.string().nullable(),
|
|
5537
|
+
/** The notice names the brain by id only: its name came from the screen. */
|
|
5538
|
+
brain_id: z8.string().nullable().optional(),
|
|
5539
|
+
account_id: z8.string().min(1),
|
|
5540
|
+
account_email: z8.string().nullable(),
|
|
5541
|
+
at: z8.string().min(1)
|
|
5542
|
+
}).optional().catch(void 0),
|
|
5543
|
+
/**
|
|
5544
|
+
* The account a `setup --web` signed this machine into, when it had no
|
|
5545
|
+
* sign-in before — shown once by the next command, because there was no
|
|
5546
|
+
* earlier account to compare it with (H-3).
|
|
5547
|
+
*/
|
|
5548
|
+
setupConnected: z8.object({ account_id: z8.string().min(1), email: z8.string().nullable(), at: z8.string().min(1) }).optional().catch(void 0)
|
|
5490
5549
|
}).default({ autoUpdate: "on" })
|
|
5491
5550
|
});
|
|
5492
5551
|
EMPTY_CONFIG = ConfigSchema.parse({});
|
|
@@ -5503,6 +5562,10 @@ async function openCloudBrain(target) {
|
|
|
5503
5562
|
apiUrl: target.apiUrl
|
|
5504
5563
|
};
|
|
5505
5564
|
}
|
|
5565
|
+
function connectorLabel(writtenBy) {
|
|
5566
|
+
const app = writtenBy.replace(/^mcp:/, "").replace(/[^A-Za-z0-9 ._-]/g, "").replace(/\s+/g, " ").trim().slice(0, 32).trim();
|
|
5567
|
+
return app === "" || app.toLowerCase() === "connector" ? "written by a chat connector" : `written by the ${app} connector`;
|
|
5568
|
+
}
|
|
5506
5569
|
function findCloudBrain(brains, named) {
|
|
5507
5570
|
return brains.find((brain) => brain.id === named) ?? brains.find((brain) => brain.name === named);
|
|
5508
5571
|
}
|
|
@@ -7054,12 +7117,12 @@ var init_deadline = __esm({
|
|
|
7054
7117
|
import { spawn } from "child_process";
|
|
7055
7118
|
async function openBrowser(url) {
|
|
7056
7119
|
const { file, args } = browserOpener(url);
|
|
7057
|
-
return new Promise((
|
|
7120
|
+
return new Promise((resolve17) => {
|
|
7058
7121
|
let settled = false;
|
|
7059
7122
|
const finish = (opened) => {
|
|
7060
7123
|
if (settled) return;
|
|
7061
7124
|
settled = true;
|
|
7062
|
-
|
|
7125
|
+
resolve17(opened);
|
|
7063
7126
|
};
|
|
7064
7127
|
const child = spawn(file, args, { detached: true, stdio: "ignore" });
|
|
7065
7128
|
child.on("error", () => finish(false));
|
|
@@ -7089,7 +7152,7 @@ function loginCommand() {
|
|
|
7089
7152
|
"start the sign-in, open the browser, and return immediately \u2014 THE path an agent should use on a user\u2019s behalf; the next command claims the token automatically. Prefer this over the plain form, whose wait for a human click will outlast an agent\u2019s command timeout"
|
|
7090
7153
|
).option("--no-browser", "do not try to open a browser \u2014 just print the link").option("--print-url", "print the approval link even when a browser opened").addHelpText(
|
|
7091
7154
|
"after",
|
|
7092
|
-
"\nAgents: run `login --emit-only`, then SHOW the code in your reply. The\napproval page asks the user to
|
|
7155
|
+
"\nAgents: run `login --emit-only`, then SHOW the code in your reply. The\napproval page asks the user to type it, and they cannot if you never\nshowed it. Do not ask the user to run a login command themselves."
|
|
7093
7156
|
).action(async (opts) => {
|
|
7094
7157
|
const base2 = apiUrl();
|
|
7095
7158
|
const already = await currentCredentials(base2);
|
|
@@ -7140,7 +7203,11 @@ async function startPairing(base2) {
|
|
|
7140
7203
|
const res = await apiRequest({
|
|
7141
7204
|
method: "POST",
|
|
7142
7205
|
path: "/v1/auth/device/code",
|
|
7143
|
-
baseUrl: base2
|
|
7206
|
+
baseUrl: base2,
|
|
7207
|
+
// Display text for the approval screen — "requested from memgineering on
|
|
7208
|
+
// darwin" — so a person can tell their own terminal's request from a link
|
|
7209
|
+
// somebody sent them (security audit H-2). Nothing identifying.
|
|
7210
|
+
body: { platform: process.platform, client: `memgineering/${VERSION}` }
|
|
7144
7211
|
});
|
|
7145
7212
|
const { device_code, user_code, verification_url, expires_in } = res.body ?? {};
|
|
7146
7213
|
if (!device_code || !user_code || !verification_url) {
|
|
@@ -7171,10 +7238,10 @@ function emit(pending, ctx) {
|
|
|
7171
7238
|
browser_opened: ctx.opened,
|
|
7172
7239
|
api_url: ctx.base,
|
|
7173
7240
|
waiting: ctx.blocking,
|
|
7174
|
-
next: ctx.blocking ? "Waiting for approval in the browser." : "SHOW user_code to the user in your reply
|
|
7241
|
+
next: ctx.blocking ? "Waiting for approval in the browser." : "SHOW user_code to the user in your reply \u2014 the approval screen asks them to type it \u2014 then run any memgineering command; the token is claimed automatically once they approve. The approval screen also asks THEM to agree to the terms and the privacy policy and to confirm they are 14 or older; you cannot answer that for them, and an account is not created until they do."
|
|
7175
7242
|
},
|
|
7176
7243
|
human: () => {
|
|
7177
|
-
printHuman("Show this code to whoever is signing in:\n");
|
|
7244
|
+
printHuman("Show this code to whoever is signing in \u2014 the approval page asks them to type it:\n");
|
|
7178
7245
|
printHuman(` ${c.bold().yellow(oneLine(pending.user_code, WIDTH.handle))}
|
|
7179
7246
|
`);
|
|
7180
7247
|
if (ctx.opened) {
|
|
@@ -7267,9 +7334,10 @@ var init_login = __esm({
|
|
|
7267
7334
|
init_open_browser();
|
|
7268
7335
|
init_pending_login();
|
|
7269
7336
|
init_ui();
|
|
7337
|
+
init_version();
|
|
7270
7338
|
BLOCKING_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
7271
|
-
sleep = (ms) => new Promise((
|
|
7272
|
-
setTimeout(
|
|
7339
|
+
sleep = (ms) => new Promise((resolve17) => {
|
|
7340
|
+
setTimeout(resolve17, ms).unref?.();
|
|
7273
7341
|
});
|
|
7274
7342
|
}
|
|
7275
7343
|
});
|
|
@@ -7380,6 +7448,7 @@ If your notes are in one of those, that rule is why \u2014 nothing here can see
|
|
|
7380
7448
|
}
|
|
7381
7449
|
}
|
|
7382
7450
|
const repointed = await pointConfigAt(destination);
|
|
7451
|
+
await clearSetupFollowUps(local.root);
|
|
7383
7452
|
printDual({
|
|
7384
7453
|
json: {
|
|
7385
7454
|
from: local.root,
|
|
@@ -7810,13 +7879,208 @@ var init_setup_signin = __esm({
|
|
|
7810
7879
|
}
|
|
7811
7880
|
});
|
|
7812
7881
|
|
|
7882
|
+
// src/lib/next-step.ts
|
|
7883
|
+
import { mkdir as mkdir15, readFile as readFile24, writeFile as writeFile13 } from "fs/promises";
|
|
7884
|
+
import { join as join23 } from "path";
|
|
7885
|
+
function isSetUp(cfg) {
|
|
7886
|
+
return Object.keys(cfg.system.installed).length > 0;
|
|
7887
|
+
}
|
|
7888
|
+
function connectedMarker(at) {
|
|
7889
|
+
return join23(brandHome(), `setup-connected-shown-${at.replace(/[^0-9A-Za-z]/g, "")}`);
|
|
7890
|
+
}
|
|
7891
|
+
async function connectedNoticeShown(at) {
|
|
7892
|
+
return readFile24(connectedMarker(at), "utf8").then(
|
|
7893
|
+
() => true,
|
|
7894
|
+
() => false
|
|
7895
|
+
);
|
|
7896
|
+
}
|
|
7897
|
+
async function markConnectedNoticeShown(at) {
|
|
7898
|
+
try {
|
|
7899
|
+
await mkdir15(brandHome(), { recursive: true });
|
|
7900
|
+
await writeFile13(connectedMarker(at), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
7901
|
+
`);
|
|
7902
|
+
} catch {
|
|
7903
|
+
}
|
|
7904
|
+
}
|
|
7905
|
+
async function claimFullSetupNotice() {
|
|
7906
|
+
const marker = join23(brandHome(), "setup-notice-shown");
|
|
7907
|
+
try {
|
|
7908
|
+
await readFile24(marker, "utf8");
|
|
7909
|
+
return false;
|
|
7910
|
+
} catch {
|
|
7911
|
+
}
|
|
7912
|
+
try {
|
|
7913
|
+
await mkdir15(brandHome(), { recursive: true });
|
|
7914
|
+
await writeFile13(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
7915
|
+
`);
|
|
7916
|
+
} catch {
|
|
7917
|
+
}
|
|
7918
|
+
return true;
|
|
7919
|
+
}
|
|
7920
|
+
function shellQuote(value) {
|
|
7921
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7922
|
+
}
|
|
7923
|
+
function confirmUploadNotice(p) {
|
|
7924
|
+
const who = p.account_email ?? "the account `setup --web` connected";
|
|
7925
|
+
const brain = p.brain_id && UUID.test(p.brain_id) ? ` --brain ${p.brain_id}` : "";
|
|
7926
|
+
return `## Notes are waiting to be uploaded \u2014 after a check.
|
|
7927
|
+
|
|
7928
|
+
\`memgineering setup --web\` chose to upload \`${p.path}\` to a brain on **${who}**.
|
|
7929
|
+
This machine has not sent them: an upload decided on the setup screen waits
|
|
7930
|
+
until somebody here has seen which account would receive the notes.
|
|
7931
|
+
|
|
7932
|
+
If that is your user's account, this uploads them:
|
|
7933
|
+
|
|
7934
|
+
\`\`\`bash
|
|
7935
|
+
memgineering push --vault ${shellQuote(p.path)}${brain}
|
|
7936
|
+
\`\`\`
|
|
7937
|
+
|
|
7938
|
+
If it is not, \`memgineering logout\` disconnects this machine from that account
|
|
7939
|
+
and nothing is uploaded. If you are not sure which it is, ask them.`;
|
|
7940
|
+
}
|
|
7941
|
+
function setupConnectedNotice(c2) {
|
|
7942
|
+
const who = c2.email ?? "an account with no email on record";
|
|
7943
|
+
return `## This machine is signed in as **${who}**.
|
|
7944
|
+
|
|
7945
|
+
\`memgineering setup --web\` connected it to that account. If that is not your user's account,
|
|
7946
|
+
\`memgineering logout\` disconnects it, and nothing they write reaches that brain.`;
|
|
7947
|
+
}
|
|
7948
|
+
function nextStepFor(cfg, signedIn = false, seen = {}) {
|
|
7949
|
+
if (cfg.system.pendingUpload) {
|
|
7950
|
+
return { id: "confirm_upload", markdown: confirmUploadNotice(cfg.system.pendingUpload) };
|
|
7951
|
+
}
|
|
7952
|
+
if (cfg.system.setupConnected && !seen.connected) {
|
|
7953
|
+
return { id: "setup_connected", markdown: setupConnectedNotice(cfg.system.setupConnected) };
|
|
7954
|
+
}
|
|
7955
|
+
if (signedIn && cfg.brain.cloud === void 0) {
|
|
7956
|
+
return { id: "point_at_a_brain", markdown: POINT_AT_A_BRAIN };
|
|
7957
|
+
}
|
|
7958
|
+
if (isSetUp(cfg) && cfg.system.guidanceVersion !== VERSION) {
|
|
7959
|
+
return { id: "refresh_guidance", markdown: REFRESH_GUIDANCE };
|
|
7960
|
+
}
|
|
7961
|
+
if (cfg.brain.cloud !== void 0 && cfg.brain.cloud.api_url === apiUrl()) return null;
|
|
7962
|
+
if (!isSetUp(cfg)) return { id: "finish_setup", markdown: FINISH_SETUP };
|
|
7963
|
+
if (cfg.brain.brains.length === 0 && cfg.brain.cloud === void 0) {
|
|
7964
|
+
return { id: "connect_a_brain", markdown: CONNECT_A_BRAIN };
|
|
7965
|
+
}
|
|
7966
|
+
return null;
|
|
7967
|
+
}
|
|
7968
|
+
async function currentNextStep() {
|
|
7969
|
+
try {
|
|
7970
|
+
const stored = await readCredentials().catch(() => null);
|
|
7971
|
+
const cfg = await loadConfig();
|
|
7972
|
+
const connected = cfg.system.setupConnected ? await connectedNoticeShown(cfg.system.setupConnected.at) : false;
|
|
7973
|
+
return nextStepFor(cfg, stored !== null && stored.api_url === apiUrl(), { connected });
|
|
7974
|
+
} catch {
|
|
7975
|
+
return null;
|
|
7976
|
+
}
|
|
7977
|
+
}
|
|
7978
|
+
var FINISH_SETUP, SETUP_NUDGE, REFRESH_GUIDANCE, CONNECT_A_BRAIN, POINT_AT_A_BRAIN, UUID;
|
|
7979
|
+
var init_next_step = __esm({
|
|
7980
|
+
"src/lib/next-step.ts"() {
|
|
7981
|
+
"use strict";
|
|
7982
|
+
init_brand();
|
|
7983
|
+
init_api_client();
|
|
7984
|
+
init_credentials();
|
|
7985
|
+
init_config();
|
|
7986
|
+
init_version();
|
|
7987
|
+
FINISH_SETUP = `## memgineering is installed, but not set up yet.
|
|
7988
|
+
|
|
7989
|
+
Nothing is connected: there is no brain to read, and the agent tools on this
|
|
7990
|
+
machine have no memgineering guidance yet. Setup is one command:
|
|
7991
|
+
|
|
7992
|
+
\`\`\`bash
|
|
7993
|
+
memgineering setup --web
|
|
7994
|
+
\`\`\`
|
|
7995
|
+
|
|
7996
|
+
It does NOT wait for them. It returns in a few seconds \u2014 measured \u2014 and leaves
|
|
7997
|
+
a setup screen open in their browser at memgineering.com, where they choose
|
|
7998
|
+
everything: signing in, where the memory lives, which notes. Nothing is
|
|
7999
|
+
installed until they finish there, and this machine picks it up on its own.
|
|
8000
|
+
|
|
8001
|
+
If they have not already asked you to set it up, ask them first \u2014 it opens a
|
|
8002
|
+
browser window on their computer. Once you run it, tell them in their language
|
|
8003
|
+
that the setup screen is open. It prints a short code first: show it to them,
|
|
8004
|
+
because the screen asks them to compare it.
|
|
8005
|
+
|
|
8006
|
+
No browser on this machine (a server, a container, a sandbox)? Run
|
|
8007
|
+
\`memgineering setup --web --print-url\` and give them the link to open on the
|
|
8008
|
+
computer they are actually sitting at.
|
|
8009
|
+
|
|
8010
|
+
\`setup --agent\` answers every one of those questions on their behalf, and
|
|
8011
|
+
\`--no-login\` answers the biggest one \u2014 no account, so the memory cannot follow
|
|
8012
|
+
them to another machine. Use it only when they have said they would rather you
|
|
8013
|
+
decided, or when this machine cannot reach a browser and \`--print-url\` is no
|
|
8014
|
+
use to them either.
|
|
8015
|
+
|
|
8016
|
+
Setup's output then says how to connect their brain \u2014 create one, pick one, or
|
|
8017
|
+
move existing notes up.
|
|
8018
|
+
|
|
8019
|
+
The \`memgineering-setup\` skill covers what comes after.`;
|
|
8020
|
+
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.";
|
|
8021
|
+
REFRESH_GUIDANCE = `## The guidance in your agent tools is older than this memgineering.
|
|
8022
|
+
|
|
8023
|
+
The hub and skills this CLI installs into your tool's home were written by an
|
|
8024
|
+
earlier release. One command rewrites them:
|
|
8025
|
+
|
|
8026
|
+
\`\`\`bash
|
|
8027
|
+
memgineering setup
|
|
8028
|
+
\`\`\`
|
|
8029
|
+
|
|
8030
|
+
It does not change which brain this machine reads, or any note. Two things it
|
|
8031
|
+
does change, both worth knowing before it runs:
|
|
8032
|
+
|
|
8033
|
+
- A setup-owned file your user edited is overwritten; setup names it afterwards.
|
|
8034
|
+
- Run with no flags where there is no terminal (an agent's shell), it keeps the
|
|
8035
|
+
auto-update and hook choices recorded on this machine, and installs into
|
|
8036
|
+
every agent tool it finds \u2014 \`--tools\` naming every tool they use keeps it to
|
|
8037
|
+
those (a tool left out keeps its old guidance, and this notice stops
|
|
8038
|
+
appearing anyway). On a machine not signed in, it starts a sign-in and prints
|
|
8039
|
+
a code; \`--no-login\` declines that for a machine they keep local.
|
|
8040
|
+
|
|
8041
|
+
If you are not sure what they chose, ask them. If you run it,
|
|
8042
|
+
mention it to your user in one line: it is upkeep for a tool they already set
|
|
8043
|
+
up, and they should not have to discover it later.
|
|
8044
|
+
|
|
8045
|
+
Guidance files are read at the start of a session, so what changes today
|
|
8046
|
+
arrives in the next one.`;
|
|
8047
|
+
CONNECT_A_BRAIN = `## memgineering is set up, and no brain is connected.
|
|
8048
|
+
|
|
8049
|
+
A brain is a folder of markdown notes. Until one is connected there is nothing
|
|
8050
|
+
to recall and nowhere to remember. Which folder is your user's choice; the
|
|
8051
|
+
command is one you can run for them:
|
|
8052
|
+
|
|
8053
|
+
\`\`\`bash
|
|
8054
|
+
memgineering link <their-notes-folder> # they already keep notes
|
|
8055
|
+
memgineering init <path> # they do not \u2014 creates one, laid out
|
|
8056
|
+
\`\`\`
|
|
8057
|
+
|
|
8058
|
+
\`link\` reports what would be stored before anything is indexed. An existing
|
|
8059
|
+
folder needs no index and no frontmatter of its own \u2014 plain markdown is a brain.
|
|
8060
|
+
|
|
8061
|
+
Then run \`memgineering onboard\`: it prints what to ask so the brain knows whose
|
|
8062
|
+
it is. A brain whose \`01_BASE/\` is still template text answers almost nothing.
|
|
8063
|
+
|
|
8064
|
+
Read \`memgineering-setup\` for accounts and which of their tools this reaches.
|
|
8065
|
+
|
|
8066
|
+
Synced instead? \`memgineering login\`, then \`memgineering create <name>\`.`;
|
|
8067
|
+
POINT_AT_A_BRAIN = `## Signed in, and this machine reads no brain yet.
|
|
8068
|
+
|
|
8069
|
+
\`memgineering brains\` lists what is on the account.
|
|
8070
|
+
\`memgineering use --brain <name>\` points this machine at one.
|
|
8071
|
+
\`memgineering create <name>\` makes one and points here, when the account has none yet.
|
|
8072
|
+
`;
|
|
8073
|
+
UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8074
|
+
}
|
|
8075
|
+
});
|
|
8076
|
+
|
|
7813
8077
|
// src/lib/provenance.ts
|
|
7814
|
-
import { execFile } from "child_process";
|
|
8078
|
+
import { execFile, spawn as spawn2 } from "child_process";
|
|
7815
8079
|
import { realpath as realpath3 } from "fs/promises";
|
|
7816
|
-
import { dirname as dirname8, join as
|
|
8080
|
+
import { dirname as dirname8, join as join24, resolve as resolve14 } from "path";
|
|
7817
8081
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7818
8082
|
import { promisify } from "util";
|
|
7819
|
-
async function detectProvenance(selfPath) {
|
|
8083
|
+
async function detectProvenance(selfPath, opts = {}) {
|
|
7820
8084
|
const entry = selfPath ?? fileURLToPath2(import.meta.url);
|
|
7821
8085
|
const selfDir = await realpath3(dirname8(entry)).catch(() => dirname8(entry));
|
|
7822
8086
|
if (/[\\/]_npx[\\/]|[\\/]\.npm[\\/]_cacache[\\/]/.test(selfDir)) {
|
|
@@ -7828,9 +8092,11 @@ async function detectProvenance(selfPath) {
|
|
|
7828
8092
|
reason: "this is a one-off npx run, not an installed copy"
|
|
7829
8093
|
};
|
|
7830
8094
|
}
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
8095
|
+
const deadline = opts.deadlineMs;
|
|
8096
|
+
const answers = deadline === void 0 ? null : await probeAllWithin(MANAGERS.map((m) => [m.bin, m.prefixArgs]), deadline);
|
|
8097
|
+
for (const [i, manager] of MANAGERS.entries()) {
|
|
8098
|
+
const prefix = answers === null ? await prefixOf(manager.bin, manager.prefixArgs) : answers.prefixes[i];
|
|
8099
|
+
if (prefix === null || prefix === void 0) continue;
|
|
7834
8100
|
if (isInside2(selfDir, manager.moduleRoot(prefix))) {
|
|
7835
8101
|
return {
|
|
7836
8102
|
kind: manager.kind,
|
|
@@ -7841,6 +8107,15 @@ async function detectProvenance(selfPath) {
|
|
|
7841
8107
|
};
|
|
7842
8108
|
}
|
|
7843
8109
|
}
|
|
8110
|
+
if (answers?.timedOut === true) {
|
|
8111
|
+
return {
|
|
8112
|
+
kind: "undetermined",
|
|
8113
|
+
selfDir,
|
|
8114
|
+
prefix: null,
|
|
8115
|
+
command: "npm i -g memgineering@latest",
|
|
8116
|
+
reason: "the package managers did not answer in time to tell which one installed this copy"
|
|
8117
|
+
};
|
|
8118
|
+
}
|
|
7844
8119
|
const local = selfDir.includes(`${"node_modules"}`);
|
|
7845
8120
|
return {
|
|
7846
8121
|
kind: local ? "local" : "unknown",
|
|
@@ -7853,6 +8128,62 @@ async function detectProvenance(selfPath) {
|
|
|
7853
8128
|
function canSelfUpdate(p) {
|
|
7854
8129
|
return p.kind === "npm" || p.kind === "pnpm" || p.kind === "bun";
|
|
7855
8130
|
}
|
|
8131
|
+
async function probeAllWithin(probes, deadlineMs) {
|
|
8132
|
+
const controller = new AbortController();
|
|
8133
|
+
const timer = setTimeout(() => controller.abort(), deadlineMs);
|
|
8134
|
+
timer.unref();
|
|
8135
|
+
try {
|
|
8136
|
+
const prefixes = await Promise.all(
|
|
8137
|
+
probes.map(([bin, args]) => probeOnce(bin, args, controller.signal))
|
|
8138
|
+
);
|
|
8139
|
+
return { prefixes, timedOut: controller.signal.aborted };
|
|
8140
|
+
} finally {
|
|
8141
|
+
clearTimeout(timer);
|
|
8142
|
+
}
|
|
8143
|
+
}
|
|
8144
|
+
function probeOnce(bin, args, signal) {
|
|
8145
|
+
return new Promise((settle) => {
|
|
8146
|
+
let child;
|
|
8147
|
+
try {
|
|
8148
|
+
child = spawn2(bin, args, { detached: true, stdio: ["ignore", "pipe", "ignore"] });
|
|
8149
|
+
} catch {
|
|
8150
|
+
settle(null);
|
|
8151
|
+
return;
|
|
8152
|
+
}
|
|
8153
|
+
let out = "";
|
|
8154
|
+
let done = false;
|
|
8155
|
+
const finish = (value) => {
|
|
8156
|
+
if (done) return;
|
|
8157
|
+
done = true;
|
|
8158
|
+
signal.removeEventListener("abort", stop);
|
|
8159
|
+
settle(value);
|
|
8160
|
+
};
|
|
8161
|
+
function stop() {
|
|
8162
|
+
try {
|
|
8163
|
+
if (process.platform === "win32" || child.pid === void 0) child.kill("SIGKILL");
|
|
8164
|
+
else process.kill(-child.pid, "SIGKILL");
|
|
8165
|
+
} catch {
|
|
8166
|
+
}
|
|
8167
|
+
child.stdout?.destroy();
|
|
8168
|
+
child.unref();
|
|
8169
|
+
finish(null);
|
|
8170
|
+
}
|
|
8171
|
+
child.stdout?.on("data", (chunk) => {
|
|
8172
|
+
out += chunk.toString("utf8");
|
|
8173
|
+
});
|
|
8174
|
+
child.on("error", () => finish(null));
|
|
8175
|
+
child.on("close", (code) => {
|
|
8176
|
+
const value = out.trim();
|
|
8177
|
+
if (code !== 0 || value === "") {
|
|
8178
|
+
finish(null);
|
|
8179
|
+
return;
|
|
8180
|
+
}
|
|
8181
|
+
void realpath3(value).catch(() => value).then(finish);
|
|
8182
|
+
});
|
|
8183
|
+
if (signal.aborted) stop();
|
|
8184
|
+
else signal.addEventListener("abort", stop, { once: true });
|
|
8185
|
+
});
|
|
8186
|
+
}
|
|
7856
8187
|
async function prefixOf(bin, args) {
|
|
7857
8188
|
try {
|
|
7858
8189
|
const { stdout } = await run(bin, args, { timeout: 5e3 });
|
|
@@ -7874,7 +8205,7 @@ async function unreachableByName(selfPath) {
|
|
|
7874
8205
|
const p = await detectProvenance(entry);
|
|
7875
8206
|
return p.kind === "local" || p.kind === "npx" ? p.reason : null;
|
|
7876
8207
|
}
|
|
7877
|
-
var run, MANAGERS;
|
|
8208
|
+
var run, MANAGERS, BACKGROUND_PROBE_DEADLINE_MS;
|
|
7878
8209
|
var init_provenance = __esm({
|
|
7879
8210
|
"src/lib/provenance.ts"() {
|
|
7880
8211
|
"use strict";
|
|
@@ -7884,7 +8215,7 @@ var init_provenance = __esm({
|
|
|
7884
8215
|
kind: "npm",
|
|
7885
8216
|
bin: "npm",
|
|
7886
8217
|
prefixArgs: ["prefix", "-g"],
|
|
7887
|
-
moduleRoot: (prefix) => process.platform === "win32" ?
|
|
8218
|
+
moduleRoot: (prefix) => process.platform === "win32" ? join24(prefix, "node_modules") : join24(prefix, "lib", "node_modules"),
|
|
7888
8219
|
install: "npm i -g memgineering@latest"
|
|
7889
8220
|
},
|
|
7890
8221
|
{
|
|
@@ -7903,6 +8234,7 @@ var init_provenance = __esm({
|
|
|
7903
8234
|
install: "bun add -g memgineering"
|
|
7904
8235
|
}
|
|
7905
8236
|
];
|
|
8237
|
+
BACKGROUND_PROBE_DEADLINE_MS = 1500;
|
|
7906
8238
|
}
|
|
7907
8239
|
});
|
|
7908
8240
|
|
|
@@ -7911,28 +8243,36 @@ var setup_cloud_exports = {};
|
|
|
7911
8243
|
__export(setup_cloud_exports, {
|
|
7912
8244
|
SETUP_CHILD_ENV: () => SETUP_CHILD_ENV,
|
|
7913
8245
|
applyAnswers: () => applyAnswers,
|
|
8246
|
+
insideFolder: () => insideFolder,
|
|
8247
|
+
newBrainRoot: () => newBrainRoot,
|
|
7914
8248
|
readChildToken: () => readChildToken,
|
|
7915
8249
|
runSetupCloud: () => runSetupCloud,
|
|
7916
8250
|
runSetupCloudChild: () => runSetupCloudChild,
|
|
7917
8251
|
waitAndApply: () => waitAndApply
|
|
7918
8252
|
});
|
|
7919
|
-
import { spawn as
|
|
8253
|
+
import { spawn as spawn3 } from "child_process";
|
|
8254
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
7920
8255
|
import { readdir as readdir7, stat as stat6 } from "fs/promises";
|
|
7921
8256
|
import { homedir as homedir5 } from "os";
|
|
7922
|
-
import { join as
|
|
7923
|
-
async function report2(setupToken, state, detail) {
|
|
8257
|
+
import { basename as basename3, dirname as dirname9, join as join25, resolve as resolve15, sep as sep4 } from "path";
|
|
8258
|
+
async function report2(setupToken, state, detail, code) {
|
|
7924
8259
|
try {
|
|
7925
8260
|
await apiRequest({
|
|
7926
8261
|
method: "POST",
|
|
7927
8262
|
path: "/v1/setup/session/report",
|
|
7928
|
-
body: {
|
|
8263
|
+
body: {
|
|
8264
|
+
setup_token: setupToken,
|
|
8265
|
+
state,
|
|
8266
|
+
...detail === void 0 ? {} : { detail },
|
|
8267
|
+
...code === void 0 ? {} : { code }
|
|
8268
|
+
}
|
|
7929
8269
|
});
|
|
7930
8270
|
} catch {
|
|
7931
8271
|
}
|
|
7932
8272
|
}
|
|
7933
8273
|
async function folderCandidates() {
|
|
7934
8274
|
const home = homedir5();
|
|
7935
|
-
const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) =>
|
|
8275
|
+
const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) => join25(home, d));
|
|
7936
8276
|
const found = [];
|
|
7937
8277
|
const countNotes = async (dir) => {
|
|
7938
8278
|
let count = 0;
|
|
@@ -7941,7 +8281,7 @@ async function folderCandidates() {
|
|
|
7941
8281
|
if (entry.name.startsWith(".")) continue;
|
|
7942
8282
|
if (entry.isFile() && /\.(md|markdown)$/i.test(entry.name)) count += 1;
|
|
7943
8283
|
else if (entry.isDirectory()) {
|
|
7944
|
-
const inner = await readdir7(
|
|
8284
|
+
const inner = await readdir7(join25(dir, entry.name)).catch(() => []);
|
|
7945
8285
|
count += inner.filter((n) => /\.(md|markdown)$/i.test(n)).length;
|
|
7946
8286
|
}
|
|
7947
8287
|
if (count > 999) break;
|
|
@@ -7963,7 +8303,7 @@ function toolsFromAnswers(answers, detected) {
|
|
|
7963
8303
|
const picked = TOOLS.filter((t) => wanted.has(t.id));
|
|
7964
8304
|
return picked.length > 0 ? picked : [...detected];
|
|
7965
8305
|
}
|
|
7966
|
-
async function applyAnswers(setupToken, claim, detected) {
|
|
8306
|
+
async function applyAnswers(setupToken, claim, detected, guards = { candidates: [], confirmUpload: null }) {
|
|
7967
8307
|
const answers = claim.answers;
|
|
7968
8308
|
if (!answers) {
|
|
7969
8309
|
throw memgError(
|
|
@@ -7972,29 +8312,53 @@ async function applyAnswers(setupToken, claim, detected) {
|
|
|
7972
8312
|
"Nothing was installed. Run `memgineering setup --web` again; if it repeats, the brain server is answering in a shape this version does not understand."
|
|
7973
8313
|
);
|
|
7974
8314
|
}
|
|
8315
|
+
const localRoot = checkLocalPath(answers, guards.candidates);
|
|
8316
|
+
let account = null;
|
|
7975
8317
|
if (typeof claim.token === "string" && claim.token !== "") {
|
|
8318
|
+
account = claim.account ?? await whoIs(claim.token);
|
|
8319
|
+
const before = await readCredentials();
|
|
8320
|
+
const firstSignIn = before === null || before.api_url !== apiUrl();
|
|
8321
|
+
if (!firstSignIn && (account === null || before.account_id !== account.id)) {
|
|
8322
|
+
throw memgError(
|
|
8323
|
+
"conflict",
|
|
8324
|
+
"this machine is signed in to a different account, so the setup changed nothing",
|
|
8325
|
+
`Signed in here: ${before.display_name ?? before.account_id}. The setup screen was confirmed by: ${account?.email ?? account?.display_name ?? "an unknown account"}. If that second account is yours and you want to switch, run \`memgineering logout\` and then \`memgineering setup --web\` again; if you did not confirm it, someone else did \u2014 nothing was changed.`
|
|
8326
|
+
);
|
|
8327
|
+
}
|
|
7976
8328
|
const registry = registryFromClaim(claim.registry);
|
|
7977
|
-
const me = await apiRequest({
|
|
7978
|
-
method: "GET",
|
|
7979
|
-
path: "/v1/me",
|
|
7980
|
-
token: claim.token
|
|
7981
|
-
}).catch(() => null);
|
|
7982
8329
|
const credentials = {
|
|
7983
8330
|
api_url: apiUrl(),
|
|
7984
8331
|
token: claim.token,
|
|
7985
|
-
account_id:
|
|
7986
|
-
display_name:
|
|
8332
|
+
account_id: account?.id ?? "",
|
|
8333
|
+
display_name: account?.display_name ?? null,
|
|
7987
8334
|
signed_in_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7988
8335
|
...registry !== void 0 ? { registry } : {}
|
|
7989
8336
|
};
|
|
7990
8337
|
await saveCredentials(credentials);
|
|
8338
|
+
if (firstSignIn && account !== null) {
|
|
8339
|
+
const cfg = await loadConfig();
|
|
8340
|
+
await saveConfig({
|
|
8341
|
+
...cfg,
|
|
8342
|
+
system: {
|
|
8343
|
+
...cfg.system,
|
|
8344
|
+
setupConnected: {
|
|
8345
|
+
account_id: account.id,
|
|
8346
|
+
email: account.email,
|
|
8347
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8348
|
+
}
|
|
8349
|
+
}
|
|
8350
|
+
});
|
|
8351
|
+
}
|
|
7991
8352
|
}
|
|
7992
|
-
await report2(setupToken, "applying", "\uC124\uC815\uC744 \uC4F0\uB294 \uC911\uC774\uC5D0\uC694.");
|
|
8353
|
+
await report2(setupToken, "applying", "\uC124\uC815\uC744 \uC4F0\uB294 \uC911\uC774\uC5D0\uC694.", "writing_config");
|
|
7993
8354
|
const choice = {
|
|
7994
8355
|
tools: toolsFromAnswers(answers, detected),
|
|
7995
8356
|
autoUpdate: answers.auto_update ? "on" : "off",
|
|
7996
8357
|
errorTelemetry: answers.error_telemetry === null ? "undecided" : answers.error_telemetry ? "on" : "off",
|
|
7997
|
-
hook: answers.hook
|
|
8358
|
+
hook: answers.hook,
|
|
8359
|
+
// The screen always shows the hook switch, so this is the person's answer,
|
|
8360
|
+
// and a later flag-less refresh keeps it only if it is recorded as one.
|
|
8361
|
+
hookAnswer: answers.hook ? "on" : "off"
|
|
7998
8362
|
};
|
|
7999
8363
|
await applySetup(choice, false);
|
|
8000
8364
|
const parts = [];
|
|
@@ -8005,7 +8369,7 @@ async function applyAnswers(setupToken, claim, detected) {
|
|
|
8005
8369
|
await recordLink(target.adapter, DEFAULT_OBSERVATIONS_DIR);
|
|
8006
8370
|
parts.push(`linked ${answers.local_path}`);
|
|
8007
8371
|
} else {
|
|
8008
|
-
const made = await createBrain(answers.local_path, "default");
|
|
8372
|
+
const made = await createBrain(localRoot ?? answers.local_path, "default");
|
|
8009
8373
|
parts.push(`created ${made.root}`);
|
|
8010
8374
|
}
|
|
8011
8375
|
}
|
|
@@ -8015,16 +8379,82 @@ async function applyAnswers(setupToken, claim, detected) {
|
|
|
8015
8379
|
parts.push(`pointed at ${claim.brain.name}`);
|
|
8016
8380
|
}
|
|
8017
8381
|
if (answers.location === "cloud" && answers.upload_path) {
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8382
|
+
const path = answers.upload_path;
|
|
8383
|
+
if (!guards.candidates.includes(path)) {
|
|
8384
|
+
parts.push(`not uploaded: ${path} was not one of the folders this machine offered`);
|
|
8385
|
+
} else {
|
|
8386
|
+
const email = account?.email ?? null;
|
|
8387
|
+
const yes = guards.confirmUpload ? await guards.confirmUpload({ path, email }) : null;
|
|
8388
|
+
if (yes === true) {
|
|
8389
|
+
await report2(setupToken, "applying", "\uB178\uD2B8\uB97C \uC62C\uB9AC\uB294 \uC911\uC774\uC5D0\uC694.", "uploading");
|
|
8390
|
+
const { pushCommand: pushCommand2 } = await Promise.resolve().then(() => (init_push(), push_exports));
|
|
8391
|
+
await pushCommand2().parseAsync(
|
|
8392
|
+
["--vault", path, ...claim.brain?.id ? ["--brain", claim.brain.id] : []],
|
|
8393
|
+
{ from: "user" }
|
|
8394
|
+
);
|
|
8395
|
+
parts.push(`uploaded ${path}`);
|
|
8396
|
+
} else if (yes === null && account !== null) {
|
|
8397
|
+
const cfg = await loadConfig();
|
|
8398
|
+
await saveConfig({
|
|
8399
|
+
...cfg,
|
|
8400
|
+
system: {
|
|
8401
|
+
...cfg.system,
|
|
8402
|
+
pendingUpload: {
|
|
8403
|
+
path,
|
|
8404
|
+
brain: claim.brain?.name ?? null,
|
|
8405
|
+
brain_id: claim.brain?.id ?? null,
|
|
8406
|
+
account_id: account.id,
|
|
8407
|
+
account_email: email,
|
|
8408
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8409
|
+
}
|
|
8410
|
+
}
|
|
8411
|
+
});
|
|
8412
|
+
await report2(
|
|
8413
|
+
setupToken,
|
|
8414
|
+
"applying",
|
|
8415
|
+
"\uB178\uD2B8 \uC62C\uB9AC\uAE30\uB294 \uC774 \uCEF4\uD4E8\uD130\uC5D0\uC11C \uD655\uC778\uD55C \uB4A4\uC5D0 \uC2DC\uC791\uB3FC\uC694.",
|
|
8416
|
+
"upload_waits_for_check"
|
|
8417
|
+
);
|
|
8418
|
+
parts.push(`upload of ${path} waits for a check on this machine`);
|
|
8419
|
+
} else {
|
|
8420
|
+
parts.push(`not uploaded: ${path}`);
|
|
8421
|
+
}
|
|
8422
|
+
}
|
|
8025
8423
|
}
|
|
8424
|
+
if (account?.email) parts.push(`signed in as ${account.email}`);
|
|
8026
8425
|
return { summary: parts.join(" \xB7 ") };
|
|
8027
8426
|
}
|
|
8427
|
+
async function whoIs(token) {
|
|
8428
|
+
const me = await apiRequest({ method: "GET", path: "/v1/me", token }).catch(() => null);
|
|
8429
|
+
const a = me?.body?.account;
|
|
8430
|
+
return a?.id ? { id: a.id, email: a.email ?? null, display_name: a.display_name ?? null } : null;
|
|
8431
|
+
}
|
|
8432
|
+
function checkLocalPath(answers, candidates) {
|
|
8433
|
+
if (answers.location !== "local" || !answers.local_path) return null;
|
|
8434
|
+
const path = answers.local_path;
|
|
8435
|
+
if (answers.local_mode === "link") {
|
|
8436
|
+
if (!candidates.includes(path)) {
|
|
8437
|
+
throw memgError(
|
|
8438
|
+
"invalid_input",
|
|
8439
|
+
"the folder the setup screen chose to link was not one this machine offered",
|
|
8440
|
+
"Nothing was changed. To use an existing folder, run `memgineering link <folder>` here."
|
|
8441
|
+
);
|
|
8442
|
+
}
|
|
8443
|
+
return null;
|
|
8444
|
+
}
|
|
8445
|
+
return newBrainRoot(path);
|
|
8446
|
+
}
|
|
8447
|
+
function newBrainRoot(path, home = homedir5()) {
|
|
8448
|
+
const root = path === "~" ? home : path.startsWith("~/") ? resolve15(home, path.slice(2)) : resolve15(home, path);
|
|
8449
|
+
if (!insideFolder(root, home)) {
|
|
8450
|
+
throw memgError(
|
|
8451
|
+
"invalid_input",
|
|
8452
|
+
"the setup screen chose a place outside the home folder for the new brain",
|
|
8453
|
+
`Nothing was changed. Run \`memgineering init <folder>\` here to make a brain somewhere under ${home}.`
|
|
8454
|
+
);
|
|
8455
|
+
}
|
|
8456
|
+
return root;
|
|
8457
|
+
}
|
|
8028
8458
|
async function pollOnce(setupToken) {
|
|
8029
8459
|
const res = await apiRequest({
|
|
8030
8460
|
method: "POST",
|
|
@@ -8036,6 +8466,7 @@ async function pollOnce(setupToken) {
|
|
|
8036
8466
|
}
|
|
8037
8467
|
async function waitAndApply(setupToken, opts) {
|
|
8038
8468
|
let sentFolders = false;
|
|
8469
|
+
let offered = [];
|
|
8039
8470
|
for (; ; ) {
|
|
8040
8471
|
if (Date.now() > opts.deadline) return "expired";
|
|
8041
8472
|
let answer = null;
|
|
@@ -8049,12 +8480,16 @@ async function waitAndApply(setupToken, opts) {
|
|
|
8049
8480
|
if (answer) {
|
|
8050
8481
|
await report2(setupToken, "applying");
|
|
8051
8482
|
try {
|
|
8052
|
-
const done = await applyAnswers(setupToken, answer, opts.detected
|
|
8053
|
-
|
|
8483
|
+
const done = await applyAnswers(setupToken, answer, opts.detected, {
|
|
8484
|
+
candidates: offered,
|
|
8485
|
+
confirmUpload: opts.confirmUpload ?? null
|
|
8486
|
+
});
|
|
8487
|
+
await report2(setupToken, "done", "\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694.", "done");
|
|
8054
8488
|
return done;
|
|
8055
8489
|
} catch (err) {
|
|
8056
8490
|
const message = err instanceof Error ? err.message : String(err);
|
|
8057
|
-
|
|
8491
|
+
const code = err.code === "conflict" ? "different_account" : "failed";
|
|
8492
|
+
await report2(setupToken, "failed", message.slice(0, 400), code);
|
|
8058
8493
|
throw err;
|
|
8059
8494
|
}
|
|
8060
8495
|
}
|
|
@@ -8066,8 +8501,9 @@ async function waitAndApply(setupToken, opts) {
|
|
|
8066
8501
|
}).catch(() => null);
|
|
8067
8502
|
if (state?.body?.folders_requested === true) {
|
|
8068
8503
|
sentFolders = true;
|
|
8069
|
-
opts.onProgress?.("
|
|
8504
|
+
opts.onProgress?.("Looking for note folders on this machine\u2026");
|
|
8070
8505
|
const candidates = await folderCandidates();
|
|
8506
|
+
offered = candidates.map((c2) => c2.path);
|
|
8071
8507
|
await apiRequest({
|
|
8072
8508
|
method: "POST",
|
|
8073
8509
|
path: "/v1/setup/session/folders",
|
|
@@ -8076,7 +8512,7 @@ async function waitAndApply(setupToken, opts) {
|
|
|
8076
8512
|
});
|
|
8077
8513
|
}
|
|
8078
8514
|
}
|
|
8079
|
-
await new Promise((
|
|
8515
|
+
await new Promise((resolve17) => setTimeout(resolve17, opts.intervalMs));
|
|
8080
8516
|
}
|
|
8081
8517
|
}
|
|
8082
8518
|
async function startSession(detected) {
|
|
@@ -8096,7 +8532,7 @@ async function startSession(detected) {
|
|
|
8096
8532
|
return body;
|
|
8097
8533
|
}
|
|
8098
8534
|
function detachChild(setupToken) {
|
|
8099
|
-
const child =
|
|
8535
|
+
const child = spawn3(process.execPath, [process.argv[1] ?? "", "setup", "--web"], {
|
|
8100
8536
|
detached: true,
|
|
8101
8537
|
stdio: ["pipe", "ignore", "ignore"],
|
|
8102
8538
|
env: { ...process.env, [SETUP_CHILD_ENV]: "1" }
|
|
@@ -8115,26 +8551,30 @@ async function runSetupCloud(opts) {
|
|
|
8115
8551
|
const session = await startSession(opts.detected);
|
|
8116
8552
|
const intervalMs = Math.max(1, session.interval ?? 5) * 1e3;
|
|
8117
8553
|
printHuman(`
|
|
8118
|
-
|
|
8554
|
+
Setup code: ${c.bold(session.user_code)}
|
|
8119
8555
|
`);
|
|
8120
8556
|
const opened = opts.printUrl ? false : await openBrowser(session.setup_url);
|
|
8121
8557
|
const foreground = opts.wait || process.stdout.isTTY === true && process.stderr.isTTY === true;
|
|
8122
8558
|
printDual({
|
|
8123
8559
|
json: {
|
|
8124
8560
|
user_code: session.user_code,
|
|
8125
|
-
|
|
8561
|
+
// The link drives the setup screen — it carries the browser key (H-3).
|
|
8562
|
+
// Once a browser has it, nobody else needs it, so it is not printed.
|
|
8563
|
+
...opened ? {
|
|
8564
|
+
hint: "The setup screen was opened in the browser. If none appeared, run `memgineering setup --web --print-url` and open the printed link yourself."
|
|
8565
|
+
} : { setup_url: session.setup_url },
|
|
8126
8566
|
opened,
|
|
8127
8567
|
waiting: foreground
|
|
8128
8568
|
},
|
|
8129
8569
|
human: () => {
|
|
8130
|
-
if (opened) printHuman("
|
|
8570
|
+
if (opened) printHuman("Opened the setup screen in the browser.");
|
|
8131
8571
|
else {
|
|
8132
|
-
printHuman("
|
|
8572
|
+
printHuman("Open this link in a browser to finish the setup:\n");
|
|
8133
8573
|
printHuman(` ${session.setup_url}`);
|
|
8134
8574
|
}
|
|
8135
8575
|
printHuman(
|
|
8136
8576
|
c.gray(
|
|
8137
|
-
foreground ? "\n
|
|
8577
|
+
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."
|
|
8138
8578
|
)
|
|
8139
8579
|
);
|
|
8140
8580
|
}
|
|
@@ -8143,14 +8583,16 @@ async function runSetupCloud(opts) {
|
|
|
8143
8583
|
detachChild(session.setup_token);
|
|
8144
8584
|
return;
|
|
8145
8585
|
}
|
|
8586
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true && !isJsonMode();
|
|
8146
8587
|
const outcome = await waitAndApply(session.setup_token, {
|
|
8147
8588
|
intervalMs,
|
|
8148
8589
|
deadline: Date.now() + CHILD_LIFETIME_MS,
|
|
8149
8590
|
detected: opts.detected,
|
|
8150
|
-
onProgress: (line) => printHuman(c.gray(` ${line}`))
|
|
8591
|
+
onProgress: (line) => printHuman(c.gray(` ${line}`)),
|
|
8592
|
+
confirmUpload: interactive ? askBeforeUpload : null
|
|
8151
8593
|
});
|
|
8152
8594
|
if (outcome === "expired") {
|
|
8153
|
-
const line = "
|
|
8595
|
+
const line = "The setup screen expired and nothing was changed. `memgineering setup --web` opens a new one.";
|
|
8154
8596
|
if (isJsonMode()) notice(JSON.stringify({ setup_web: "expired" }));
|
|
8155
8597
|
else printHuman(c.gray(`
|
|
8156
8598
|
${line}`));
|
|
@@ -8158,14 +8600,49 @@ ${line}`));
|
|
|
8158
8600
|
}
|
|
8159
8601
|
if (isJsonMode()) notice(JSON.stringify({ setup_web: "applied" }));
|
|
8160
8602
|
else {
|
|
8161
|
-
printHuman(c.green("\
|
|
8603
|
+
printHuman(c.green("\nSetup finished."));
|
|
8162
8604
|
printHuman(
|
|
8163
8605
|
c.gray(
|
|
8164
|
-
"
|
|
8606
|
+
" recall and remember work now.\n The newly installed guidance is read from the next conversation on."
|
|
8165
8607
|
)
|
|
8166
8608
|
);
|
|
8167
8609
|
}
|
|
8168
8610
|
}
|
|
8611
|
+
function insideFolder(target, folder) {
|
|
8612
|
+
const real = (p) => {
|
|
8613
|
+
try {
|
|
8614
|
+
return realpathSync2(p);
|
|
8615
|
+
} catch {
|
|
8616
|
+
return p;
|
|
8617
|
+
}
|
|
8618
|
+
};
|
|
8619
|
+
const root = real(resolve15(folder));
|
|
8620
|
+
let existing = resolve15(target);
|
|
8621
|
+
const rest = [];
|
|
8622
|
+
for (; ; ) {
|
|
8623
|
+
try {
|
|
8624
|
+
realpathSync2(existing);
|
|
8625
|
+
break;
|
|
8626
|
+
} catch {
|
|
8627
|
+
const parent = dirname9(existing);
|
|
8628
|
+
if (parent === existing) break;
|
|
8629
|
+
rest.unshift(basename3(existing));
|
|
8630
|
+
existing = parent;
|
|
8631
|
+
}
|
|
8632
|
+
}
|
|
8633
|
+
const landed = resolve15(real(existing), ...rest);
|
|
8634
|
+
return landed === root || landed.startsWith(root + sep4);
|
|
8635
|
+
}
|
|
8636
|
+
async function askBeforeUpload(q) {
|
|
8637
|
+
const { default: prompts6 } = await import("prompts");
|
|
8638
|
+
const { yes } = await prompts6({
|
|
8639
|
+
type: "confirm",
|
|
8640
|
+
name: "yes",
|
|
8641
|
+
message: `Upload the notes in ${q.path} to the brain on ${q.email ?? "this account"}?`,
|
|
8642
|
+
initial: false
|
|
8643
|
+
});
|
|
8644
|
+
return yes === true;
|
|
8645
|
+
}
|
|
8169
8646
|
async function runSetupCloudChild(detected) {
|
|
8170
8647
|
const setupToken = await readChildToken();
|
|
8171
8648
|
if (setupToken === "") return;
|
|
@@ -8182,6 +8659,7 @@ var init_setup_cloud = __esm({
|
|
|
8182
8659
|
"use strict";
|
|
8183
8660
|
init_api_client();
|
|
8184
8661
|
init_errors();
|
|
8662
|
+
init_config();
|
|
8185
8663
|
init_credentials();
|
|
8186
8664
|
init_open_browser();
|
|
8187
8665
|
init_ui();
|
|
@@ -8195,8 +8673,8 @@ var init_setup_cloud = __esm({
|
|
|
8195
8673
|
|
|
8196
8674
|
// src/commands/setup.ts
|
|
8197
8675
|
import { createHash as createHash9 } from "crypto";
|
|
8198
|
-
import { access as access2, mkdir as
|
|
8199
|
-
import { dirname as
|
|
8676
|
+
import { access as access2, mkdir as mkdir16, readFile as readFile25 } from "fs/promises";
|
|
8677
|
+
import { dirname as dirname10, join as join26 } from "path";
|
|
8200
8678
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8201
8679
|
import { Command as Command40 } from "commander";
|
|
8202
8680
|
import prompts5 from "prompts";
|
|
@@ -8221,10 +8699,11 @@ function setupCommand() {
|
|
|
8221
8699
|
"after",
|
|
8222
8700
|
[
|
|
8223
8701
|
"",
|
|
8224
|
-
"Chat apps with no shell
|
|
8225
|
-
`
|
|
8226
|
-
"
|
|
8227
|
-
"
|
|
8702
|
+
"Chat apps with no shell need no setup. They read and write the brain on your",
|
|
8703
|
+
`account (not a folder on this machine) at ${HOSTED_CONNECTOR_URL}:`,
|
|
8704
|
+
" ChatGPT: Settings \u2192 Developer mode \u2192 add an MCP server with that URL, sign in.",
|
|
8705
|
+
" Claude.ai, Claude Desktop, Cowork: Customize \u2192 Connectors \u2192 Add custom",
|
|
8706
|
+
" connector with that URL, sign in."
|
|
8228
8707
|
].join("\n")
|
|
8229
8708
|
).action(async (opts) => {
|
|
8230
8709
|
const detected = await detectTools();
|
|
@@ -8259,16 +8738,18 @@ function setupCommand() {
|
|
|
8259
8738
|
dryRun: opts.dryRun === true
|
|
8260
8739
|
});
|
|
8261
8740
|
const mode = await resolveMode(opts);
|
|
8262
|
-
const
|
|
8741
|
+
const recorded = mode.kind === "human" ? {} : await recordedChoices(detected);
|
|
8742
|
+
const choice = mode.kind === "human" ? await askHuman(detected, opts) : fromFlags(detected, opts, recorded);
|
|
8263
8743
|
const defaultsTaken = !mode.chosen ? [
|
|
8264
|
-
|
|
8744
|
+
// Only what nobody chose, here or on an earlier run.
|
|
8745
|
+
...opts.autoUpdate === void 0 && recorded.autoUpdate === void 0 ? ["auto-update: on"] : [],
|
|
8265
8746
|
// Every callback this flag arms, named — on Claude Code and Codex
|
|
8266
8747
|
// that is two (note summaries at session start, the user's own
|
|
8267
8748
|
// rules before an edit), and saying only the first would be
|
|
8268
8749
|
// reporting half of what was decided on their behalf. On Grok it
|
|
8269
8750
|
// is one, at the end of a turn, and naming the other two there
|
|
8270
8751
|
// reported callbacks that were never installed.
|
|
8271
|
-
...opts.hook === void 0 && choice?.hook ? hooksFor(choice.tools).map((h) => `${h.what}: on`) : []
|
|
8752
|
+
...opts.hook === void 0 && recorded.hook === void 0 && choice?.hook ? hooksFor(choice.tools).map((h) => `${h.what}: on`) : []
|
|
8272
8753
|
] : [];
|
|
8273
8754
|
if (choice === null) {
|
|
8274
8755
|
printHuman(c.yellow("Cancelled \u2014 nothing was changed."));
|
|
@@ -8534,6 +9015,7 @@ async function applySetup(choice, dryRun) {
|
|
|
8534
9015
|
system: {
|
|
8535
9016
|
...cfg.system,
|
|
8536
9017
|
autoUpdate: choice.autoUpdate,
|
|
9018
|
+
...choice.hookAnswer === void 0 ? {} : { hook: choice.hookAnswer },
|
|
8537
9019
|
installed,
|
|
8538
9020
|
// Stamped whether or not anything was rewritten. A run that finds every
|
|
8539
9021
|
// file already current is exactly the run that must clear the notice —
|
|
@@ -8559,7 +9041,7 @@ async function applySetup(choice, dryRun) {
|
|
|
8559
9041
|
}
|
|
8560
9042
|
async function write2() {
|
|
8561
9043
|
for (const tool of choice.tools) {
|
|
8562
|
-
const hubPath =
|
|
9044
|
+
const hubPath = join26(agentHome(), tool.hubFile);
|
|
8563
9045
|
const written = await injectBlock(
|
|
8564
9046
|
hubPath,
|
|
8565
9047
|
hub + tailFor(tool, choice.hook),
|
|
@@ -8574,8 +9056,8 @@ async function applySetup(choice, dryRun) {
|
|
|
8574
9056
|
});
|
|
8575
9057
|
if (!dryRun && written.after !== null) mark(hubPath, written.after);
|
|
8576
9058
|
for (const skill of BRAND.subSkills) {
|
|
8577
|
-
const content = await loadAsset(
|
|
8578
|
-
const path =
|
|
9059
|
+
const content = await loadAsset(join26(skill, "SKILL.md"));
|
|
9060
|
+
const path = join26(agentHome(), tool.skillsDir, skill, "SKILL.md");
|
|
8579
9061
|
const out = await writeIfDifferent(path, content, dryRun);
|
|
8580
9062
|
results.push({
|
|
8581
9063
|
tool: tool.label,
|
|
@@ -8648,6 +9130,23 @@ function globalInstallHint(reason, hooks) {
|
|
|
8648
9130
|
return `${hooks ? "The hooks and the guide call" : "The guide tells your agent to run"} \`memgineering\` by name, and ${reason} \u2014
|
|
8649
9131
|
so nothing reaches it until a global install exists: \`npm i -g memgineering\`. Nothing needs re-running after that.`;
|
|
8650
9132
|
}
|
|
9133
|
+
async function recordedChoices(detected) {
|
|
9134
|
+
const cfg = await loadConfig();
|
|
9135
|
+
if (!isSetUp(cfg)) return {};
|
|
9136
|
+
const hook = cfg.system.hook ?? await hooksArmed(detected);
|
|
9137
|
+
return { autoUpdate: cfg.system.autoUpdate, ...hook === void 0 ? {} : { hook } };
|
|
9138
|
+
}
|
|
9139
|
+
async function hooksArmed(tools) {
|
|
9140
|
+
const capable = tools.filter((t) => t.hooks && t.hooksFile);
|
|
9141
|
+
if (capable.length === 0) return void 0;
|
|
9142
|
+
for (const tool of capable) {
|
|
9143
|
+
const raw = await readFile25(join26(agentHome(), ...(tool.hooksFile ?? "").split("/")), "utf8").catch(
|
|
9144
|
+
() => ""
|
|
9145
|
+
);
|
|
9146
|
+
if (hooksFor([tool]).some((spec) => raw.includes(spec.signature))) return "on";
|
|
9147
|
+
}
|
|
9148
|
+
return "off";
|
|
9149
|
+
}
|
|
8651
9150
|
async function resolveMode(opts) {
|
|
8652
9151
|
if (opts.agent) return { kind: "agent", chosen: true };
|
|
8653
9152
|
if (opts.human) {
|
|
@@ -8681,11 +9180,11 @@ async function detectTools() {
|
|
|
8681
9180
|
const home = agentHome();
|
|
8682
9181
|
const found = [];
|
|
8683
9182
|
for (const tool of TOOLS) {
|
|
8684
|
-
if (await exists2(
|
|
9183
|
+
if (await exists2(join26(home, tool.dir))) found.push(tool);
|
|
8685
9184
|
}
|
|
8686
9185
|
return found;
|
|
8687
9186
|
}
|
|
8688
|
-
function fromFlags(detected, opts) {
|
|
9187
|
+
function fromFlags(detected, opts, recorded = {}) {
|
|
8689
9188
|
const tools = opts.tools ? opts.tools.split(",").map((t) => t.trim().toLowerCase()).map((id) => {
|
|
8690
9189
|
const tool = detected.find((t) => t.id === id);
|
|
8691
9190
|
if (tool) return tool;
|
|
@@ -8704,14 +9203,16 @@ function fromFlags(detected, opts) {
|
|
|
8704
9203
|
`${known.label} creates ~/${known.dir} the first time it runs. If it is installed, open it once, then run setup again. ${detectedList}`
|
|
8705
9204
|
);
|
|
8706
9205
|
}) : detected;
|
|
9206
|
+
const hookAnswer = parseChoice(opts.hook, recorded.hook ?? "on", "--hook");
|
|
8707
9207
|
return {
|
|
8708
9208
|
tools,
|
|
8709
|
-
autoUpdate: parseChoice(opts.autoUpdate, "on", "--auto-update"),
|
|
9209
|
+
autoUpdate: parseChoice(opts.autoUpdate, recorded.autoUpdate ?? "on", "--auto-update"),
|
|
8710
9210
|
errorTelemetry: opts.errorTelemetry === void 0 ? "undecided" : parseChoice(opts.errorTelemetry, "off", "--error-telemetry"),
|
|
8711
9211
|
// Only Claude Code can run something at session start. For the others the
|
|
8712
9212
|
// skill tells the agent to call resurface itself, which is why asking for
|
|
8713
9213
|
// a hook there is not an error — it simply has nowhere to go.
|
|
8714
|
-
hook:
|
|
9214
|
+
hook: hookAnswer === "on" && tools.some((t) => t.hooks),
|
|
9215
|
+
hookAnswer
|
|
8715
9216
|
};
|
|
8716
9217
|
}
|
|
8717
9218
|
function parseChoice(value, fallback, flag2) {
|
|
@@ -8770,17 +9271,19 @@ async function askHuman(detected, opts) {
|
|
|
8770
9271
|
autoUpdate: answers.autoUpdate === false ? "off" : parseChoice(opts.autoUpdate, "on", "--auto-update"),
|
|
8771
9272
|
// An aborted prompt is not an answer — undecided, never a silent yes.
|
|
8772
9273
|
errorTelemetry: answers.errorTelemetry === void 0 ? "undecided" : answers.errorTelemetry ? "on" : "off",
|
|
8773
|
-
hook: answers.hook !== false
|
|
9274
|
+
hook: answers.hook !== false,
|
|
9275
|
+
// Only a question that was put is an answer to record.
|
|
9276
|
+
...answers.hook === void 0 ? {} : { hookAnswer: answers.hook ? "on" : "off" }
|
|
8774
9277
|
};
|
|
8775
9278
|
}
|
|
8776
9279
|
async function injectBlock(filePath, content, dryRun, weWroteThis) {
|
|
8777
9280
|
const block = `${BRAND.hubMarkerStart}
|
|
8778
9281
|
${content.trimEnd()}
|
|
8779
9282
|
${BRAND.hubMarkerEnd}`;
|
|
8780
|
-
const existing = await
|
|
9283
|
+
const existing = await readFile25(filePath, "utf8").catch(() => null);
|
|
8781
9284
|
if (existing === null) {
|
|
8782
9285
|
if (!dryRun) {
|
|
8783
|
-
await
|
|
9286
|
+
await mkdir16(dirname10(filePath), { recursive: true });
|
|
8784
9287
|
await writeThenRename(filePath, `${block}
|
|
8785
9288
|
`);
|
|
8786
9289
|
}
|
|
@@ -8820,11 +9323,11 @@ function looksLikeOurGuide(text) {
|
|
|
8820
9323
|
return text.includes("memgineering recall") && text.includes("memgineering remember");
|
|
8821
9324
|
}
|
|
8822
9325
|
async function writeIfDifferent(filePath, content, dryRun) {
|
|
8823
|
-
const existing = await
|
|
9326
|
+
const existing = await readFile25(filePath, "utf8").catch(() => null);
|
|
8824
9327
|
const after = fingerprint(content);
|
|
8825
9328
|
if (existing === content) return { outcome: "unchanged", before: existing, after };
|
|
8826
9329
|
if (!dryRun) {
|
|
8827
|
-
await
|
|
9330
|
+
await mkdir16(dirname10(filePath), { recursive: true });
|
|
8828
9331
|
await writeThenRename(filePath, content);
|
|
8829
9332
|
}
|
|
8830
9333
|
return { outcome: existing === null ? "injected" : "updated", before: existing, after };
|
|
@@ -8835,8 +9338,8 @@ function hooksFor(tools) {
|
|
|
8835
9338
|
);
|
|
8836
9339
|
}
|
|
8837
9340
|
async function installHook(spec, tool, dryRun) {
|
|
8838
|
-
const settingsPath =
|
|
8839
|
-
const raw = await
|
|
9341
|
+
const settingsPath = join26(agentHome(), ...(tool.hooksFile ?? "").split("/"));
|
|
9342
|
+
const raw = await readFile25(settingsPath, "utf8").catch(() => null);
|
|
8840
9343
|
let settings = {};
|
|
8841
9344
|
if (raw !== null) {
|
|
8842
9345
|
try {
|
|
@@ -8878,15 +9381,15 @@ async function installHook(spec, tool, dryRun) {
|
|
|
8878
9381
|
}
|
|
8879
9382
|
const next = { ...settings, hooks: { ...hooks, [spec.event]: existingGroups } };
|
|
8880
9383
|
if (!dryRun) {
|
|
8881
|
-
await
|
|
9384
|
+
await mkdir16(dirname10(settingsPath), { recursive: true });
|
|
8882
9385
|
await writeThenRename(settingsPath, `${JSON.stringify(next, null, 2)}
|
|
8883
9386
|
`);
|
|
8884
9387
|
}
|
|
8885
9388
|
return raw === null ? "injected" : "updated";
|
|
8886
9389
|
}
|
|
8887
9390
|
async function removeHook(spec, tool, dryRun) {
|
|
8888
|
-
const settingsPath =
|
|
8889
|
-
const raw = await
|
|
9391
|
+
const settingsPath = join26(agentHome(), ...(tool.hooksFile ?? "").split("/"));
|
|
9392
|
+
const raw = await readFile25(settingsPath, "utf8").catch(() => null);
|
|
8890
9393
|
if (raw === null) return "unchanged";
|
|
8891
9394
|
let parsed;
|
|
8892
9395
|
try {
|
|
@@ -8910,7 +9413,7 @@ async function removeHook(spec, tool, dryRun) {
|
|
|
8910
9413
|
return "removed";
|
|
8911
9414
|
}
|
|
8912
9415
|
async function loadAsset(relPath) {
|
|
8913
|
-
const raw = await
|
|
9416
|
+
const raw = await readFile25(join26(await assetRoot(), relPath), "utf8");
|
|
8914
9417
|
if (relPath === BRAND.hubFileName && raw.startsWith("---")) {
|
|
8915
9418
|
const end = raw.indexOf("\n---", 4);
|
|
8916
9419
|
if (end !== -1) return raw.slice(end + 4).trimStart();
|
|
@@ -8918,14 +9421,14 @@ async function loadAsset(relPath) {
|
|
|
8918
9421
|
return raw;
|
|
8919
9422
|
}
|
|
8920
9423
|
async function assetRoot() {
|
|
8921
|
-
const here =
|
|
8922
|
-
const bundled =
|
|
8923
|
-
if (await exists2(
|
|
9424
|
+
const here = dirname10(fileURLToPath3(import.meta.url));
|
|
9425
|
+
const bundled = join26(here, "..", "assets");
|
|
9426
|
+
if (await exists2(join26(bundled, BRAND.hubFileName))) return bundled;
|
|
8924
9427
|
let dir = here;
|
|
8925
9428
|
for (; ; ) {
|
|
8926
|
-
const candidate =
|
|
8927
|
-
if (await exists2(
|
|
8928
|
-
const parent =
|
|
9429
|
+
const candidate = join26(dir, "packages", "skills");
|
|
9430
|
+
if (await exists2(join26(candidate, BRAND.hubFileName))) return candidate;
|
|
9431
|
+
const parent = dirname10(dir);
|
|
8929
9432
|
if (parent === dir) break;
|
|
8930
9433
|
dir = parent;
|
|
8931
9434
|
}
|
|
@@ -8954,6 +9457,7 @@ var init_setup = __esm({
|
|
|
8954
9457
|
init_setup_brain();
|
|
8955
9458
|
init_setup_signin();
|
|
8956
9459
|
init_config();
|
|
9460
|
+
init_next_step();
|
|
8957
9461
|
init_provenance();
|
|
8958
9462
|
init_ui();
|
|
8959
9463
|
init_version();
|
|
@@ -9312,7 +9816,7 @@ async function reportError(kind, err) {
|
|
|
9312
9816
|
headers: { "content-type": "application/json" },
|
|
9313
9817
|
body: JSON.stringify(payload)
|
|
9314
9818
|
}),
|
|
9315
|
-
new Promise((
|
|
9819
|
+
new Promise((resolve17) => setTimeout(resolve17, REPORT_TIMEOUT_MS))
|
|
9316
9820
|
]);
|
|
9317
9821
|
} catch {
|
|
9318
9822
|
}
|
|
@@ -11003,7 +11507,7 @@ var KordisClient = class {
|
|
|
11003
11507
|
`[kordis] api: ${method} ${url.pathname}${url.search} \u2192 fetch failed, retrying in 150ms (${err.message})`
|
|
11004
11508
|
);
|
|
11005
11509
|
}
|
|
11006
|
-
await new Promise((
|
|
11510
|
+
await new Promise((resolve17) => setTimeout(resolve17, 150));
|
|
11007
11511
|
continue;
|
|
11008
11512
|
}
|
|
11009
11513
|
}
|
|
@@ -12858,12 +13362,12 @@ var BASE_FILES = [
|
|
|
12858
13362
|
];
|
|
12859
13363
|
var HUB_PATH = "00_HUB/HUB.md";
|
|
12860
13364
|
var TEMPLATE_SLOTS = 2;
|
|
12861
|
-
function capTemplates(cards, isTemplate) {
|
|
13365
|
+
function capTemplates(cards, isTemplate, inConflict = () => false) {
|
|
12862
13366
|
const kept = [];
|
|
12863
13367
|
const withheld = [];
|
|
12864
13368
|
let templates = 0;
|
|
12865
13369
|
for (const card of cards) {
|
|
12866
|
-
if (isTemplate(card) && ++templates > TEMPLATE_SLOTS) withheld.push(card);
|
|
13370
|
+
if (isTemplate(card) && !inConflict(card) && ++templates > TEMPLATE_SLOTS) withheld.push(card);
|
|
12867
13371
|
else kept.push(card);
|
|
12868
13372
|
}
|
|
12869
13373
|
return { kept, withheld };
|
|
@@ -14507,11 +15011,11 @@ function lines(text, marker) {
|
|
|
14507
15011
|
return text === "" ? [] : text.split("\n").map((l) => `${marker} ${l}`);
|
|
14508
15012
|
}
|
|
14509
15013
|
async function readInput(source) {
|
|
14510
|
-
const raw = source === "-" ? await new Promise((
|
|
15014
|
+
const raw = source === "-" ? await new Promise((resolve17, reject) => {
|
|
14511
15015
|
let buf = "";
|
|
14512
15016
|
process.stdin.setEncoding("utf8");
|
|
14513
15017
|
process.stdin.on("data", (chunk) => buf += chunk);
|
|
14514
|
-
process.stdin.on("end", () =>
|
|
15018
|
+
process.stdin.on("end", () => resolve17(buf));
|
|
14515
15019
|
process.stdin.on("error", reject);
|
|
14516
15020
|
}) : await readFile16(source, "utf8").catch(() => source);
|
|
14517
15021
|
try {
|
|
@@ -17174,8 +17678,14 @@ function groupHostedLines(rules, project) {
|
|
|
17174
17678
|
const width = ruleWidthFor(rules.length);
|
|
17175
17679
|
const line = (r) => {
|
|
17176
17680
|
const quoted = renderRuleLine(r.text ?? r.summary ?? r.title, width);
|
|
17177
|
-
|
|
17178
|
-
|
|
17681
|
+
const labels = [
|
|
17682
|
+
...r.freshness === "current" || r.freshness === void 0 ? [] : [`(${r.freshness === "expired" ? "past its end date" : "superseded"} \u2014 worth checking)`],
|
|
17683
|
+
// Whose words these are, when a chat app's (security audit M-2). The
|
|
17684
|
+
// person promoted it; the agent is owed knowing where it came from.
|
|
17685
|
+
...r.written_by === void 0 ? [] : [`(${connectorLabel(r.written_by)})`]
|
|
17686
|
+
];
|
|
17687
|
+
return labels.length === 0 ? quoted : `${quoted}
|
|
17688
|
+
${labels.join(" ")}`;
|
|
17179
17689
|
};
|
|
17180
17690
|
const globals = rules.filter((r) => r.lane === "global");
|
|
17181
17691
|
const scoped = rules.filter((r) => r.lane === "project");
|
|
@@ -17896,6 +18406,7 @@ init_login();
|
|
|
17896
18406
|
init_errors();
|
|
17897
18407
|
init_api_client();
|
|
17898
18408
|
init_credentials();
|
|
18409
|
+
init_config();
|
|
17899
18410
|
init_pending_login();
|
|
17900
18411
|
init_ui();
|
|
17901
18412
|
import { Command as Command22 } from "commander";
|
|
@@ -17907,6 +18418,7 @@ function logoutCommand() {
|
|
|
17907
18418
|
const base2 = apiUrl();
|
|
17908
18419
|
const credentials = await readCredentials();
|
|
17909
18420
|
await clearPendingLogin();
|
|
18421
|
+
await clearSetupFollowUps();
|
|
17910
18422
|
if (!credentials) {
|
|
17911
18423
|
printDual({
|
|
17912
18424
|
json: { signed_out: true, was_signed_in: false, api_url: base2 },
|
|
@@ -22008,7 +22520,8 @@ function recallCommand() {
|
|
|
22008
22520
|
})
|
|
22009
22521
|
);
|
|
22010
22522
|
isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
|
|
22011
|
-
const
|
|
22523
|
+
const sides = new Set(local.conflicts.flatMap((conflict) => conflict.between));
|
|
22524
|
+
const capped2 = capTemplates(local.memory_cards, isScaffolding, (card) => sides.has(card.id));
|
|
22012
22525
|
templatesWithheld = capped2.withheld.length;
|
|
22013
22526
|
result = { ...local, memory_cards: capped2.kept };
|
|
22014
22527
|
entries = built.entries;
|
|
@@ -22203,7 +22716,7 @@ ${unsearchedLocal.map(
|
|
|
22203
22716
|
if (templatesWithheld > 0) {
|
|
22204
22717
|
printHuman(
|
|
22205
22718
|
c.gray(
|
|
22206
|
-
` ${templatesWithheld} more untouched template${templatesWithheld === 1 ? "" : "s"} matched and ${templatesWithheld === 1 ? "is" : "are"} not shown \u2014 at most ${TEMPLATE_SLOTS} per answer.
|
|
22719
|
+
` ${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.
|
|
22207
22720
|
`
|
|
22208
22721
|
)
|
|
22209
22722
|
);
|
|
@@ -22716,7 +23229,7 @@ function rulesCommand() {
|
|
|
22716
23229
|
const lane = rule.lane === "project" ? oneLine(project2 ?? "this project", WIDTH.title) : "global";
|
|
22717
23230
|
printHuman(
|
|
22718
23231
|
c.gray(
|
|
22719
|
-
` \`open: ${oneLine(rule.handle, WIDTH.handle)}\`${rule.lane ? ` \xB7 ${lane}` : ""}`
|
|
23232
|
+
` \`open: ${oneLine(rule.handle, WIDTH.handle)}\`${rule.lane ? ` \xB7 ${lane}` : ""}${rule.written_by === void 0 ? "" : ` \xB7 ${connectorLabel(rule.written_by)}`}`
|
|
22720
23233
|
)
|
|
22721
23234
|
);
|
|
22722
23235
|
}
|
|
@@ -22735,7 +23248,7 @@ ${shownOf - rules2.length} more not shown.`));
|
|
|
22735
23248
|
printHuman(`- ${oneLine(rule.summary ?? rule.title, WIDTH.text)}`);
|
|
22736
23249
|
printHuman(
|
|
22737
23250
|
c.gray(
|
|
22738
|
-
` \`open: ${oneLine(rule.handle, WIDTH.handle)}\` \xB7 ${oneLine(rule.scope ?? "", WIDTH.title)}`
|
|
23251
|
+
` \`open: ${oneLine(rule.handle, WIDTH.handle)}\` \xB7 ${oneLine(rule.scope ?? "", WIDTH.title)}${rule.written_by === void 0 ? "" : ` \xB7 ${connectorLabel(rule.written_by)}`}`
|
|
22739
23252
|
)
|
|
22740
23253
|
);
|
|
22741
23254
|
}
|
|
@@ -22834,151 +23347,14 @@ init_setup();
|
|
|
22834
23347
|
init_brand();
|
|
22835
23348
|
init_version();
|
|
22836
23349
|
init_config();
|
|
22837
|
-
|
|
23350
|
+
init_next_step();
|
|
23351
|
+
init_provenance();
|
|
23352
|
+
import { spawn as spawn4 } from "child_process";
|
|
22838
23353
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
22839
|
-
import { mkdir as mkdir18, open, readFile as readFile27, stat as stat7, unlink as unlink4 } from "fs/promises";
|
|
23354
|
+
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";
|
|
22840
23355
|
import { join as join28 } from "path";
|
|
22841
23356
|
import { Command as Command41 } from "commander";
|
|
22842
23357
|
|
|
22843
|
-
// src/lib/next-step.ts
|
|
22844
|
-
init_brand();
|
|
22845
|
-
init_api_client();
|
|
22846
|
-
init_credentials();
|
|
22847
|
-
init_config();
|
|
22848
|
-
init_version();
|
|
22849
|
-
import { mkdir as mkdir16, readFile as readFile25, writeFile as writeFile13 } from "fs/promises";
|
|
22850
|
-
import { join as join26 } from "path";
|
|
22851
|
-
function isSetUp(cfg) {
|
|
22852
|
-
return Object.keys(cfg.system.installed).length > 0;
|
|
22853
|
-
}
|
|
22854
|
-
var FINISH_SETUP = `## memgineering is installed, but not set up yet.
|
|
22855
|
-
|
|
22856
|
-
Nothing is connected: there is no brain to read, and the agent tools on this
|
|
22857
|
-
machine have no memgineering guidance yet. Setup is one command:
|
|
22858
|
-
|
|
22859
|
-
\`\`\`bash
|
|
22860
|
-
memgineering setup --web
|
|
22861
|
-
\`\`\`
|
|
22862
|
-
|
|
22863
|
-
It does NOT wait for them. It returns in a few seconds \u2014 measured \u2014 and leaves
|
|
22864
|
-
a setup screen open in their browser at memgineering.com, where they choose
|
|
22865
|
-
everything: signing in, where the memory lives, which notes. Nothing is
|
|
22866
|
-
installed until they finish there, and this machine picks it up on its own.
|
|
22867
|
-
|
|
22868
|
-
If they have not already asked you to set it up, ask them first \u2014 it opens a
|
|
22869
|
-
browser window on their computer. Once you run it, tell them in their language
|
|
22870
|
-
that the setup screen is open. It prints a short code first: show it to them,
|
|
22871
|
-
because the screen asks them to compare it.
|
|
22872
|
-
|
|
22873
|
-
No browser on this machine (a server, a container, a sandbox)? Run
|
|
22874
|
-
\`memgineering setup --web --print-url\` and give them the link to open on the
|
|
22875
|
-
computer they are actually sitting at.
|
|
22876
|
-
|
|
22877
|
-
\`setup --agent\` answers every one of those questions on their behalf, and
|
|
22878
|
-
\`--no-login\` answers the biggest one \u2014 no account, so the memory cannot follow
|
|
22879
|
-
them to another machine. Use it only when they have said they would rather you
|
|
22880
|
-
decided, or when this machine cannot reach a browser and \`--print-url\` is no
|
|
22881
|
-
use to them either.
|
|
22882
|
-
|
|
22883
|
-
Setup's output then says how to connect their brain \u2014 create one, pick one, or
|
|
22884
|
-
move existing notes up.
|
|
22885
|
-
|
|
22886
|
-
The \`memgineering-setup\` skill covers what comes after.`;
|
|
22887
|
-
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.";
|
|
22888
|
-
async function claimFullSetupNotice() {
|
|
22889
|
-
const marker = join26(brandHome(), "setup-notice-shown");
|
|
22890
|
-
try {
|
|
22891
|
-
await readFile25(marker, "utf8");
|
|
22892
|
-
return false;
|
|
22893
|
-
} catch {
|
|
22894
|
-
}
|
|
22895
|
-
try {
|
|
22896
|
-
await mkdir16(brandHome(), { recursive: true });
|
|
22897
|
-
await writeFile13(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
22898
|
-
`);
|
|
22899
|
-
} catch {
|
|
22900
|
-
}
|
|
22901
|
-
return true;
|
|
22902
|
-
}
|
|
22903
|
-
var REFRESH_GUIDANCE = `## The guidance in your agent tools is older than this memgineering.
|
|
22904
|
-
|
|
22905
|
-
The hub and skills this CLI installs into your tool's home were written by an
|
|
22906
|
-
earlier release. One command rewrites them:
|
|
22907
|
-
|
|
22908
|
-
\`\`\`bash
|
|
22909
|
-
memgineering setup
|
|
22910
|
-
\`\`\`
|
|
22911
|
-
|
|
22912
|
-
It does not change which brain this machine reads, or any note. Two things it
|
|
22913
|
-
does change, both worth knowing before it runs:
|
|
22914
|
-
|
|
22915
|
-
- A setup-owned file your user edited is overwritten; setup names it afterwards.
|
|
22916
|
-
- Run with no flags where there is no terminal (an agent's shell), it applies
|
|
22917
|
-
setup's defaults over whatever they chose before: auto-update on, its hooks
|
|
22918
|
-
on, every agent tool it finds \u2014 and on a machine not signed in, it starts a
|
|
22919
|
-
sign-in and prints a code. To keep what they chose, pass it:
|
|
22920
|
-
\`--auto-update off\`, \`--hook off\`, \`--no-login\` for a machine they keep
|
|
22921
|
-
local, and \`--tools\` naming every tool they use (a tool left out keeps its
|
|
22922
|
-
old guidance, and this notice stops appearing anyway).
|
|
22923
|
-
|
|
22924
|
-
If you are not sure what they chose, ask them. If you run it,
|
|
22925
|
-
mention it to your user in one line: it is upkeep for a tool they already set
|
|
22926
|
-
up, and they should not have to discover it later.
|
|
22927
|
-
|
|
22928
|
-
Guidance files are read at the start of a session, so what changes today
|
|
22929
|
-
arrives in the next one.`;
|
|
22930
|
-
var CONNECT_A_BRAIN = `## memgineering is set up, and no brain is connected.
|
|
22931
|
-
|
|
22932
|
-
A brain is a folder of markdown notes. Until one is connected there is nothing
|
|
22933
|
-
to recall and nowhere to remember. Which folder is your user's choice; the
|
|
22934
|
-
command is one you can run for them:
|
|
22935
|
-
|
|
22936
|
-
\`\`\`bash
|
|
22937
|
-
memgineering link <their-notes-folder> # they already keep notes
|
|
22938
|
-
memgineering init <path> # they do not \u2014 creates one, laid out
|
|
22939
|
-
\`\`\`
|
|
22940
|
-
|
|
22941
|
-
\`link\` reports what would be stored before anything is indexed. An existing
|
|
22942
|
-
folder needs no index and no frontmatter of its own \u2014 plain markdown is a brain.
|
|
22943
|
-
|
|
22944
|
-
Then run \`memgineering onboard\`: it prints what to ask so the brain knows whose
|
|
22945
|
-
it is. A brain whose \`01_BASE/\` is still template text answers almost nothing.
|
|
22946
|
-
|
|
22947
|
-
Read \`memgineering-setup\` for accounts and which of their tools this reaches.
|
|
22948
|
-
|
|
22949
|
-
Synced instead? \`memgineering login\`, then \`memgineering create <name>\`.`;
|
|
22950
|
-
var POINT_AT_A_BRAIN = `## Signed in, and this machine reads no brain yet.
|
|
22951
|
-
|
|
22952
|
-
\`memgineering brains\` lists what is on the account.
|
|
22953
|
-
\`memgineering use --brain <name>\` points this machine at one.
|
|
22954
|
-
\`memgineering create <name>\` makes one and points here, when the account has none yet.
|
|
22955
|
-
`;
|
|
22956
|
-
function nextStepFor(cfg, signedIn = false) {
|
|
22957
|
-
if (signedIn && cfg.brain.cloud === void 0) {
|
|
22958
|
-
return { id: "point_at_a_brain", markdown: POINT_AT_A_BRAIN };
|
|
22959
|
-
}
|
|
22960
|
-
if (isSetUp(cfg) && cfg.system.guidanceVersion !== VERSION) {
|
|
22961
|
-
return { id: "refresh_guidance", markdown: REFRESH_GUIDANCE };
|
|
22962
|
-
}
|
|
22963
|
-
if (cfg.brain.cloud !== void 0 && cfg.brain.cloud.api_url === apiUrl()) return null;
|
|
22964
|
-
if (!isSetUp(cfg)) return { id: "finish_setup", markdown: FINISH_SETUP };
|
|
22965
|
-
if (cfg.brain.brains.length === 0 && cfg.brain.cloud === void 0) {
|
|
22966
|
-
return { id: "connect_a_brain", markdown: CONNECT_A_BRAIN };
|
|
22967
|
-
}
|
|
22968
|
-
return null;
|
|
22969
|
-
}
|
|
22970
|
-
async function currentNextStep() {
|
|
22971
|
-
try {
|
|
22972
|
-
const stored = await readCredentials().catch(() => null);
|
|
22973
|
-
return nextStepFor(await loadConfig(), stored !== null && stored.api_url === apiUrl());
|
|
22974
|
-
} catch {
|
|
22975
|
-
return null;
|
|
22976
|
-
}
|
|
22977
|
-
}
|
|
22978
|
-
|
|
22979
|
-
// src/commands/update.ts
|
|
22980
|
-
init_provenance();
|
|
22981
|
-
|
|
22982
23358
|
// src/lib/update-check.ts
|
|
22983
23359
|
init_atomic();
|
|
22984
23360
|
init_brand();
|
|
@@ -23188,6 +23564,18 @@ async function runUpdate(opts, inherited) {
|
|
|
23188
23564
|
return;
|
|
23189
23565
|
}
|
|
23190
23566
|
const held = await holdUpdateSlot(inherited);
|
|
23567
|
+
if (held === null && await blockedBy() === "stuck") {
|
|
23568
|
+
const lock = join28(brandHome(), RECLAIM_LOCK);
|
|
23569
|
+
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.`;
|
|
23570
|
+
printDual({
|
|
23571
|
+
json: { ...status, provenance: provenance.kind, updated: false, reason: "update_claim_stuck", hint },
|
|
23572
|
+
human: () => {
|
|
23573
|
+
printHuman(`memgineering ${status.latest} is available, but a previous update's leftover claim could not be cleared.`);
|
|
23574
|
+
printHuman(c.gray(hint));
|
|
23575
|
+
}
|
|
23576
|
+
});
|
|
23577
|
+
return;
|
|
23578
|
+
}
|
|
23191
23579
|
if (held === null) {
|
|
23192
23580
|
const started = (await claimOwner())?.split(" ")[0];
|
|
23193
23581
|
const hint = "Another memgineering update is installing right now. Run `memgineering update` again in a few minutes \u2014 it reports whether that install worked.";
|
|
@@ -23243,10 +23631,10 @@ async function backgroundUpdateNotice(deps = {}) {
|
|
|
23243
23631
|
const status = await (deps.check ?? checkForUpdate)(VERSION);
|
|
23244
23632
|
await reportPreviousAttempt();
|
|
23245
23633
|
if (status.updateAvailable && cfg.system.autoUpdate === "on" && isSetUp(cfg) && !process.env["CI"]) {
|
|
23246
|
-
const provenance = await (deps.provenance ?? detectProvenance)();
|
|
23634
|
+
const provenance = await (deps.provenance ?? (() => detectProvenance(void 0, { deadlineMs: BACKGROUND_PROBE_DEADLINE_MS })))();
|
|
23247
23635
|
const token = newClaimToken();
|
|
23248
23636
|
if (canSelfUpdate(provenance) && await claimUpdateSlot(token)) {
|
|
23249
|
-
(deps.startUpdate ?? ((t) => startDetachedUpdate(
|
|
23637
|
+
(deps.startUpdate ?? ((t) => startDetachedUpdate(spawn4, t)))(token);
|
|
23250
23638
|
notice(
|
|
23251
23639
|
c.gray(
|
|
23252
23640
|
`memgineering ${status.latest} is available \u2014 installing it in the background (auto-update is on). The next run uses it.`
|
|
@@ -23269,7 +23657,7 @@ async function backgroundUpdateNotice(deps = {}) {
|
|
|
23269
23657
|
} catch {
|
|
23270
23658
|
}
|
|
23271
23659
|
}
|
|
23272
|
-
function startDetachedUpdate(spawnImpl =
|
|
23660
|
+
function startDetachedUpdate(spawnImpl = spawn4, token) {
|
|
23273
23661
|
const child = spawnImpl(process.execPath, [process.argv[1] ?? "", "update"], {
|
|
23274
23662
|
detached: true,
|
|
23275
23663
|
stdio: "ignore",
|
|
@@ -23310,7 +23698,7 @@ async function holdUpdateSlot(inherited) {
|
|
|
23310
23698
|
}
|
|
23311
23699
|
var HOOK_VERBS = /* @__PURE__ */ new Set(["guard", "resurface"]);
|
|
23312
23700
|
var UPDATE_CLAIM_MS = 15 * 6e4;
|
|
23313
|
-
async function claimUpdateSlot(token = newClaimToken()) {
|
|
23701
|
+
async function claimUpdateSlot(token = newClaimToken(), hooks = {}) {
|
|
23314
23702
|
const path = join28(brandHome(), "update-running");
|
|
23315
23703
|
try {
|
|
23316
23704
|
await mkdir18(brandHome(), { recursive: true });
|
|
@@ -23323,17 +23711,86 @@ async function claimUpdateSlot(token = newClaimToken()) {
|
|
|
23323
23711
|
if (err?.code !== "EEXIST") return false;
|
|
23324
23712
|
}
|
|
23325
23713
|
try {
|
|
23326
|
-
if (Date.now() - (await stat7(path)).mtimeMs >= UPDATE_CLAIM_MS)
|
|
23714
|
+
if (Date.now() - (await stat7(path)).mtimeMs >= UPDATE_CLAIM_MS) {
|
|
23715
|
+
await hooks.afterObserve?.();
|
|
23716
|
+
await withReclaimLock(async () => {
|
|
23717
|
+
if (Date.now() - (await stat7(path)).mtimeMs < UPDATE_CLAIM_MS) return;
|
|
23718
|
+
await hooks.afterRecheck?.();
|
|
23719
|
+
await unlink4(path);
|
|
23720
|
+
});
|
|
23721
|
+
}
|
|
23327
23722
|
} catch {
|
|
23328
23723
|
}
|
|
23329
23724
|
return false;
|
|
23330
23725
|
}
|
|
23726
|
+
var RECLAIM_LOCK = "update-running.reclaim";
|
|
23727
|
+
var RECLAIM_LOCK_MAX_MS = 10 * 6e4;
|
|
23728
|
+
async function withReclaimLock(fn) {
|
|
23729
|
+
const home = brandHome();
|
|
23730
|
+
const lock = join28(home, RECLAIM_LOCK);
|
|
23731
|
+
const staging = join28(home, `${RECLAIM_LOCK}.${process.pid}.${randomBytes2(4).toString("hex")}`);
|
|
23732
|
+
await mkdir18(staging, { recursive: true });
|
|
23733
|
+
await writeFile15(join28(staging, "owner"), `${process.pid} ${Date.now()}
|
|
23734
|
+
`);
|
|
23735
|
+
try {
|
|
23736
|
+
await rename4(staging, lock);
|
|
23737
|
+
} catch {
|
|
23738
|
+
await rm8(staging, { recursive: true, force: true }).catch(() => void 0);
|
|
23739
|
+
await breakAbandonedLock(lock);
|
|
23740
|
+
return null;
|
|
23741
|
+
}
|
|
23742
|
+
try {
|
|
23743
|
+
return await fn();
|
|
23744
|
+
} finally {
|
|
23745
|
+
const aside = `${lock}.done.${randomBytes2(4).toString("hex")}`;
|
|
23746
|
+
await rename4(lock, aside).then(() => rm8(aside, { recursive: true, force: true })).catch(() => void 0);
|
|
23747
|
+
}
|
|
23748
|
+
}
|
|
23749
|
+
async function breakAbandonedLock(lock) {
|
|
23750
|
+
try {
|
|
23751
|
+
const seen = (await readFile27(join28(lock, "owner"), "utf8")).trim();
|
|
23752
|
+
if (!lockAbandoned(seen)) return;
|
|
23753
|
+
const aside = `${lock}.dead.${randomBytes2(4).toString("hex")}`;
|
|
23754
|
+
await rename4(lock, aside);
|
|
23755
|
+
const moved = (await readFile27(join28(aside, "owner"), "utf8").catch(() => "")).trim();
|
|
23756
|
+
if (moved !== seen) {
|
|
23757
|
+
await rename4(aside, lock).catch(() => void 0);
|
|
23758
|
+
return;
|
|
23759
|
+
}
|
|
23760
|
+
await rm8(aside, { recursive: true, force: true });
|
|
23761
|
+
} catch {
|
|
23762
|
+
}
|
|
23763
|
+
}
|
|
23764
|
+
function lockAbandoned(owner, now = Date.now()) {
|
|
23765
|
+
const [pidText, atText] = owner.split(" ");
|
|
23766
|
+
const pid = Number(pidText);
|
|
23767
|
+
const at = Number(atText);
|
|
23768
|
+
if (!Number.isFinite(at) || now - at > RECLAIM_LOCK_MAX_MS) return true;
|
|
23769
|
+
if (!Number.isInteger(pid) || pid <= 0) return true;
|
|
23770
|
+
try {
|
|
23771
|
+
process.kill(pid, 0);
|
|
23772
|
+
return false;
|
|
23773
|
+
} catch (err) {
|
|
23774
|
+
return err?.code !== "EPERM";
|
|
23775
|
+
}
|
|
23776
|
+
}
|
|
23777
|
+
async function blockedBy() {
|
|
23778
|
+
try {
|
|
23779
|
+
const old = Date.now() - (await stat7(join28(brandHome(), "update-running"))).mtimeMs >= UPDATE_CLAIM_MS;
|
|
23780
|
+
if (old) {
|
|
23781
|
+
await stat7(join28(brandHome(), RECLAIM_LOCK));
|
|
23782
|
+
return "stuck";
|
|
23783
|
+
}
|
|
23784
|
+
} catch {
|
|
23785
|
+
}
|
|
23786
|
+
return "in_progress";
|
|
23787
|
+
}
|
|
23331
23788
|
async function reportPreviousAttempt() {
|
|
23332
23789
|
const previous = await readUpdateState();
|
|
23333
23790
|
if (!previous || previous.ok) return;
|
|
23334
23791
|
notice(
|
|
23335
23792
|
c.yellow(`\u26A0 The last update attempt did not complete: ${previous.message}`) + c.gray(
|
|
23336
|
-
"\n `memgineering update` shows why. `memgineering setup --agent --auto-update off
|
|
23793
|
+
"\n `memgineering update` shows why. `memgineering setup --agent --auto-update off` stops\n these attempts; the hooks stay as this machine recorded them."
|
|
23337
23794
|
)
|
|
23338
23795
|
);
|
|
23339
23796
|
}
|
|
@@ -23353,7 +23810,7 @@ function reportBelowMinimum(below) {
|
|
|
23353
23810
|
|
|
23354
23811
|
// src/commands/undo.ts
|
|
23355
23812
|
init_src();
|
|
23356
|
-
import { readFile as readFile28, rm as
|
|
23813
|
+
import { readFile as readFile28, rm as rm9 } from "fs/promises";
|
|
23357
23814
|
import { join as join29 } from "path";
|
|
23358
23815
|
import { Command as Command42 } from "commander";
|
|
23359
23816
|
init_atomic();
|
|
@@ -23523,7 +23980,7 @@ async function reverse(brain, op, current) {
|
|
|
23523
23980
|
"Nothing to take back. `memgineering log` shows what this brain recorded."
|
|
23524
23981
|
);
|
|
23525
23982
|
}
|
|
23526
|
-
await
|
|
23983
|
+
await rm9(absolute, { force: true });
|
|
23527
23984
|
return null;
|
|
23528
23985
|
}
|
|
23529
23986
|
if (op.verb === "exclude" || op.verb === "unexclude") {
|
|
@@ -23663,7 +24120,7 @@ init_api_client();
|
|
|
23663
24120
|
init_credentials();
|
|
23664
24121
|
init_cloud_brain();
|
|
23665
24122
|
init_config();
|
|
23666
|
-
import { isAbsolute as isAbsolute5, relative as relative2, resolve as
|
|
24123
|
+
import { isAbsolute as isAbsolute5, relative as relative2, resolve as resolve16 } from "path";
|
|
23667
24124
|
import { Command as Command44 } from "commander";
|
|
23668
24125
|
init_vault();
|
|
23669
24126
|
init_ui();
|
|
@@ -23693,7 +24150,7 @@ function useCommand() {
|
|
|
23693
24150
|
await pointAtHosted(opts.brain, opts, brainPath);
|
|
23694
24151
|
return;
|
|
23695
24152
|
}
|
|
23696
|
-
const where = await canonicalize(
|
|
24153
|
+
const where = await canonicalize(resolve16(opts.here ?? process.cwd()));
|
|
23697
24154
|
if (!brainPath) {
|
|
23698
24155
|
const pointer = await readPointer(where);
|
|
23699
24156
|
const resolved = await resolveBrain(void 0, where).catch((err) => err);
|
|
@@ -23839,7 +24296,7 @@ ${hint}`));
|
|
|
23839
24296
|
});
|
|
23840
24297
|
return;
|
|
23841
24298
|
}
|
|
23842
|
-
const root = await canonicalize(
|
|
24299
|
+
const root = await canonicalize(resolve16(brainPath));
|
|
23843
24300
|
const cfg = await loadConfig();
|
|
23844
24301
|
if (!findBrain(cfg, root)) {
|
|
23845
24302
|
throw memgError(
|
|
@@ -23866,7 +24323,7 @@ A pointer to an unlinked folder would fail on every command instead of at this o
|
|
|
23866
24323
|
return;
|
|
23867
24324
|
}
|
|
23868
24325
|
const value = isInside(root, where) ? relative2(where, root) || "." : root;
|
|
23869
|
-
const file =
|
|
24326
|
+
const file = resolve16(where, BRAND.pointerFileName);
|
|
23870
24327
|
await writeThenRename(file, `${value}
|
|
23871
24328
|
`);
|
|
23872
24329
|
printDual({
|
|
@@ -24056,6 +24513,8 @@ function whoamiCommand() {
|
|
|
24056
24513
|
}
|
|
24057
24514
|
|
|
24058
24515
|
// src/program.ts
|
|
24516
|
+
init_config();
|
|
24517
|
+
init_next_step();
|
|
24059
24518
|
init_ui();
|
|
24060
24519
|
init_brand();
|
|
24061
24520
|
|
|
@@ -24178,6 +24637,10 @@ async function announceNextStep(commandName, reader) {
|
|
|
24178
24637
|
const step = await currentNextStep();
|
|
24179
24638
|
if (step === null) return;
|
|
24180
24639
|
announced = true;
|
|
24640
|
+
if (step.id === "setup_connected") {
|
|
24641
|
+
const cfg = await loadConfig().catch(() => null);
|
|
24642
|
+
if (cfg?.system.setupConnected) await markConnectedNoticeShown(cfg.system.setupConnected.at);
|
|
24643
|
+
}
|
|
24181
24644
|
if (step.id === "finish_setup" && reader !== "full") {
|
|
24182
24645
|
const text = reader === "person" ? SHORT_SETUP_NOTICE : reader === "verb" && await claimFullSetupNotice() ? step.markdown : SETUP_NUDGE;
|
|
24183
24646
|
process.stderr.write(`
|
|
@@ -24242,8 +24705,14 @@ async function run3(argv) {
|
|
|
24242
24705
|
process.stderr.isTTY === true ? "person" : cleaned[2] === void 0 ? "full" : "brief"
|
|
24243
24706
|
);
|
|
24244
24707
|
}
|
|
24708
|
+
let ran;
|
|
24709
|
+
program.hook("preAction", (_root, action) => {
|
|
24710
|
+
let command = action;
|
|
24711
|
+
while (command.parent && command.parent !== program) command = command.parent;
|
|
24712
|
+
ran = command === program ? void 0 : command.name();
|
|
24713
|
+
});
|
|
24245
24714
|
await program.parseAsync(cleaned);
|
|
24246
|
-
if (
|
|
24715
|
+
if (ran !== "update") await backgroundUpdateNotice(ran === void 0 ? {} : { verb: ran });
|
|
24247
24716
|
}
|
|
24248
24717
|
async function handleError(err) {
|
|
24249
24718
|
const failure = adaptRegistryError(err) ?? err;
|