memgineering 0.25.1 → 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 +56 -0
- package/assets/MEMGINEERING.md +1 -1
- package/assets/memgineering-recall/SKILL.md +3 -2
- package/assets/memgineering-setup/SKILL.md +3 -2
- package/dist/index.js +498 -278
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,62 @@ language the reader wants. The bilingual rule the monorepo applies to
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.25.2] — 2026-10-05
|
|
15
|
+
|
|
16
|
+
Small fixes found by an independent review of 0.25.0 and 0.25.1. Nothing here
|
|
17
|
+
changes what you type.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **A refresh `setup` keeps your choices.** `memgineering setup` run again
|
|
22
|
+
without `--hook` or `--auto-update` used to turn both back on. It now keeps
|
|
23
|
+
the hook and auto-update choices recorded on this machine, including a hook
|
|
24
|
+
choice made on the `setup --web` screen. On a machine set
|
|
25
|
+
up before this release, the hook choice is read from whether the hook is
|
|
26
|
+
installed. A kept choice is no longer listed as a default nobody was asked
|
|
27
|
+
about.
|
|
28
|
+
- **A command no longer waits on the update check.** Before a background
|
|
29
|
+
install, the CLI asks npm, pnpm and bun where they keep global packages. That
|
|
30
|
+
could hold your command open for up to 15 seconds after its output, and
|
|
31
|
+
longer behind a version manager. The CLI now asks all three at once and gives
|
|
32
|
+
up after 1.5 seconds. If it cannot tell how memgineering was installed, it
|
|
33
|
+
installs nothing and prints the usual "is available — `memgineering update`"
|
|
34
|
+
line.
|
|
35
|
+
- **The before-edit hook never starts an update**, even when it is called with
|
|
36
|
+
a flag first (`memgineering --json guard`). The same holds for `resurface`.
|
|
37
|
+
- **Two commands starting together cannot both start a background install.**
|
|
38
|
+
This could happen when the previous install had been left unfinished. If the
|
|
39
|
+
lock behind this fix is ever left in place, `memgineering update` says so
|
|
40
|
+
(`update_claim_stuck`) instead of reporting another install in progress.
|
|
41
|
+
- **Both sides of a conflict appear in recall.** When a note contradicted an
|
|
42
|
+
unfilled base template, the limit of two template cards could hide one side
|
|
43
|
+
while recall said "Both sides are above". Both sides now show, even if that
|
|
44
|
+
means more than two template cards, and the line counting held-back
|
|
45
|
+
templates says so.
|
|
46
|
+
- **Hosted recall by meaning no longer drops a base note you have filled in.**
|
|
47
|
+
A `USER.md` or other `01_BASE/` note you rewrote in your own words was
|
|
48
|
+
treated as an untouched template and left out. Only an exact, untouched
|
|
49
|
+
template is held back now.
|
|
50
|
+
- **A chat-app connector cannot change a rule by other routes.** Retiring,
|
|
51
|
+
restoring or undoing a rule from a connector is now refused with
|
|
52
|
+
`rule_needs_a_person`, as editing one already was. That includes undoing the
|
|
53
|
+
`remember` that created a rule. A connector's `undo` with no id undoes only
|
|
54
|
+
that app's own changes, including those made before its sign-in was
|
|
55
|
+
refreshed. If it has made none, it is told so and nothing is undone.
|
|
56
|
+
- Hosted `retire` and `unretire` no longer say that `undo` cannot reverse
|
|
57
|
+
them. It can, and their hint now names both ways back.
|
|
58
|
+
- `setup --help` gives ChatGPT its own route: Settings → Developer mode → add
|
|
59
|
+
an MCP server. Claude.ai, Claude Desktop and Cowork use a custom connector.
|
|
60
|
+
- `setup --web` prints its terminal lines in English.
|
|
61
|
+
|
|
62
|
+
### Agent guidance
|
|
63
|
+
|
|
64
|
+
- `memgineering-setup`: turning auto-update off with
|
|
65
|
+
`setup --agent --auto-update off` no longer needs `--hook`, because the hook
|
|
66
|
+
stays as this machine recorded it. It also explains `update_claim_stuck`.
|
|
67
|
+
- `memgineering-recall`: a template card that is one side of a conflict comes
|
|
68
|
+
on top of the two-card limit.
|
|
69
|
+
|
|
14
70
|
## [0.25.1] — 2026-10-05
|
|
15
71
|
|
|
16
72
|
A security release. Upgrade if you use `setup --web`, or a hosted brain with a
|
package/assets/MEMGINEERING.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: memgineering
|
|
3
3
|
description: Use whenever the user refers to something they told you before, asks what was decided, tells you something worth keeping, or settles something that should hold next time. The memory lives in their own folder and outlives this session; check it before answering from guesswork, and write to it when you learn something durable.
|
|
4
4
|
type: skill
|
|
5
|
-
version: 0.25.
|
|
5
|
+
version: 0.25.2
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# memgineering
|
|
@@ -210,8 +210,9 @@ it, so it is evidence of nothing — do not quote it back as what they think. Wh
|
|
|
210
210
|
every card carries that mark, recall says so above them (`--json`:
|
|
211
211
|
`scaffolding: true`), and that is the moment to offer to fill them in rather
|
|
212
212
|
than answer from them — see `memgineering-writing`. At most two such cards come
|
|
213
|
-
back per answer
|
|
214
|
-
|
|
213
|
+
back per answer, plus any that is one side of a conflict, so both sides of a
|
|
214
|
+
conflict always show; the rest are counted (`templates_withheld`), never
|
|
215
|
+
silently dropped, and nothing anyone wrote is ever held back.
|
|
215
216
|
|
|
216
217
|
The mark disappears once the user writes in the file. A filled-in base note is
|
|
217
218
|
often the best answer in the brain: _who am I_, _what am I not allowed to do_,
|
|
@@ -127,7 +127,8 @@ or npx copy is never touched — it only gets the notice; nor is anything instal
|
|
|
127
127
|
from a session hook or under CI). One install runs at a time: `memgineering
|
|
128
128
|
update` run while a background install is going answers `update_in_progress`
|
|
129
129
|
and installs nothing — wait a few minutes and run it again; it reports whether
|
|
130
|
-
that install worked.
|
|
130
|
+
that install worked. `update_claim_stuck` means a crashed install's leftover
|
|
131
|
+
could not be cleared; its hint names the folder to delete. `--hook on` means note
|
|
131
132
|
summaries and their standing rules reach an agent unasked. Recommend both, but
|
|
132
133
|
they are theirs to decide.
|
|
133
134
|
|
|
@@ -513,7 +514,7 @@ answers without changing its name and the index has to be laid down again.
|
|
|
513
514
|
## Turning things off
|
|
514
515
|
|
|
515
516
|
```
|
|
516
|
-
memgineering setup --agent --auto-update off
|
|
517
|
+
memgineering setup --agent --auto-update off # the hook stays as this machine recorded it
|
|
517
518
|
memgineering unlink # stop reading a brain; notes untouched
|
|
518
519
|
memgineering unlink --purge # also delete the local index and undo snapshots
|
|
519
520
|
```
|
package/dist/index.js
CHANGED
|
@@ -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"() {
|
|
@@ -4905,6 +4912,7 @@ var init_src = __esm({
|
|
|
4905
4912
|
init_planner();
|
|
4906
4913
|
init_semantic();
|
|
4907
4914
|
init_unfilled();
|
|
4915
|
+
init_setup_progress();
|
|
4908
4916
|
init_query_prefix();
|
|
4909
4917
|
init_base_templates();
|
|
4910
4918
|
init_scope_spread();
|
|
@@ -5507,6 +5515,13 @@ var init_config = __esm({
|
|
|
5507
5515
|
* answer as "older than this CLI" and wants the same one command.
|
|
5508
5516
|
*/
|
|
5509
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),
|
|
5510
5525
|
/**
|
|
5511
5526
|
* Notes `setup --web` was asked to upload, not yet sent.
|
|
5512
5527
|
*
|
|
@@ -7864,13 +7879,208 @@ var init_setup_signin = __esm({
|
|
|
7864
7879
|
}
|
|
7865
7880
|
});
|
|
7866
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
|
+
|
|
7867
8077
|
// src/lib/provenance.ts
|
|
7868
|
-
import { execFile } from "child_process";
|
|
8078
|
+
import { execFile, spawn as spawn2 } from "child_process";
|
|
7869
8079
|
import { realpath as realpath3 } from "fs/promises";
|
|
7870
|
-
import { dirname as dirname8, join as
|
|
8080
|
+
import { dirname as dirname8, join as join24, resolve as resolve14 } from "path";
|
|
7871
8081
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7872
8082
|
import { promisify } from "util";
|
|
7873
|
-
async function detectProvenance(selfPath) {
|
|
8083
|
+
async function detectProvenance(selfPath, opts = {}) {
|
|
7874
8084
|
const entry = selfPath ?? fileURLToPath2(import.meta.url);
|
|
7875
8085
|
const selfDir = await realpath3(dirname8(entry)).catch(() => dirname8(entry));
|
|
7876
8086
|
if (/[\\/]_npx[\\/]|[\\/]\.npm[\\/]_cacache[\\/]/.test(selfDir)) {
|
|
@@ -7882,9 +8092,11 @@ async function detectProvenance(selfPath) {
|
|
|
7882
8092
|
reason: "this is a one-off npx run, not an installed copy"
|
|
7883
8093
|
};
|
|
7884
8094
|
}
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
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;
|
|
7888
8100
|
if (isInside2(selfDir, manager.moduleRoot(prefix))) {
|
|
7889
8101
|
return {
|
|
7890
8102
|
kind: manager.kind,
|
|
@@ -7895,6 +8107,15 @@ async function detectProvenance(selfPath) {
|
|
|
7895
8107
|
};
|
|
7896
8108
|
}
|
|
7897
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
|
+
}
|
|
7898
8119
|
const local = selfDir.includes(`${"node_modules"}`);
|
|
7899
8120
|
return {
|
|
7900
8121
|
kind: local ? "local" : "unknown",
|
|
@@ -7907,6 +8128,62 @@ async function detectProvenance(selfPath) {
|
|
|
7907
8128
|
function canSelfUpdate(p) {
|
|
7908
8129
|
return p.kind === "npm" || p.kind === "pnpm" || p.kind === "bun";
|
|
7909
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
|
+
}
|
|
7910
8187
|
async function prefixOf(bin, args) {
|
|
7911
8188
|
try {
|
|
7912
8189
|
const { stdout } = await run(bin, args, { timeout: 5e3 });
|
|
@@ -7928,7 +8205,7 @@ async function unreachableByName(selfPath) {
|
|
|
7928
8205
|
const p = await detectProvenance(entry);
|
|
7929
8206
|
return p.kind === "local" || p.kind === "npx" ? p.reason : null;
|
|
7930
8207
|
}
|
|
7931
|
-
var run, MANAGERS;
|
|
8208
|
+
var run, MANAGERS, BACKGROUND_PROBE_DEADLINE_MS;
|
|
7932
8209
|
var init_provenance = __esm({
|
|
7933
8210
|
"src/lib/provenance.ts"() {
|
|
7934
8211
|
"use strict";
|
|
@@ -7938,7 +8215,7 @@ var init_provenance = __esm({
|
|
|
7938
8215
|
kind: "npm",
|
|
7939
8216
|
bin: "npm",
|
|
7940
8217
|
prefixArgs: ["prefix", "-g"],
|
|
7941
|
-
moduleRoot: (prefix) => process.platform === "win32" ?
|
|
8218
|
+
moduleRoot: (prefix) => process.platform === "win32" ? join24(prefix, "node_modules") : join24(prefix, "lib", "node_modules"),
|
|
7942
8219
|
install: "npm i -g memgineering@latest"
|
|
7943
8220
|
},
|
|
7944
8221
|
{
|
|
@@ -7957,6 +8234,7 @@ var init_provenance = __esm({
|
|
|
7957
8234
|
install: "bun add -g memgineering"
|
|
7958
8235
|
}
|
|
7959
8236
|
];
|
|
8237
|
+
BACKGROUND_PROBE_DEADLINE_MS = 1500;
|
|
7960
8238
|
}
|
|
7961
8239
|
});
|
|
7962
8240
|
|
|
@@ -7972,24 +8250,29 @@ __export(setup_cloud_exports, {
|
|
|
7972
8250
|
runSetupCloudChild: () => runSetupCloudChild,
|
|
7973
8251
|
waitAndApply: () => waitAndApply
|
|
7974
8252
|
});
|
|
7975
|
-
import { spawn as
|
|
8253
|
+
import { spawn as spawn3 } from "child_process";
|
|
7976
8254
|
import { realpathSync as realpathSync2 } from "fs";
|
|
7977
8255
|
import { readdir as readdir7, stat as stat6 } from "fs/promises";
|
|
7978
8256
|
import { homedir as homedir5 } from "os";
|
|
7979
|
-
import { basename as basename3, dirname as dirname9, join as
|
|
7980
|
-
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) {
|
|
7981
8259
|
try {
|
|
7982
8260
|
await apiRequest({
|
|
7983
8261
|
method: "POST",
|
|
7984
8262
|
path: "/v1/setup/session/report",
|
|
7985
|
-
body: {
|
|
8263
|
+
body: {
|
|
8264
|
+
setup_token: setupToken,
|
|
8265
|
+
state,
|
|
8266
|
+
...detail === void 0 ? {} : { detail },
|
|
8267
|
+
...code === void 0 ? {} : { code }
|
|
8268
|
+
}
|
|
7986
8269
|
});
|
|
7987
8270
|
} catch {
|
|
7988
8271
|
}
|
|
7989
8272
|
}
|
|
7990
8273
|
async function folderCandidates() {
|
|
7991
8274
|
const home = homedir5();
|
|
7992
|
-
const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) =>
|
|
8275
|
+
const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) => join25(home, d));
|
|
7993
8276
|
const found = [];
|
|
7994
8277
|
const countNotes = async (dir) => {
|
|
7995
8278
|
let count = 0;
|
|
@@ -7998,7 +8281,7 @@ async function folderCandidates() {
|
|
|
7998
8281
|
if (entry.name.startsWith(".")) continue;
|
|
7999
8282
|
if (entry.isFile() && /\.(md|markdown)$/i.test(entry.name)) count += 1;
|
|
8000
8283
|
else if (entry.isDirectory()) {
|
|
8001
|
-
const inner = await readdir7(
|
|
8284
|
+
const inner = await readdir7(join25(dir, entry.name)).catch(() => []);
|
|
8002
8285
|
count += inner.filter((n) => /\.(md|markdown)$/i.test(n)).length;
|
|
8003
8286
|
}
|
|
8004
8287
|
if (count > 999) break;
|
|
@@ -8058,17 +8341,24 @@ async function applyAnswers(setupToken, claim, detected, guards = { candidates:
|
|
|
8058
8341
|
...cfg,
|
|
8059
8342
|
system: {
|
|
8060
8343
|
...cfg.system,
|
|
8061
|
-
setupConnected: {
|
|
8344
|
+
setupConnected: {
|
|
8345
|
+
account_id: account.id,
|
|
8346
|
+
email: account.email,
|
|
8347
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8348
|
+
}
|
|
8062
8349
|
}
|
|
8063
8350
|
});
|
|
8064
8351
|
}
|
|
8065
8352
|
}
|
|
8066
|
-
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");
|
|
8067
8354
|
const choice = {
|
|
8068
8355
|
tools: toolsFromAnswers(answers, detected),
|
|
8069
8356
|
autoUpdate: answers.auto_update ? "on" : "off",
|
|
8070
8357
|
errorTelemetry: answers.error_telemetry === null ? "undecided" : answers.error_telemetry ? "on" : "off",
|
|
8071
|
-
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"
|
|
8072
8362
|
};
|
|
8073
8363
|
await applySetup(choice, false);
|
|
8074
8364
|
const parts = [];
|
|
@@ -8096,7 +8386,7 @@ async function applyAnswers(setupToken, claim, detected, guards = { candidates:
|
|
|
8096
8386
|
const email = account?.email ?? null;
|
|
8097
8387
|
const yes = guards.confirmUpload ? await guards.confirmUpload({ path, email }) : null;
|
|
8098
8388
|
if (yes === true) {
|
|
8099
|
-
await report2(setupToken, "applying", "\uB178\uD2B8\uB97C \uC62C\uB9AC\uB294 \uC911\uC774\uC5D0\uC694.");
|
|
8389
|
+
await report2(setupToken, "applying", "\uB178\uD2B8\uB97C \uC62C\uB9AC\uB294 \uC911\uC774\uC5D0\uC694.", "uploading");
|
|
8100
8390
|
const { pushCommand: pushCommand2 } = await Promise.resolve().then(() => (init_push(), push_exports));
|
|
8101
8391
|
await pushCommand2().parseAsync(
|
|
8102
8392
|
["--vault", path, ...claim.brain?.id ? ["--brain", claim.brain.id] : []],
|
|
@@ -8119,7 +8409,12 @@ async function applyAnswers(setupToken, claim, detected, guards = { candidates:
|
|
|
8119
8409
|
}
|
|
8120
8410
|
}
|
|
8121
8411
|
});
|
|
8122
|
-
await report2(
|
|
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
|
+
);
|
|
8123
8418
|
parts.push(`upload of ${path} waits for a check on this machine`);
|
|
8124
8419
|
} else {
|
|
8125
8420
|
parts.push(`not uploaded: ${path}`);
|
|
@@ -8189,11 +8484,12 @@ async function waitAndApply(setupToken, opts) {
|
|
|
8189
8484
|
candidates: offered,
|
|
8190
8485
|
confirmUpload: opts.confirmUpload ?? null
|
|
8191
8486
|
});
|
|
8192
|
-
await report2(setupToken, "done", "\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694.");
|
|
8487
|
+
await report2(setupToken, "done", "\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694.", "done");
|
|
8193
8488
|
return done;
|
|
8194
8489
|
} catch (err) {
|
|
8195
8490
|
const message = err instanceof Error ? err.message : String(err);
|
|
8196
|
-
|
|
8491
|
+
const code = err.code === "conflict" ? "different_account" : "failed";
|
|
8492
|
+
await report2(setupToken, "failed", message.slice(0, 400), code);
|
|
8197
8493
|
throw err;
|
|
8198
8494
|
}
|
|
8199
8495
|
}
|
|
@@ -8205,7 +8501,7 @@ async function waitAndApply(setupToken, opts) {
|
|
|
8205
8501
|
}).catch(() => null);
|
|
8206
8502
|
if (state?.body?.folders_requested === true) {
|
|
8207
8503
|
sentFolders = true;
|
|
8208
|
-
opts.onProgress?.("
|
|
8504
|
+
opts.onProgress?.("Looking for note folders on this machine\u2026");
|
|
8209
8505
|
const candidates = await folderCandidates();
|
|
8210
8506
|
offered = candidates.map((c2) => c2.path);
|
|
8211
8507
|
await apiRequest({
|
|
@@ -8236,7 +8532,7 @@ async function startSession(detected) {
|
|
|
8236
8532
|
return body;
|
|
8237
8533
|
}
|
|
8238
8534
|
function detachChild(setupToken) {
|
|
8239
|
-
const child =
|
|
8535
|
+
const child = spawn3(process.execPath, [process.argv[1] ?? "", "setup", "--web"], {
|
|
8240
8536
|
detached: true,
|
|
8241
8537
|
stdio: ["pipe", "ignore", "ignore"],
|
|
8242
8538
|
env: { ...process.env, [SETUP_CHILD_ENV]: "1" }
|
|
@@ -8255,7 +8551,7 @@ async function runSetupCloud(opts) {
|
|
|
8255
8551
|
const session = await startSession(opts.detected);
|
|
8256
8552
|
const intervalMs = Math.max(1, session.interval ?? 5) * 1e3;
|
|
8257
8553
|
printHuman(`
|
|
8258
|
-
|
|
8554
|
+
Setup code: ${c.bold(session.user_code)}
|
|
8259
8555
|
`);
|
|
8260
8556
|
const opened = opts.printUrl ? false : await openBrowser(session.setup_url);
|
|
8261
8557
|
const foreground = opts.wait || process.stdout.isTTY === true && process.stderr.isTTY === true;
|
|
@@ -8271,14 +8567,14 @@ async function runSetupCloud(opts) {
|
|
|
8271
8567
|
waiting: foreground
|
|
8272
8568
|
},
|
|
8273
8569
|
human: () => {
|
|
8274
|
-
if (opened) printHuman("
|
|
8570
|
+
if (opened) printHuman("Opened the setup screen in the browser.");
|
|
8275
8571
|
else {
|
|
8276
|
-
printHuman("
|
|
8572
|
+
printHuman("Open this link in a browser to finish the setup:\n");
|
|
8277
8573
|
printHuman(` ${session.setup_url}`);
|
|
8278
8574
|
}
|
|
8279
8575
|
printHuman(
|
|
8280
8576
|
c.gray(
|
|
8281
|
-
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."
|
|
8282
8578
|
)
|
|
8283
8579
|
);
|
|
8284
8580
|
}
|
|
@@ -8296,7 +8592,7 @@ async function runSetupCloud(opts) {
|
|
|
8296
8592
|
confirmUpload: interactive ? askBeforeUpload : null
|
|
8297
8593
|
});
|
|
8298
8594
|
if (outcome === "expired") {
|
|
8299
|
-
const line = "
|
|
8595
|
+
const line = "The setup screen expired and nothing was changed. `memgineering setup --web` opens a new one.";
|
|
8300
8596
|
if (isJsonMode()) notice(JSON.stringify({ setup_web: "expired" }));
|
|
8301
8597
|
else printHuman(c.gray(`
|
|
8302
8598
|
${line}`));
|
|
@@ -8304,10 +8600,10 @@ ${line}`));
|
|
|
8304
8600
|
}
|
|
8305
8601
|
if (isJsonMode()) notice(JSON.stringify({ setup_web: "applied" }));
|
|
8306
8602
|
else {
|
|
8307
|
-
printHuman(c.green("\
|
|
8603
|
+
printHuman(c.green("\nSetup finished."));
|
|
8308
8604
|
printHuman(
|
|
8309
8605
|
c.gray(
|
|
8310
|
-
"
|
|
8606
|
+
" recall and remember work now.\n The newly installed guidance is read from the next conversation on."
|
|
8311
8607
|
)
|
|
8312
8608
|
);
|
|
8313
8609
|
}
|
|
@@ -8342,7 +8638,7 @@ async function askBeforeUpload(q) {
|
|
|
8342
8638
|
const { yes } = await prompts6({
|
|
8343
8639
|
type: "confirm",
|
|
8344
8640
|
name: "yes",
|
|
8345
|
-
message:
|
|
8641
|
+
message: `Upload the notes in ${q.path} to the brain on ${q.email ?? "this account"}?`,
|
|
8346
8642
|
initial: false
|
|
8347
8643
|
});
|
|
8348
8644
|
return yes === true;
|
|
@@ -8377,8 +8673,8 @@ var init_setup_cloud = __esm({
|
|
|
8377
8673
|
|
|
8378
8674
|
// src/commands/setup.ts
|
|
8379
8675
|
import { createHash as createHash9 } from "crypto";
|
|
8380
|
-
import { access as access2, mkdir as
|
|
8381
|
-
import { dirname as dirname10, join 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";
|
|
8382
8678
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8383
8679
|
import { Command as Command40 } from "commander";
|
|
8384
8680
|
import prompts5 from "prompts";
|
|
@@ -8403,10 +8699,11 @@ function setupCommand() {
|
|
|
8403
8699
|
"after",
|
|
8404
8700
|
[
|
|
8405
8701
|
"",
|
|
8406
|
-
"Chat apps with no shell
|
|
8407
|
-
`
|
|
8408
|
-
"
|
|
8409
|
-
"
|
|
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."
|
|
8410
8707
|
].join("\n")
|
|
8411
8708
|
).action(async (opts) => {
|
|
8412
8709
|
const detected = await detectTools();
|
|
@@ -8441,16 +8738,18 @@ function setupCommand() {
|
|
|
8441
8738
|
dryRun: opts.dryRun === true
|
|
8442
8739
|
});
|
|
8443
8740
|
const mode = await resolveMode(opts);
|
|
8444
|
-
const
|
|
8741
|
+
const recorded = mode.kind === "human" ? {} : await recordedChoices(detected);
|
|
8742
|
+
const choice = mode.kind === "human" ? await askHuman(detected, opts) : fromFlags(detected, opts, recorded);
|
|
8445
8743
|
const defaultsTaken = !mode.chosen ? [
|
|
8446
|
-
|
|
8744
|
+
// Only what nobody chose, here or on an earlier run.
|
|
8745
|
+
...opts.autoUpdate === void 0 && recorded.autoUpdate === void 0 ? ["auto-update: on"] : [],
|
|
8447
8746
|
// Every callback this flag arms, named — on Claude Code and Codex
|
|
8448
8747
|
// that is two (note summaries at session start, the user's own
|
|
8449
8748
|
// rules before an edit), and saying only the first would be
|
|
8450
8749
|
// reporting half of what was decided on their behalf. On Grok it
|
|
8451
8750
|
// is one, at the end of a turn, and naming the other two there
|
|
8452
8751
|
// reported callbacks that were never installed.
|
|
8453
|
-
...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`) : []
|
|
8454
8753
|
] : [];
|
|
8455
8754
|
if (choice === null) {
|
|
8456
8755
|
printHuman(c.yellow("Cancelled \u2014 nothing was changed."));
|
|
@@ -8716,6 +9015,7 @@ async function applySetup(choice, dryRun) {
|
|
|
8716
9015
|
system: {
|
|
8717
9016
|
...cfg.system,
|
|
8718
9017
|
autoUpdate: choice.autoUpdate,
|
|
9018
|
+
...choice.hookAnswer === void 0 ? {} : { hook: choice.hookAnswer },
|
|
8719
9019
|
installed,
|
|
8720
9020
|
// Stamped whether or not anything was rewritten. A run that finds every
|
|
8721
9021
|
// file already current is exactly the run that must clear the notice —
|
|
@@ -8741,7 +9041,7 @@ async function applySetup(choice, dryRun) {
|
|
|
8741
9041
|
}
|
|
8742
9042
|
async function write2() {
|
|
8743
9043
|
for (const tool of choice.tools) {
|
|
8744
|
-
const hubPath =
|
|
9044
|
+
const hubPath = join26(agentHome(), tool.hubFile);
|
|
8745
9045
|
const written = await injectBlock(
|
|
8746
9046
|
hubPath,
|
|
8747
9047
|
hub + tailFor(tool, choice.hook),
|
|
@@ -8756,8 +9056,8 @@ async function applySetup(choice, dryRun) {
|
|
|
8756
9056
|
});
|
|
8757
9057
|
if (!dryRun && written.after !== null) mark(hubPath, written.after);
|
|
8758
9058
|
for (const skill of BRAND.subSkills) {
|
|
8759
|
-
const content = await loadAsset(
|
|
8760
|
-
const path =
|
|
9059
|
+
const content = await loadAsset(join26(skill, "SKILL.md"));
|
|
9060
|
+
const path = join26(agentHome(), tool.skillsDir, skill, "SKILL.md");
|
|
8761
9061
|
const out = await writeIfDifferent(path, content, dryRun);
|
|
8762
9062
|
results.push({
|
|
8763
9063
|
tool: tool.label,
|
|
@@ -8830,6 +9130,23 @@ function globalInstallHint(reason, hooks) {
|
|
|
8830
9130
|
return `${hooks ? "The hooks and the guide call" : "The guide tells your agent to run"} \`memgineering\` by name, and ${reason} \u2014
|
|
8831
9131
|
so nothing reaches it until a global install exists: \`npm i -g memgineering\`. Nothing needs re-running after that.`;
|
|
8832
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
|
+
}
|
|
8833
9150
|
async function resolveMode(opts) {
|
|
8834
9151
|
if (opts.agent) return { kind: "agent", chosen: true };
|
|
8835
9152
|
if (opts.human) {
|
|
@@ -8863,11 +9180,11 @@ async function detectTools() {
|
|
|
8863
9180
|
const home = agentHome();
|
|
8864
9181
|
const found = [];
|
|
8865
9182
|
for (const tool of TOOLS) {
|
|
8866
|
-
if (await exists2(
|
|
9183
|
+
if (await exists2(join26(home, tool.dir))) found.push(tool);
|
|
8867
9184
|
}
|
|
8868
9185
|
return found;
|
|
8869
9186
|
}
|
|
8870
|
-
function fromFlags(detected, opts) {
|
|
9187
|
+
function fromFlags(detected, opts, recorded = {}) {
|
|
8871
9188
|
const tools = opts.tools ? opts.tools.split(",").map((t) => t.trim().toLowerCase()).map((id) => {
|
|
8872
9189
|
const tool = detected.find((t) => t.id === id);
|
|
8873
9190
|
if (tool) return tool;
|
|
@@ -8886,14 +9203,16 @@ function fromFlags(detected, opts) {
|
|
|
8886
9203
|
`${known.label} creates ~/${known.dir} the first time it runs. If it is installed, open it once, then run setup again. ${detectedList}`
|
|
8887
9204
|
);
|
|
8888
9205
|
}) : detected;
|
|
9206
|
+
const hookAnswer = parseChoice(opts.hook, recorded.hook ?? "on", "--hook");
|
|
8889
9207
|
return {
|
|
8890
9208
|
tools,
|
|
8891
|
-
autoUpdate: parseChoice(opts.autoUpdate, "on", "--auto-update"),
|
|
9209
|
+
autoUpdate: parseChoice(opts.autoUpdate, recorded.autoUpdate ?? "on", "--auto-update"),
|
|
8892
9210
|
errorTelemetry: opts.errorTelemetry === void 0 ? "undecided" : parseChoice(opts.errorTelemetry, "off", "--error-telemetry"),
|
|
8893
9211
|
// Only Claude Code can run something at session start. For the others the
|
|
8894
9212
|
// skill tells the agent to call resurface itself, which is why asking for
|
|
8895
9213
|
// a hook there is not an error — it simply has nowhere to go.
|
|
8896
|
-
hook:
|
|
9214
|
+
hook: hookAnswer === "on" && tools.some((t) => t.hooks),
|
|
9215
|
+
hookAnswer
|
|
8897
9216
|
};
|
|
8898
9217
|
}
|
|
8899
9218
|
function parseChoice(value, fallback, flag2) {
|
|
@@ -8952,17 +9271,19 @@ async function askHuman(detected, opts) {
|
|
|
8952
9271
|
autoUpdate: answers.autoUpdate === false ? "off" : parseChoice(opts.autoUpdate, "on", "--auto-update"),
|
|
8953
9272
|
// An aborted prompt is not an answer — undecided, never a silent yes.
|
|
8954
9273
|
errorTelemetry: answers.errorTelemetry === void 0 ? "undecided" : answers.errorTelemetry ? "on" : "off",
|
|
8955
|
-
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" }
|
|
8956
9277
|
};
|
|
8957
9278
|
}
|
|
8958
9279
|
async function injectBlock(filePath, content, dryRun, weWroteThis) {
|
|
8959
9280
|
const block = `${BRAND.hubMarkerStart}
|
|
8960
9281
|
${content.trimEnd()}
|
|
8961
9282
|
${BRAND.hubMarkerEnd}`;
|
|
8962
|
-
const existing = await
|
|
9283
|
+
const existing = await readFile25(filePath, "utf8").catch(() => null);
|
|
8963
9284
|
if (existing === null) {
|
|
8964
9285
|
if (!dryRun) {
|
|
8965
|
-
await
|
|
9286
|
+
await mkdir16(dirname10(filePath), { recursive: true });
|
|
8966
9287
|
await writeThenRename(filePath, `${block}
|
|
8967
9288
|
`);
|
|
8968
9289
|
}
|
|
@@ -9002,11 +9323,11 @@ function looksLikeOurGuide(text) {
|
|
|
9002
9323
|
return text.includes("memgineering recall") && text.includes("memgineering remember");
|
|
9003
9324
|
}
|
|
9004
9325
|
async function writeIfDifferent(filePath, content, dryRun) {
|
|
9005
|
-
const existing = await
|
|
9326
|
+
const existing = await readFile25(filePath, "utf8").catch(() => null);
|
|
9006
9327
|
const after = fingerprint(content);
|
|
9007
9328
|
if (existing === content) return { outcome: "unchanged", before: existing, after };
|
|
9008
9329
|
if (!dryRun) {
|
|
9009
|
-
await
|
|
9330
|
+
await mkdir16(dirname10(filePath), { recursive: true });
|
|
9010
9331
|
await writeThenRename(filePath, content);
|
|
9011
9332
|
}
|
|
9012
9333
|
return { outcome: existing === null ? "injected" : "updated", before: existing, after };
|
|
@@ -9017,8 +9338,8 @@ function hooksFor(tools) {
|
|
|
9017
9338
|
);
|
|
9018
9339
|
}
|
|
9019
9340
|
async function installHook(spec, tool, dryRun) {
|
|
9020
|
-
const settingsPath =
|
|
9021
|
-
const raw = await
|
|
9341
|
+
const settingsPath = join26(agentHome(), ...(tool.hooksFile ?? "").split("/"));
|
|
9342
|
+
const raw = await readFile25(settingsPath, "utf8").catch(() => null);
|
|
9022
9343
|
let settings = {};
|
|
9023
9344
|
if (raw !== null) {
|
|
9024
9345
|
try {
|
|
@@ -9060,15 +9381,15 @@ async function installHook(spec, tool, dryRun) {
|
|
|
9060
9381
|
}
|
|
9061
9382
|
const next = { ...settings, hooks: { ...hooks, [spec.event]: existingGroups } };
|
|
9062
9383
|
if (!dryRun) {
|
|
9063
|
-
await
|
|
9384
|
+
await mkdir16(dirname10(settingsPath), { recursive: true });
|
|
9064
9385
|
await writeThenRename(settingsPath, `${JSON.stringify(next, null, 2)}
|
|
9065
9386
|
`);
|
|
9066
9387
|
}
|
|
9067
9388
|
return raw === null ? "injected" : "updated";
|
|
9068
9389
|
}
|
|
9069
9390
|
async function removeHook(spec, tool, dryRun) {
|
|
9070
|
-
const settingsPath =
|
|
9071
|
-
const raw = await
|
|
9391
|
+
const settingsPath = join26(agentHome(), ...(tool.hooksFile ?? "").split("/"));
|
|
9392
|
+
const raw = await readFile25(settingsPath, "utf8").catch(() => null);
|
|
9072
9393
|
if (raw === null) return "unchanged";
|
|
9073
9394
|
let parsed;
|
|
9074
9395
|
try {
|
|
@@ -9092,7 +9413,7 @@ async function removeHook(spec, tool, dryRun) {
|
|
|
9092
9413
|
return "removed";
|
|
9093
9414
|
}
|
|
9094
9415
|
async function loadAsset(relPath) {
|
|
9095
|
-
const raw = await
|
|
9416
|
+
const raw = await readFile25(join26(await assetRoot(), relPath), "utf8");
|
|
9096
9417
|
if (relPath === BRAND.hubFileName && raw.startsWith("---")) {
|
|
9097
9418
|
const end = raw.indexOf("\n---", 4);
|
|
9098
9419
|
if (end !== -1) return raw.slice(end + 4).trimStart();
|
|
@@ -9101,12 +9422,12 @@ async function loadAsset(relPath) {
|
|
|
9101
9422
|
}
|
|
9102
9423
|
async function assetRoot() {
|
|
9103
9424
|
const here = dirname10(fileURLToPath3(import.meta.url));
|
|
9104
|
-
const bundled =
|
|
9105
|
-
if (await exists2(
|
|
9425
|
+
const bundled = join26(here, "..", "assets");
|
|
9426
|
+
if (await exists2(join26(bundled, BRAND.hubFileName))) return bundled;
|
|
9106
9427
|
let dir = here;
|
|
9107
9428
|
for (; ; ) {
|
|
9108
|
-
const candidate =
|
|
9109
|
-
if (await exists2(
|
|
9429
|
+
const candidate = join26(dir, "packages", "skills");
|
|
9430
|
+
if (await exists2(join26(candidate, BRAND.hubFileName))) return candidate;
|
|
9110
9431
|
const parent = dirname10(dir);
|
|
9111
9432
|
if (parent === dir) break;
|
|
9112
9433
|
dir = parent;
|
|
@@ -9136,6 +9457,7 @@ var init_setup = __esm({
|
|
|
9136
9457
|
init_setup_brain();
|
|
9137
9458
|
init_setup_signin();
|
|
9138
9459
|
init_config();
|
|
9460
|
+
init_next_step();
|
|
9139
9461
|
init_provenance();
|
|
9140
9462
|
init_ui();
|
|
9141
9463
|
init_version();
|
|
@@ -13040,12 +13362,12 @@ var BASE_FILES = [
|
|
|
13040
13362
|
];
|
|
13041
13363
|
var HUB_PATH = "00_HUB/HUB.md";
|
|
13042
13364
|
var TEMPLATE_SLOTS = 2;
|
|
13043
|
-
function capTemplates(cards, isTemplate) {
|
|
13365
|
+
function capTemplates(cards, isTemplate, inConflict = () => false) {
|
|
13044
13366
|
const kept = [];
|
|
13045
13367
|
const withheld = [];
|
|
13046
13368
|
let templates = 0;
|
|
13047
13369
|
for (const card of cards) {
|
|
13048
|
-
if (isTemplate(card) && ++templates > TEMPLATE_SLOTS) withheld.push(card);
|
|
13370
|
+
if (isTemplate(card) && !inConflict(card) && ++templates > TEMPLATE_SLOTS) withheld.push(card);
|
|
13049
13371
|
else kept.push(card);
|
|
13050
13372
|
}
|
|
13051
13373
|
return { kept, withheld };
|
|
@@ -22198,7 +22520,8 @@ function recallCommand() {
|
|
|
22198
22520
|
})
|
|
22199
22521
|
);
|
|
22200
22522
|
isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
|
|
22201
|
-
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));
|
|
22202
22525
|
templatesWithheld = capped2.withheld.length;
|
|
22203
22526
|
result = { ...local, memory_cards: capped2.kept };
|
|
22204
22527
|
entries = built.entries;
|
|
@@ -22393,7 +22716,7 @@ ${unsearchedLocal.map(
|
|
|
22393
22716
|
if (templatesWithheld > 0) {
|
|
22394
22717
|
printHuman(
|
|
22395
22718
|
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.
|
|
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.
|
|
22397
22720
|
`
|
|
22398
22721
|
)
|
|
22399
22722
|
);
|
|
@@ -23024,205 +23347,14 @@ init_setup();
|
|
|
23024
23347
|
init_brand();
|
|
23025
23348
|
init_version();
|
|
23026
23349
|
init_config();
|
|
23027
|
-
|
|
23350
|
+
init_next_step();
|
|
23351
|
+
init_provenance();
|
|
23352
|
+
import { spawn as spawn4 } from "child_process";
|
|
23028
23353
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
23029
|
-
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";
|
|
23030
23355
|
import { join as join28 } from "path";
|
|
23031
23356
|
import { Command as Command41 } from "commander";
|
|
23032
23357
|
|
|
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
23358
|
// src/lib/update-check.ts
|
|
23227
23359
|
init_atomic();
|
|
23228
23360
|
init_brand();
|
|
@@ -23432,6 +23564,18 @@ async function runUpdate(opts, inherited) {
|
|
|
23432
23564
|
return;
|
|
23433
23565
|
}
|
|
23434
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
|
+
}
|
|
23435
23579
|
if (held === null) {
|
|
23436
23580
|
const started = (await claimOwner())?.split(" ")[0];
|
|
23437
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.";
|
|
@@ -23487,10 +23631,10 @@ async function backgroundUpdateNotice(deps = {}) {
|
|
|
23487
23631
|
const status = await (deps.check ?? checkForUpdate)(VERSION);
|
|
23488
23632
|
await reportPreviousAttempt();
|
|
23489
23633
|
if (status.updateAvailable && cfg.system.autoUpdate === "on" && isSetUp(cfg) && !process.env["CI"]) {
|
|
23490
|
-
const provenance = await (deps.provenance ?? detectProvenance)();
|
|
23634
|
+
const provenance = await (deps.provenance ?? (() => detectProvenance(void 0, { deadlineMs: BACKGROUND_PROBE_DEADLINE_MS })))();
|
|
23491
23635
|
const token = newClaimToken();
|
|
23492
23636
|
if (canSelfUpdate(provenance) && await claimUpdateSlot(token)) {
|
|
23493
|
-
(deps.startUpdate ?? ((t) => startDetachedUpdate(
|
|
23637
|
+
(deps.startUpdate ?? ((t) => startDetachedUpdate(spawn4, t)))(token);
|
|
23494
23638
|
notice(
|
|
23495
23639
|
c.gray(
|
|
23496
23640
|
`memgineering ${status.latest} is available \u2014 installing it in the background (auto-update is on). The next run uses it.`
|
|
@@ -23513,7 +23657,7 @@ async function backgroundUpdateNotice(deps = {}) {
|
|
|
23513
23657
|
} catch {
|
|
23514
23658
|
}
|
|
23515
23659
|
}
|
|
23516
|
-
function startDetachedUpdate(spawnImpl =
|
|
23660
|
+
function startDetachedUpdate(spawnImpl = spawn4, token) {
|
|
23517
23661
|
const child = spawnImpl(process.execPath, [process.argv[1] ?? "", "update"], {
|
|
23518
23662
|
detached: true,
|
|
23519
23663
|
stdio: "ignore",
|
|
@@ -23554,7 +23698,7 @@ async function holdUpdateSlot(inherited) {
|
|
|
23554
23698
|
}
|
|
23555
23699
|
var HOOK_VERBS = /* @__PURE__ */ new Set(["guard", "resurface"]);
|
|
23556
23700
|
var UPDATE_CLAIM_MS = 15 * 6e4;
|
|
23557
|
-
async function claimUpdateSlot(token = newClaimToken()) {
|
|
23701
|
+
async function claimUpdateSlot(token = newClaimToken(), hooks = {}) {
|
|
23558
23702
|
const path = join28(brandHome(), "update-running");
|
|
23559
23703
|
try {
|
|
23560
23704
|
await mkdir18(brandHome(), { recursive: true });
|
|
@@ -23567,17 +23711,86 @@ async function claimUpdateSlot(token = newClaimToken()) {
|
|
|
23567
23711
|
if (err?.code !== "EEXIST") return false;
|
|
23568
23712
|
}
|
|
23569
23713
|
try {
|
|
23570
|
-
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
|
+
}
|
|
23571
23722
|
} catch {
|
|
23572
23723
|
}
|
|
23573
23724
|
return false;
|
|
23574
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
|
+
}
|
|
23575
23788
|
async function reportPreviousAttempt() {
|
|
23576
23789
|
const previous = await readUpdateState();
|
|
23577
23790
|
if (!previous || previous.ok) return;
|
|
23578
23791
|
notice(
|
|
23579
23792
|
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
|
|
23793
|
+
"\n `memgineering update` shows why. `memgineering setup --agent --auto-update off` stops\n these attempts; the hooks stay as this machine recorded them."
|
|
23581
23794
|
)
|
|
23582
23795
|
);
|
|
23583
23796
|
}
|
|
@@ -23597,7 +23810,7 @@ function reportBelowMinimum(below) {
|
|
|
23597
23810
|
|
|
23598
23811
|
// src/commands/undo.ts
|
|
23599
23812
|
init_src();
|
|
23600
|
-
import { readFile as readFile28, rm as
|
|
23813
|
+
import { readFile as readFile28, rm as rm9 } from "fs/promises";
|
|
23601
23814
|
import { join as join29 } from "path";
|
|
23602
23815
|
import { Command as Command42 } from "commander";
|
|
23603
23816
|
init_atomic();
|
|
@@ -23767,7 +23980,7 @@ async function reverse(brain, op, current) {
|
|
|
23767
23980
|
"Nothing to take back. `memgineering log` shows what this brain recorded."
|
|
23768
23981
|
);
|
|
23769
23982
|
}
|
|
23770
|
-
await
|
|
23983
|
+
await rm9(absolute, { force: true });
|
|
23771
23984
|
return null;
|
|
23772
23985
|
}
|
|
23773
23986
|
if (op.verb === "exclude" || op.verb === "unexclude") {
|
|
@@ -24301,6 +24514,7 @@ function whoamiCommand() {
|
|
|
24301
24514
|
|
|
24302
24515
|
// src/program.ts
|
|
24303
24516
|
init_config();
|
|
24517
|
+
init_next_step();
|
|
24304
24518
|
init_ui();
|
|
24305
24519
|
init_brand();
|
|
24306
24520
|
|
|
@@ -24491,8 +24705,14 @@ async function run3(argv) {
|
|
|
24491
24705
|
process.stderr.isTTY === true ? "person" : cleaned[2] === void 0 ? "full" : "brief"
|
|
24492
24706
|
);
|
|
24493
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
|
+
});
|
|
24494
24714
|
await program.parseAsync(cleaned);
|
|
24495
|
-
if (
|
|
24715
|
+
if (ran !== "update") await backgroundUpdateNotice(ran === void 0 ? {} : { verb: ran });
|
|
24496
24716
|
}
|
|
24497
24717
|
async function handleError(err) {
|
|
24498
24718
|
const failure = adaptRegistryError(err) ?? err;
|
package/package.json
CHANGED