memgineering 0.25.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/assets/MEMGINEERING.md +1 -1
- package/assets/memgineering-rules/SKILL.md +14 -0
- package/assets/memgineering-setup/SKILL.md +11 -0
- package/dist/index.js +303 -54
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,55 @@ language the reader wants. The bilingual rule the monorepo applies to
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [0.25.1] — 2026-10-05
|
|
15
|
+
|
|
16
|
+
A security release. Upgrade if you use `setup --web`, or a hosted brain with a
|
|
17
|
+
chat-app connector.
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
|
|
21
|
+
- **`setup --web` no longer switches a machine to another account.** If the
|
|
22
|
+
setup screen is confirmed by a different account from the one this machine is
|
|
23
|
+
signed in to, nothing is written and the terminal says why. The setup screen
|
|
24
|
+
is only told "different account", never who is signed in here.
|
|
25
|
+
- **Notes chosen for upload on the setup screen wait for a check on this
|
|
26
|
+
machine.** A setup left running in the background uploads nothing. The next
|
|
27
|
+
command names the folder, the account that would receive it, and the `push`
|
|
28
|
+
that sends it. That command is quoted, so neither a folder name nor a brain
|
|
29
|
+
name can run as shell code. A setup in the foreground asks first. A `push` of
|
|
30
|
+
that folder, or `logout`, clears the notice.
|
|
31
|
+
- **The setup screen can link or upload only a folder this machine offered.**
|
|
32
|
+
It can create a new local brain only inside the home folder. A relative or
|
|
33
|
+
`~/` path is read against the home folder and created exactly where it was
|
|
34
|
+
checked, and symlinks are followed.
|
|
35
|
+
- **`setup --web --json` prints `setup_url` only when no browser opened**, with
|
|
36
|
+
a hint to use `--print-url` if the browser never appeared. A machine signing
|
|
37
|
+
in for the first time is told once which account it was connected to.
|
|
38
|
+
- **`login` tells the approval screen when and from what the sign-in came**
|
|
39
|
+
(`platform`, `client`). Its wording now says that screen asks the person to
|
|
40
|
+
type the code.
|
|
41
|
+
- **Rules first written through a chat-app connector say so.** `memgineering
|
|
42
|
+
rules` and the before-edit hook print `(written by the ChatGPT connector)`
|
|
43
|
+
next to such a rule, and `--json` carries `written_by`. A rule asked for from
|
|
44
|
+
a chat app is now held as a note until the person promotes it at a terminal.
|
|
45
|
+
The hold is enforced by the server.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- `revise --rule` with the claim a note already holds makes it a rule. It used
|
|
50
|
+
to be refused as "already says this", and nothing bound.
|
|
51
|
+
|
|
52
|
+
### Agent guidance
|
|
53
|
+
|
|
54
|
+
- `memgineering-setup` explains what the setup link gives whoever holds it, and
|
|
55
|
+
what happens to an upload chosen on the screen.
|
|
56
|
+
- `memgineering-rules` explains rules held from chat apps. Promoting one binds
|
|
57
|
+
the note's own text, so read it first with `memgineering open <id> --detail
|
|
58
|
+
full`; to word it differently, write a new rule with `remember --rule`. It
|
|
59
|
+
also explains the `written by` label.
|
|
60
|
+
|
|
61
|
+
Restart the agent session after upgrading so it reads the new guidance.
|
|
62
|
+
|
|
14
63
|
## [0.25.0] — 2026-10-04
|
|
15
64
|
|
|
16
65
|
### Added
|
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.1
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# memgineering
|
|
@@ -69,6 +69,20 @@ A hosted brain answers this too, and `--rule` writes to one land marked. The
|
|
|
69
69
|
marking lives in the note's own frontmatter either way, so a brain that was
|
|
70
70
|
pushed from a folder keeps every rule it had.
|
|
71
71
|
|
|
72
|
+
**A rule saved from a chat app waits for the user.** ChatGPT, Claude.ai and
|
|
73
|
+
other connectors can write notes, but not rules: a rule asked for there is
|
|
74
|
+
kept as an ordinary note, and binds only once the user promotes it at a
|
|
75
|
+
terminal. Promoting binds the note's own text as the chat wrote it — `--claim`
|
|
76
|
+
changes the summary, not the words that bind — so they read it first with
|
|
77
|
+
`memgineering open <id> --detail full` (the card alone shows only the title and
|
|
78
|
+
summary, which a chat app may have rewritten), then `memgineering revise <id> --rule --claim "<what
|
|
79
|
+
the note says>"`. Worded differently, it is a new rule: `memgineering remember
|
|
80
|
+
"<the rule>" --rule`. A connector also cannot change or unmark an existing rule. Once
|
|
81
|
+
promoted, the rule says where its words came from — `(written by the ChatGPT
|
|
82
|
+
connector)` beside it in `memgineering rules` and in the before-edit hook, and
|
|
83
|
+
`written_by` in `--json`. Treat it as their rule; the label says which surface
|
|
84
|
+
it was first said on.
|
|
85
|
+
|
|
72
86
|
**A rule also wins a tie in `recall` now.** Where an ordinary memory and a
|
|
73
87
|
standing decision matched a query equally well, the decision is ranked first —
|
|
74
88
|
the asymmetry being that a missed fact costs the reader some context, while a
|
|
@@ -66,6 +66,17 @@ Three things to do after running it:
|
|
|
66
66
|
picks it up on its own when they do. There is nothing to poll and nothing
|
|
67
67
|
to wait for.
|
|
68
68
|
|
|
69
|
+
The link is the key to that screen, not the code: whoever holds the whole link
|
|
70
|
+
can drive it. Give it only to them, never paste it anywhere else — with
|
|
71
|
+
`--json`, `setup_url` is printed only when no browser opened.
|
|
72
|
+
|
|
73
|
+
If they chose to upload existing notes on the screen, this machine does NOT
|
|
74
|
+
upload them on its own: the next `memgineering` command shows which folder and
|
|
75
|
+
which account would receive them, with the `push` that sends them. Check with
|
|
76
|
+
them that the account is theirs before running it. And a machine already
|
|
77
|
+
signed in to a different account is never switched by `setup --web` — the
|
|
78
|
+
terminal says so; `logout` first if they really mean to switch.
|
|
79
|
+
|
|
69
80
|
The screen asks everything `--agent` takes from flags — sign in, where the
|
|
70
81
|
memory lives, which brain, which tools, auto-update, the session hook — with
|
|
71
82
|
the recommended answer pre-selected. A person who is already signed in to
|
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: [] };
|
|
@@ -4030,6 +4030,7 @@ function saysNothingNew(entry, input, normalize) {
|
|
|
4030
4030
|
if (norm(claim.text) !== norm(previousClaim(entry))) return false;
|
|
4031
4031
|
if (claim.summary != null && norm(claim.summary) !== norm(entry.memory.summary)) return false;
|
|
4032
4032
|
if (claim.title != null && norm(claim.title) !== norm(entry.memory.title)) return false;
|
|
4033
|
+
if (claim.binding != null && claim.binding !== (entry.memory.binding === true)) return false;
|
|
4033
4034
|
const declared = new Set(
|
|
4034
4035
|
entry.relations.filter((r) => r.type === "contradicts").map((r) => r.to)
|
|
4035
4036
|
);
|
|
@@ -5137,7 +5138,7 @@ async function apiRequest(args) {
|
|
|
5137
5138
|
if (cause.retryable && attempts < MAX_CONNECT_RETRIES) {
|
|
5138
5139
|
const backoff = args.backoffMs ?? CONNECT_BACKOFF_MS;
|
|
5139
5140
|
const wait = backoff[Math.min(attempts, backoff.length - 1)] ?? 0;
|
|
5140
|
-
await new Promise((
|
|
5141
|
+
await new Promise((resolve17) => setTimeout(resolve17, wait));
|
|
5141
5142
|
return apiRequest({ ...args, connectRetries: attempts + 1 });
|
|
5142
5143
|
}
|
|
5143
5144
|
throw memgError(
|
|
@@ -5161,7 +5162,7 @@ async function apiRequest(args) {
|
|
|
5161
5162
|
if (res.status === 429 && (args.retries ?? MAX_RATE_LIMIT_RETRIES) > 0) {
|
|
5162
5163
|
const wait = retryAfterMs(res.headers.get("retry-after"));
|
|
5163
5164
|
args.onWait?.(wait);
|
|
5164
|
-
await new Promise((
|
|
5165
|
+
await new Promise((resolve17) => setTimeout(resolve17, wait));
|
|
5165
5166
|
return apiRequest({ ...args, retries: (args.retries ?? MAX_RATE_LIMIT_RETRIES) - 1 });
|
|
5166
5167
|
}
|
|
5167
5168
|
const envelope = parsed?.error;
|
|
@@ -5348,6 +5349,25 @@ function findBrain(cfg, root) {
|
|
|
5348
5349
|
const target = resolve(root);
|
|
5349
5350
|
return cfg.brain.brains.find((b) => resolve(b.root) === target);
|
|
5350
5351
|
}
|
|
5352
|
+
async function clearSetupFollowUps(onlyPath) {
|
|
5353
|
+
try {
|
|
5354
|
+
const cfg = await loadConfig();
|
|
5355
|
+
const pending = cfg.system.pendingUpload;
|
|
5356
|
+
const dropUpload = pending !== void 0 && (onlyPath === void 0 || resolve(pending.path) === resolve(onlyPath));
|
|
5357
|
+
const dropConnected = onlyPath === void 0 && cfg.system.setupConnected !== void 0;
|
|
5358
|
+
if (!dropUpload && !dropConnected) return;
|
|
5359
|
+
const { pendingUpload, setupConnected, ...rest } = cfg.system;
|
|
5360
|
+
await saveConfig({
|
|
5361
|
+
...cfg,
|
|
5362
|
+
system: {
|
|
5363
|
+
...rest,
|
|
5364
|
+
...dropUpload ? {} : pendingUpload ? { pendingUpload } : {},
|
|
5365
|
+
...dropConnected ? {} : setupConnected ? { setupConnected } : {}
|
|
5366
|
+
}
|
|
5367
|
+
});
|
|
5368
|
+
} catch {
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5351
5371
|
var BrainSchema, ConfigSchema, EMPTY_CONFIG;
|
|
5352
5372
|
var init_config = __esm({
|
|
5353
5373
|
"src/lib/config.ts"() {
|
|
@@ -5486,7 +5506,31 @@ var init_config = __esm({
|
|
|
5486
5506
|
* Absent means "written before this field existed", which is the same
|
|
5487
5507
|
* answer as "older than this CLI" and wants the same one command.
|
|
5488
5508
|
*/
|
|
5489
|
-
guidanceVersion: z8.string().optional()
|
|
5509
|
+
guidanceVersion: z8.string().optional(),
|
|
5510
|
+
/**
|
|
5511
|
+
* Notes `setup --web` was asked to upload, not yet sent.
|
|
5512
|
+
*
|
|
5513
|
+
* The detached half of the setup never uploads on its own (security
|
|
5514
|
+
* audit H-3): whoever confirmed the setup decided which account receives
|
|
5515
|
+
* the notes, and that is checked by somebody on THIS machine first. The
|
|
5516
|
+
* next command's notice names the path and the receiving account; a
|
|
5517
|
+
* `push` of that path, or `logout`, clears it.
|
|
5518
|
+
*/
|
|
5519
|
+
pendingUpload: z8.object({
|
|
5520
|
+
path: z8.string().min(1),
|
|
5521
|
+
brain: z8.string().nullable(),
|
|
5522
|
+
/** The notice names the brain by id only: its name came from the screen. */
|
|
5523
|
+
brain_id: z8.string().nullable().optional(),
|
|
5524
|
+
account_id: z8.string().min(1),
|
|
5525
|
+
account_email: z8.string().nullable(),
|
|
5526
|
+
at: z8.string().min(1)
|
|
5527
|
+
}).optional().catch(void 0),
|
|
5528
|
+
/**
|
|
5529
|
+
* The account a `setup --web` signed this machine into, when it had no
|
|
5530
|
+
* sign-in before — shown once by the next command, because there was no
|
|
5531
|
+
* earlier account to compare it with (H-3).
|
|
5532
|
+
*/
|
|
5533
|
+
setupConnected: z8.object({ account_id: z8.string().min(1), email: z8.string().nullable(), at: z8.string().min(1) }).optional().catch(void 0)
|
|
5490
5534
|
}).default({ autoUpdate: "on" })
|
|
5491
5535
|
});
|
|
5492
5536
|
EMPTY_CONFIG = ConfigSchema.parse({});
|
|
@@ -5503,6 +5547,10 @@ async function openCloudBrain(target) {
|
|
|
5503
5547
|
apiUrl: target.apiUrl
|
|
5504
5548
|
};
|
|
5505
5549
|
}
|
|
5550
|
+
function connectorLabel(writtenBy) {
|
|
5551
|
+
const app = writtenBy.replace(/^mcp:/, "").replace(/[^A-Za-z0-9 ._-]/g, "").replace(/\s+/g, " ").trim().slice(0, 32).trim();
|
|
5552
|
+
return app === "" || app.toLowerCase() === "connector" ? "written by a chat connector" : `written by the ${app} connector`;
|
|
5553
|
+
}
|
|
5506
5554
|
function findCloudBrain(brains, named) {
|
|
5507
5555
|
return brains.find((brain) => brain.id === named) ?? brains.find((brain) => brain.name === named);
|
|
5508
5556
|
}
|
|
@@ -7054,12 +7102,12 @@ var init_deadline = __esm({
|
|
|
7054
7102
|
import { spawn } from "child_process";
|
|
7055
7103
|
async function openBrowser(url) {
|
|
7056
7104
|
const { file, args } = browserOpener(url);
|
|
7057
|
-
return new Promise((
|
|
7105
|
+
return new Promise((resolve17) => {
|
|
7058
7106
|
let settled = false;
|
|
7059
7107
|
const finish = (opened) => {
|
|
7060
7108
|
if (settled) return;
|
|
7061
7109
|
settled = true;
|
|
7062
|
-
|
|
7110
|
+
resolve17(opened);
|
|
7063
7111
|
};
|
|
7064
7112
|
const child = spawn(file, args, { detached: true, stdio: "ignore" });
|
|
7065
7113
|
child.on("error", () => finish(false));
|
|
@@ -7089,7 +7137,7 @@ function loginCommand() {
|
|
|
7089
7137
|
"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
7138
|
).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
7139
|
"after",
|
|
7092
|
-
"\nAgents: run `login --emit-only`, then SHOW the code in your reply. The\napproval page asks the user to
|
|
7140
|
+
"\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
7141
|
).action(async (opts) => {
|
|
7094
7142
|
const base2 = apiUrl();
|
|
7095
7143
|
const already = await currentCredentials(base2);
|
|
@@ -7140,7 +7188,11 @@ async function startPairing(base2) {
|
|
|
7140
7188
|
const res = await apiRequest({
|
|
7141
7189
|
method: "POST",
|
|
7142
7190
|
path: "/v1/auth/device/code",
|
|
7143
|
-
baseUrl: base2
|
|
7191
|
+
baseUrl: base2,
|
|
7192
|
+
// Display text for the approval screen — "requested from memgineering on
|
|
7193
|
+
// darwin" — so a person can tell their own terminal's request from a link
|
|
7194
|
+
// somebody sent them (security audit H-2). Nothing identifying.
|
|
7195
|
+
body: { platform: process.platform, client: `memgineering/${VERSION}` }
|
|
7144
7196
|
});
|
|
7145
7197
|
const { device_code, user_code, verification_url, expires_in } = res.body ?? {};
|
|
7146
7198
|
if (!device_code || !user_code || !verification_url) {
|
|
@@ -7171,10 +7223,10 @@ function emit(pending, ctx) {
|
|
|
7171
7223
|
browser_opened: ctx.opened,
|
|
7172
7224
|
api_url: ctx.base,
|
|
7173
7225
|
waiting: ctx.blocking,
|
|
7174
|
-
next: ctx.blocking ? "Waiting for approval in the browser." : "SHOW user_code to the user in your reply
|
|
7226
|
+
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
7227
|
},
|
|
7176
7228
|
human: () => {
|
|
7177
|
-
printHuman("Show this code to whoever is signing in:\n");
|
|
7229
|
+
printHuman("Show this code to whoever is signing in \u2014 the approval page asks them to type it:\n");
|
|
7178
7230
|
printHuman(` ${c.bold().yellow(oneLine(pending.user_code, WIDTH.handle))}
|
|
7179
7231
|
`);
|
|
7180
7232
|
if (ctx.opened) {
|
|
@@ -7267,9 +7319,10 @@ var init_login = __esm({
|
|
|
7267
7319
|
init_open_browser();
|
|
7268
7320
|
init_pending_login();
|
|
7269
7321
|
init_ui();
|
|
7322
|
+
init_version();
|
|
7270
7323
|
BLOCKING_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
7271
|
-
sleep = (ms) => new Promise((
|
|
7272
|
-
setTimeout(
|
|
7324
|
+
sleep = (ms) => new Promise((resolve17) => {
|
|
7325
|
+
setTimeout(resolve17, ms).unref?.();
|
|
7273
7326
|
});
|
|
7274
7327
|
}
|
|
7275
7328
|
});
|
|
@@ -7380,6 +7433,7 @@ If your notes are in one of those, that rule is why \u2014 nothing here can see
|
|
|
7380
7433
|
}
|
|
7381
7434
|
}
|
|
7382
7435
|
const repointed = await pointConfigAt(destination);
|
|
7436
|
+
await clearSetupFollowUps(local.root);
|
|
7383
7437
|
printDual({
|
|
7384
7438
|
json: {
|
|
7385
7439
|
from: local.root,
|
|
@@ -7911,15 +7965,18 @@ var setup_cloud_exports = {};
|
|
|
7911
7965
|
__export(setup_cloud_exports, {
|
|
7912
7966
|
SETUP_CHILD_ENV: () => SETUP_CHILD_ENV,
|
|
7913
7967
|
applyAnswers: () => applyAnswers,
|
|
7968
|
+
insideFolder: () => insideFolder,
|
|
7969
|
+
newBrainRoot: () => newBrainRoot,
|
|
7914
7970
|
readChildToken: () => readChildToken,
|
|
7915
7971
|
runSetupCloud: () => runSetupCloud,
|
|
7916
7972
|
runSetupCloudChild: () => runSetupCloudChild,
|
|
7917
7973
|
waitAndApply: () => waitAndApply
|
|
7918
7974
|
});
|
|
7919
7975
|
import { spawn as spawn2 } from "child_process";
|
|
7976
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
7920
7977
|
import { readdir as readdir7, stat as stat6 } from "fs/promises";
|
|
7921
7978
|
import { homedir as homedir5 } from "os";
|
|
7922
|
-
import { join as join24 } from "path";
|
|
7979
|
+
import { basename as basename3, dirname as dirname9, join as join24, resolve as resolve15, sep as sep4 } from "path";
|
|
7923
7980
|
async function report2(setupToken, state, detail) {
|
|
7924
7981
|
try {
|
|
7925
7982
|
await apiRequest({
|
|
@@ -7963,7 +8020,7 @@ function toolsFromAnswers(answers, detected) {
|
|
|
7963
8020
|
const picked = TOOLS.filter((t) => wanted.has(t.id));
|
|
7964
8021
|
return picked.length > 0 ? picked : [...detected];
|
|
7965
8022
|
}
|
|
7966
|
-
async function applyAnswers(setupToken, claim, detected) {
|
|
8023
|
+
async function applyAnswers(setupToken, claim, detected, guards = { candidates: [], confirmUpload: null }) {
|
|
7967
8024
|
const answers = claim.answers;
|
|
7968
8025
|
if (!answers) {
|
|
7969
8026
|
throw memgError(
|
|
@@ -7972,22 +8029,39 @@ async function applyAnswers(setupToken, claim, detected) {
|
|
|
7972
8029
|
"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
8030
|
);
|
|
7974
8031
|
}
|
|
8032
|
+
const localRoot = checkLocalPath(answers, guards.candidates);
|
|
8033
|
+
let account = null;
|
|
7975
8034
|
if (typeof claim.token === "string" && claim.token !== "") {
|
|
8035
|
+
account = claim.account ?? await whoIs(claim.token);
|
|
8036
|
+
const before = await readCredentials();
|
|
8037
|
+
const firstSignIn = before === null || before.api_url !== apiUrl();
|
|
8038
|
+
if (!firstSignIn && (account === null || before.account_id !== account.id)) {
|
|
8039
|
+
throw memgError(
|
|
8040
|
+
"conflict",
|
|
8041
|
+
"this machine is signed in to a different account, so the setup changed nothing",
|
|
8042
|
+
`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.`
|
|
8043
|
+
);
|
|
8044
|
+
}
|
|
7976
8045
|
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
8046
|
const credentials = {
|
|
7983
8047
|
api_url: apiUrl(),
|
|
7984
8048
|
token: claim.token,
|
|
7985
|
-
account_id:
|
|
7986
|
-
display_name:
|
|
8049
|
+
account_id: account?.id ?? "",
|
|
8050
|
+
display_name: account?.display_name ?? null,
|
|
7987
8051
|
signed_in_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7988
8052
|
...registry !== void 0 ? { registry } : {}
|
|
7989
8053
|
};
|
|
7990
8054
|
await saveCredentials(credentials);
|
|
8055
|
+
if (firstSignIn && account !== null) {
|
|
8056
|
+
const cfg = await loadConfig();
|
|
8057
|
+
await saveConfig({
|
|
8058
|
+
...cfg,
|
|
8059
|
+
system: {
|
|
8060
|
+
...cfg.system,
|
|
8061
|
+
setupConnected: { account_id: account.id, email: account.email, at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
8062
|
+
}
|
|
8063
|
+
});
|
|
8064
|
+
}
|
|
7991
8065
|
}
|
|
7992
8066
|
await report2(setupToken, "applying", "\uC124\uC815\uC744 \uC4F0\uB294 \uC911\uC774\uC5D0\uC694.");
|
|
7993
8067
|
const choice = {
|
|
@@ -8005,7 +8079,7 @@ async function applyAnswers(setupToken, claim, detected) {
|
|
|
8005
8079
|
await recordLink(target.adapter, DEFAULT_OBSERVATIONS_DIR);
|
|
8006
8080
|
parts.push(`linked ${answers.local_path}`);
|
|
8007
8081
|
} else {
|
|
8008
|
-
const made = await createBrain(answers.local_path, "default");
|
|
8082
|
+
const made = await createBrain(localRoot ?? answers.local_path, "default");
|
|
8009
8083
|
parts.push(`created ${made.root}`);
|
|
8010
8084
|
}
|
|
8011
8085
|
}
|
|
@@ -8015,16 +8089,77 @@ async function applyAnswers(setupToken, claim, detected) {
|
|
|
8015
8089
|
parts.push(`pointed at ${claim.brain.name}`);
|
|
8016
8090
|
}
|
|
8017
8091
|
if (answers.location === "cloud" && answers.upload_path) {
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8092
|
+
const path = answers.upload_path;
|
|
8093
|
+
if (!guards.candidates.includes(path)) {
|
|
8094
|
+
parts.push(`not uploaded: ${path} was not one of the folders this machine offered`);
|
|
8095
|
+
} else {
|
|
8096
|
+
const email = account?.email ?? null;
|
|
8097
|
+
const yes = guards.confirmUpload ? await guards.confirmUpload({ path, email }) : null;
|
|
8098
|
+
if (yes === true) {
|
|
8099
|
+
await report2(setupToken, "applying", "\uB178\uD2B8\uB97C \uC62C\uB9AC\uB294 \uC911\uC774\uC5D0\uC694.");
|
|
8100
|
+
const { pushCommand: pushCommand2 } = await Promise.resolve().then(() => (init_push(), push_exports));
|
|
8101
|
+
await pushCommand2().parseAsync(
|
|
8102
|
+
["--vault", path, ...claim.brain?.id ? ["--brain", claim.brain.id] : []],
|
|
8103
|
+
{ from: "user" }
|
|
8104
|
+
);
|
|
8105
|
+
parts.push(`uploaded ${path}`);
|
|
8106
|
+
} else if (yes === null && account !== null) {
|
|
8107
|
+
const cfg = await loadConfig();
|
|
8108
|
+
await saveConfig({
|
|
8109
|
+
...cfg,
|
|
8110
|
+
system: {
|
|
8111
|
+
...cfg.system,
|
|
8112
|
+
pendingUpload: {
|
|
8113
|
+
path,
|
|
8114
|
+
brain: claim.brain?.name ?? null,
|
|
8115
|
+
brain_id: claim.brain?.id ?? null,
|
|
8116
|
+
account_id: account.id,
|
|
8117
|
+
account_email: email,
|
|
8118
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8119
|
+
}
|
|
8120
|
+
}
|
|
8121
|
+
});
|
|
8122
|
+
await report2(setupToken, "applying", "\uB178\uD2B8 \uC62C\uB9AC\uAE30\uB294 \uC774 \uCEF4\uD4E8\uD130\uC5D0\uC11C \uD655\uC778\uD55C \uB4A4\uC5D0 \uC2DC\uC791\uB3FC\uC694.");
|
|
8123
|
+
parts.push(`upload of ${path} waits for a check on this machine`);
|
|
8124
|
+
} else {
|
|
8125
|
+
parts.push(`not uploaded: ${path}`);
|
|
8126
|
+
}
|
|
8127
|
+
}
|
|
8025
8128
|
}
|
|
8129
|
+
if (account?.email) parts.push(`signed in as ${account.email}`);
|
|
8026
8130
|
return { summary: parts.join(" \xB7 ") };
|
|
8027
8131
|
}
|
|
8132
|
+
async function whoIs(token) {
|
|
8133
|
+
const me = await apiRequest({ method: "GET", path: "/v1/me", token }).catch(() => null);
|
|
8134
|
+
const a = me?.body?.account;
|
|
8135
|
+
return a?.id ? { id: a.id, email: a.email ?? null, display_name: a.display_name ?? null } : null;
|
|
8136
|
+
}
|
|
8137
|
+
function checkLocalPath(answers, candidates) {
|
|
8138
|
+
if (answers.location !== "local" || !answers.local_path) return null;
|
|
8139
|
+
const path = answers.local_path;
|
|
8140
|
+
if (answers.local_mode === "link") {
|
|
8141
|
+
if (!candidates.includes(path)) {
|
|
8142
|
+
throw memgError(
|
|
8143
|
+
"invalid_input",
|
|
8144
|
+
"the folder the setup screen chose to link was not one this machine offered",
|
|
8145
|
+
"Nothing was changed. To use an existing folder, run `memgineering link <folder>` here."
|
|
8146
|
+
);
|
|
8147
|
+
}
|
|
8148
|
+
return null;
|
|
8149
|
+
}
|
|
8150
|
+
return newBrainRoot(path);
|
|
8151
|
+
}
|
|
8152
|
+
function newBrainRoot(path, home = homedir5()) {
|
|
8153
|
+
const root = path === "~" ? home : path.startsWith("~/") ? resolve15(home, path.slice(2)) : resolve15(home, path);
|
|
8154
|
+
if (!insideFolder(root, home)) {
|
|
8155
|
+
throw memgError(
|
|
8156
|
+
"invalid_input",
|
|
8157
|
+
"the setup screen chose a place outside the home folder for the new brain",
|
|
8158
|
+
`Nothing was changed. Run \`memgineering init <folder>\` here to make a brain somewhere under ${home}.`
|
|
8159
|
+
);
|
|
8160
|
+
}
|
|
8161
|
+
return root;
|
|
8162
|
+
}
|
|
8028
8163
|
async function pollOnce(setupToken) {
|
|
8029
8164
|
const res = await apiRequest({
|
|
8030
8165
|
method: "POST",
|
|
@@ -8036,6 +8171,7 @@ async function pollOnce(setupToken) {
|
|
|
8036
8171
|
}
|
|
8037
8172
|
async function waitAndApply(setupToken, opts) {
|
|
8038
8173
|
let sentFolders = false;
|
|
8174
|
+
let offered = [];
|
|
8039
8175
|
for (; ; ) {
|
|
8040
8176
|
if (Date.now() > opts.deadline) return "expired";
|
|
8041
8177
|
let answer = null;
|
|
@@ -8049,7 +8185,10 @@ async function waitAndApply(setupToken, opts) {
|
|
|
8049
8185
|
if (answer) {
|
|
8050
8186
|
await report2(setupToken, "applying");
|
|
8051
8187
|
try {
|
|
8052
|
-
const done = await applyAnswers(setupToken, answer, opts.detected
|
|
8188
|
+
const done = await applyAnswers(setupToken, answer, opts.detected, {
|
|
8189
|
+
candidates: offered,
|
|
8190
|
+
confirmUpload: opts.confirmUpload ?? null
|
|
8191
|
+
});
|
|
8053
8192
|
await report2(setupToken, "done", "\uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694.");
|
|
8054
8193
|
return done;
|
|
8055
8194
|
} catch (err) {
|
|
@@ -8068,6 +8207,7 @@ async function waitAndApply(setupToken, opts) {
|
|
|
8068
8207
|
sentFolders = true;
|
|
8069
8208
|
opts.onProgress?.("\uD3F4\uB354\uB97C \uCC3E\uC544\uBCF4\uB294 \uC911\u2026");
|
|
8070
8209
|
const candidates = await folderCandidates();
|
|
8210
|
+
offered = candidates.map((c2) => c2.path);
|
|
8071
8211
|
await apiRequest({
|
|
8072
8212
|
method: "POST",
|
|
8073
8213
|
path: "/v1/setup/session/folders",
|
|
@@ -8076,7 +8216,7 @@ async function waitAndApply(setupToken, opts) {
|
|
|
8076
8216
|
});
|
|
8077
8217
|
}
|
|
8078
8218
|
}
|
|
8079
|
-
await new Promise((
|
|
8219
|
+
await new Promise((resolve17) => setTimeout(resolve17, opts.intervalMs));
|
|
8080
8220
|
}
|
|
8081
8221
|
}
|
|
8082
8222
|
async function startSession(detected) {
|
|
@@ -8122,7 +8262,11 @@ async function runSetupCloud(opts) {
|
|
|
8122
8262
|
printDual({
|
|
8123
8263
|
json: {
|
|
8124
8264
|
user_code: session.user_code,
|
|
8125
|
-
|
|
8265
|
+
// The link drives the setup screen — it carries the browser key (H-3).
|
|
8266
|
+
// Once a browser has it, nobody else needs it, so it is not printed.
|
|
8267
|
+
...opened ? {
|
|
8268
|
+
hint: "The setup screen was opened in the browser. If none appeared, run `memgineering setup --web --print-url` and open the printed link yourself."
|
|
8269
|
+
} : { setup_url: session.setup_url },
|
|
8126
8270
|
opened,
|
|
8127
8271
|
waiting: foreground
|
|
8128
8272
|
},
|
|
@@ -8143,11 +8287,13 @@ async function runSetupCloud(opts) {
|
|
|
8143
8287
|
detachChild(session.setup_token);
|
|
8144
8288
|
return;
|
|
8145
8289
|
}
|
|
8290
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true && !isJsonMode();
|
|
8146
8291
|
const outcome = await waitAndApply(session.setup_token, {
|
|
8147
8292
|
intervalMs,
|
|
8148
8293
|
deadline: Date.now() + CHILD_LIFETIME_MS,
|
|
8149
8294
|
detected: opts.detected,
|
|
8150
|
-
onProgress: (line) => printHuman(c.gray(` ${line}`))
|
|
8295
|
+
onProgress: (line) => printHuman(c.gray(` ${line}`)),
|
|
8296
|
+
confirmUpload: interactive ? askBeforeUpload : null
|
|
8151
8297
|
});
|
|
8152
8298
|
if (outcome === "expired") {
|
|
8153
8299
|
const line = "\uC124\uCE58 \uD654\uBA74\uC774 \uB9CC\uB8CC\uB410\uC5B4\uC694. \uC544\uBB34\uAC83\uB3C4 \uBC14\uB00C\uC9C0 \uC54A\uC558\uC5B4\uC694 \u2014 `memgineering setup --web`\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 \uC0C8 \uD654\uBA74\uC774 \uC5F4\uB824\uC694.";
|
|
@@ -8166,6 +8312,41 @@ ${line}`));
|
|
|
8166
8312
|
);
|
|
8167
8313
|
}
|
|
8168
8314
|
}
|
|
8315
|
+
function insideFolder(target, folder) {
|
|
8316
|
+
const real = (p) => {
|
|
8317
|
+
try {
|
|
8318
|
+
return realpathSync2(p);
|
|
8319
|
+
} catch {
|
|
8320
|
+
return p;
|
|
8321
|
+
}
|
|
8322
|
+
};
|
|
8323
|
+
const root = real(resolve15(folder));
|
|
8324
|
+
let existing = resolve15(target);
|
|
8325
|
+
const rest = [];
|
|
8326
|
+
for (; ; ) {
|
|
8327
|
+
try {
|
|
8328
|
+
realpathSync2(existing);
|
|
8329
|
+
break;
|
|
8330
|
+
} catch {
|
|
8331
|
+
const parent = dirname9(existing);
|
|
8332
|
+
if (parent === existing) break;
|
|
8333
|
+
rest.unshift(basename3(existing));
|
|
8334
|
+
existing = parent;
|
|
8335
|
+
}
|
|
8336
|
+
}
|
|
8337
|
+
const landed = resolve15(real(existing), ...rest);
|
|
8338
|
+
return landed === root || landed.startsWith(root + sep4);
|
|
8339
|
+
}
|
|
8340
|
+
async function askBeforeUpload(q) {
|
|
8341
|
+
const { default: prompts6 } = await import("prompts");
|
|
8342
|
+
const { yes } = await prompts6({
|
|
8343
|
+
type: "confirm",
|
|
8344
|
+
name: "yes",
|
|
8345
|
+
message: `${q.path} \uC758 \uB178\uD2B8\uB97C ${q.email ?? "\uC774 \uACC4\uC815"} \uBE0C\uB808\uC778\uC73C\uB85C \uC62C\uB9B4\uAE4C\uC694?`,
|
|
8346
|
+
initial: false
|
|
8347
|
+
});
|
|
8348
|
+
return yes === true;
|
|
8349
|
+
}
|
|
8169
8350
|
async function runSetupCloudChild(detected) {
|
|
8170
8351
|
const setupToken = await readChildToken();
|
|
8171
8352
|
if (setupToken === "") return;
|
|
@@ -8182,6 +8363,7 @@ var init_setup_cloud = __esm({
|
|
|
8182
8363
|
"use strict";
|
|
8183
8364
|
init_api_client();
|
|
8184
8365
|
init_errors();
|
|
8366
|
+
init_config();
|
|
8185
8367
|
init_credentials();
|
|
8186
8368
|
init_open_browser();
|
|
8187
8369
|
init_ui();
|
|
@@ -8196,7 +8378,7 @@ var init_setup_cloud = __esm({
|
|
|
8196
8378
|
// src/commands/setup.ts
|
|
8197
8379
|
import { createHash as createHash9 } from "crypto";
|
|
8198
8380
|
import { access as access2, mkdir as mkdir15, readFile as readFile24 } from "fs/promises";
|
|
8199
|
-
import { dirname as
|
|
8381
|
+
import { dirname as dirname10, join as join25 } from "path";
|
|
8200
8382
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8201
8383
|
import { Command as Command40 } from "commander";
|
|
8202
8384
|
import prompts5 from "prompts";
|
|
@@ -8780,7 +8962,7 @@ ${BRAND.hubMarkerEnd}`;
|
|
|
8780
8962
|
const existing = await readFile24(filePath, "utf8").catch(() => null);
|
|
8781
8963
|
if (existing === null) {
|
|
8782
8964
|
if (!dryRun) {
|
|
8783
|
-
await mkdir15(
|
|
8965
|
+
await mkdir15(dirname10(filePath), { recursive: true });
|
|
8784
8966
|
await writeThenRename(filePath, `${block}
|
|
8785
8967
|
`);
|
|
8786
8968
|
}
|
|
@@ -8824,7 +9006,7 @@ async function writeIfDifferent(filePath, content, dryRun) {
|
|
|
8824
9006
|
const after = fingerprint(content);
|
|
8825
9007
|
if (existing === content) return { outcome: "unchanged", before: existing, after };
|
|
8826
9008
|
if (!dryRun) {
|
|
8827
|
-
await mkdir15(
|
|
9009
|
+
await mkdir15(dirname10(filePath), { recursive: true });
|
|
8828
9010
|
await writeThenRename(filePath, content);
|
|
8829
9011
|
}
|
|
8830
9012
|
return { outcome: existing === null ? "injected" : "updated", before: existing, after };
|
|
@@ -8878,7 +9060,7 @@ async function installHook(spec, tool, dryRun) {
|
|
|
8878
9060
|
}
|
|
8879
9061
|
const next = { ...settings, hooks: { ...hooks, [spec.event]: existingGroups } };
|
|
8880
9062
|
if (!dryRun) {
|
|
8881
|
-
await mkdir15(
|
|
9063
|
+
await mkdir15(dirname10(settingsPath), { recursive: true });
|
|
8882
9064
|
await writeThenRename(settingsPath, `${JSON.stringify(next, null, 2)}
|
|
8883
9065
|
`);
|
|
8884
9066
|
}
|
|
@@ -8918,14 +9100,14 @@ async function loadAsset(relPath) {
|
|
|
8918
9100
|
return raw;
|
|
8919
9101
|
}
|
|
8920
9102
|
async function assetRoot() {
|
|
8921
|
-
const here =
|
|
9103
|
+
const here = dirname10(fileURLToPath3(import.meta.url));
|
|
8922
9104
|
const bundled = join25(here, "..", "assets");
|
|
8923
9105
|
if (await exists2(join25(bundled, BRAND.hubFileName))) return bundled;
|
|
8924
9106
|
let dir = here;
|
|
8925
9107
|
for (; ; ) {
|
|
8926
9108
|
const candidate = join25(dir, "packages", "skills");
|
|
8927
9109
|
if (await exists2(join25(candidate, BRAND.hubFileName))) return candidate;
|
|
8928
|
-
const parent =
|
|
9110
|
+
const parent = dirname10(dir);
|
|
8929
9111
|
if (parent === dir) break;
|
|
8930
9112
|
dir = parent;
|
|
8931
9113
|
}
|
|
@@ -9312,7 +9494,7 @@ async function reportError(kind, err) {
|
|
|
9312
9494
|
headers: { "content-type": "application/json" },
|
|
9313
9495
|
body: JSON.stringify(payload)
|
|
9314
9496
|
}),
|
|
9315
|
-
new Promise((
|
|
9497
|
+
new Promise((resolve17) => setTimeout(resolve17, REPORT_TIMEOUT_MS))
|
|
9316
9498
|
]);
|
|
9317
9499
|
} catch {
|
|
9318
9500
|
}
|
|
@@ -11003,7 +11185,7 @@ var KordisClient = class {
|
|
|
11003
11185
|
`[kordis] api: ${method} ${url.pathname}${url.search} \u2192 fetch failed, retrying in 150ms (${err.message})`
|
|
11004
11186
|
);
|
|
11005
11187
|
}
|
|
11006
|
-
await new Promise((
|
|
11188
|
+
await new Promise((resolve17) => setTimeout(resolve17, 150));
|
|
11007
11189
|
continue;
|
|
11008
11190
|
}
|
|
11009
11191
|
}
|
|
@@ -14507,11 +14689,11 @@ function lines(text, marker) {
|
|
|
14507
14689
|
return text === "" ? [] : text.split("\n").map((l) => `${marker} ${l}`);
|
|
14508
14690
|
}
|
|
14509
14691
|
async function readInput(source) {
|
|
14510
|
-
const raw = source === "-" ? await new Promise((
|
|
14692
|
+
const raw = source === "-" ? await new Promise((resolve17, reject) => {
|
|
14511
14693
|
let buf = "";
|
|
14512
14694
|
process.stdin.setEncoding("utf8");
|
|
14513
14695
|
process.stdin.on("data", (chunk) => buf += chunk);
|
|
14514
|
-
process.stdin.on("end", () =>
|
|
14696
|
+
process.stdin.on("end", () => resolve17(buf));
|
|
14515
14697
|
process.stdin.on("error", reject);
|
|
14516
14698
|
}) : await readFile16(source, "utf8").catch(() => source);
|
|
14517
14699
|
try {
|
|
@@ -17174,8 +17356,14 @@ function groupHostedLines(rules, project) {
|
|
|
17174
17356
|
const width = ruleWidthFor(rules.length);
|
|
17175
17357
|
const line = (r) => {
|
|
17176
17358
|
const quoted = renderRuleLine(r.text ?? r.summary ?? r.title, width);
|
|
17177
|
-
|
|
17178
|
-
|
|
17359
|
+
const labels = [
|
|
17360
|
+
...r.freshness === "current" || r.freshness === void 0 ? [] : [`(${r.freshness === "expired" ? "past its end date" : "superseded"} \u2014 worth checking)`],
|
|
17361
|
+
// Whose words these are, when a chat app's (security audit M-2). The
|
|
17362
|
+
// person promoted it; the agent is owed knowing where it came from.
|
|
17363
|
+
...r.written_by === void 0 ? [] : [`(${connectorLabel(r.written_by)})`]
|
|
17364
|
+
];
|
|
17365
|
+
return labels.length === 0 ? quoted : `${quoted}
|
|
17366
|
+
${labels.join(" ")}`;
|
|
17179
17367
|
};
|
|
17180
17368
|
const globals = rules.filter((r) => r.lane === "global");
|
|
17181
17369
|
const scoped = rules.filter((r) => r.lane === "project");
|
|
@@ -17896,6 +18084,7 @@ init_login();
|
|
|
17896
18084
|
init_errors();
|
|
17897
18085
|
init_api_client();
|
|
17898
18086
|
init_credentials();
|
|
18087
|
+
init_config();
|
|
17899
18088
|
init_pending_login();
|
|
17900
18089
|
init_ui();
|
|
17901
18090
|
import { Command as Command22 } from "commander";
|
|
@@ -17907,6 +18096,7 @@ function logoutCommand() {
|
|
|
17907
18096
|
const base2 = apiUrl();
|
|
17908
18097
|
const credentials = await readCredentials();
|
|
17909
18098
|
await clearPendingLogin();
|
|
18099
|
+
await clearSetupFollowUps();
|
|
17910
18100
|
if (!credentials) {
|
|
17911
18101
|
printDual({
|
|
17912
18102
|
json: { signed_out: true, was_signed_in: false, api_url: base2 },
|
|
@@ -22716,7 +22906,7 @@ function rulesCommand() {
|
|
|
22716
22906
|
const lane = rule.lane === "project" ? oneLine(project2 ?? "this project", WIDTH.title) : "global";
|
|
22717
22907
|
printHuman(
|
|
22718
22908
|
c.gray(
|
|
22719
|
-
` \`open: ${oneLine(rule.handle, WIDTH.handle)}\`${rule.lane ? ` \xB7 ${lane}` : ""}`
|
|
22909
|
+
` \`open: ${oneLine(rule.handle, WIDTH.handle)}\`${rule.lane ? ` \xB7 ${lane}` : ""}${rule.written_by === void 0 ? "" : ` \xB7 ${connectorLabel(rule.written_by)}`}`
|
|
22720
22910
|
)
|
|
22721
22911
|
);
|
|
22722
22912
|
}
|
|
@@ -22735,7 +22925,7 @@ ${shownOf - rules2.length} more not shown.`));
|
|
|
22735
22925
|
printHuman(`- ${oneLine(rule.summary ?? rule.title, WIDTH.text)}`);
|
|
22736
22926
|
printHuman(
|
|
22737
22927
|
c.gray(
|
|
22738
|
-
` \`open: ${oneLine(rule.handle, WIDTH.handle)}\` \xB7 ${oneLine(rule.scope ?? "", WIDTH.title)}`
|
|
22928
|
+
` \`open: ${oneLine(rule.handle, WIDTH.handle)}\` \xB7 ${oneLine(rule.scope ?? "", WIDTH.title)}${rule.written_by === void 0 ? "" : ` \xB7 ${connectorLabel(rule.written_by)}`}`
|
|
22739
22929
|
)
|
|
22740
22930
|
);
|
|
22741
22931
|
}
|
|
@@ -22885,6 +23075,23 @@ move existing notes up.
|
|
|
22885
23075
|
|
|
22886
23076
|
The \`memgineering-setup\` skill covers what comes after.`;
|
|
22887
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
|
+
}
|
|
22888
23095
|
async function claimFullSetupNotice() {
|
|
22889
23096
|
const marker = join26(brandHome(), "setup-notice-shown");
|
|
22890
23097
|
try {
|
|
@@ -22953,7 +23160,42 @@ var POINT_AT_A_BRAIN = `## Signed in, and this machine reads no brain yet.
|
|
|
22953
23160
|
\`memgineering use --brain <name>\` points this machine at one.
|
|
22954
23161
|
\`memgineering create <name>\` makes one and points here, when the account has none yet.
|
|
22955
23162
|
`;
|
|
22956
|
-
|
|
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
|
+
}
|
|
22957
23199
|
if (signedIn && cfg.brain.cloud === void 0) {
|
|
22958
23200
|
return { id: "point_at_a_brain", markdown: POINT_AT_A_BRAIN };
|
|
22959
23201
|
}
|
|
@@ -22970,7 +23212,9 @@ function nextStepFor(cfg, signedIn = false) {
|
|
|
22970
23212
|
async function currentNextStep() {
|
|
22971
23213
|
try {
|
|
22972
23214
|
const stored = await readCredentials().catch(() => null);
|
|
22973
|
-
|
|
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 });
|
|
22974
23218
|
} catch {
|
|
22975
23219
|
return null;
|
|
22976
23220
|
}
|
|
@@ -23663,7 +23907,7 @@ init_api_client();
|
|
|
23663
23907
|
init_credentials();
|
|
23664
23908
|
init_cloud_brain();
|
|
23665
23909
|
init_config();
|
|
23666
|
-
import { isAbsolute as isAbsolute5, relative as relative2, resolve as
|
|
23910
|
+
import { isAbsolute as isAbsolute5, relative as relative2, resolve as resolve16 } from "path";
|
|
23667
23911
|
import { Command as Command44 } from "commander";
|
|
23668
23912
|
init_vault();
|
|
23669
23913
|
init_ui();
|
|
@@ -23693,7 +23937,7 @@ function useCommand() {
|
|
|
23693
23937
|
await pointAtHosted(opts.brain, opts, brainPath);
|
|
23694
23938
|
return;
|
|
23695
23939
|
}
|
|
23696
|
-
const where = await canonicalize(
|
|
23940
|
+
const where = await canonicalize(resolve16(opts.here ?? process.cwd()));
|
|
23697
23941
|
if (!brainPath) {
|
|
23698
23942
|
const pointer = await readPointer(where);
|
|
23699
23943
|
const resolved = await resolveBrain(void 0, where).catch((err) => err);
|
|
@@ -23839,7 +24083,7 @@ ${hint}`));
|
|
|
23839
24083
|
});
|
|
23840
24084
|
return;
|
|
23841
24085
|
}
|
|
23842
|
-
const root = await canonicalize(
|
|
24086
|
+
const root = await canonicalize(resolve16(brainPath));
|
|
23843
24087
|
const cfg = await loadConfig();
|
|
23844
24088
|
if (!findBrain(cfg, root)) {
|
|
23845
24089
|
throw memgError(
|
|
@@ -23866,7 +24110,7 @@ A pointer to an unlinked folder would fail on every command instead of at this o
|
|
|
23866
24110
|
return;
|
|
23867
24111
|
}
|
|
23868
24112
|
const value = isInside(root, where) ? relative2(where, root) || "." : root;
|
|
23869
|
-
const file =
|
|
24113
|
+
const file = resolve16(where, BRAND.pointerFileName);
|
|
23870
24114
|
await writeThenRename(file, `${value}
|
|
23871
24115
|
`);
|
|
23872
24116
|
printDual({
|
|
@@ -24056,6 +24300,7 @@ function whoamiCommand() {
|
|
|
24056
24300
|
}
|
|
24057
24301
|
|
|
24058
24302
|
// src/program.ts
|
|
24303
|
+
init_config();
|
|
24059
24304
|
init_ui();
|
|
24060
24305
|
init_brand();
|
|
24061
24306
|
|
|
@@ -24178,6 +24423,10 @@ async function announceNextStep(commandName, reader) {
|
|
|
24178
24423
|
const step = await currentNextStep();
|
|
24179
24424
|
if (step === null) return;
|
|
24180
24425
|
announced = true;
|
|
24426
|
+
if (step.id === "setup_connected") {
|
|
24427
|
+
const cfg = await loadConfig().catch(() => null);
|
|
24428
|
+
if (cfg?.system.setupConnected) await markConnectedNoticeShown(cfg.system.setupConnected.at);
|
|
24429
|
+
}
|
|
24181
24430
|
if (step.id === "finish_setup" && reader !== "full") {
|
|
24182
24431
|
const text = reader === "person" ? SHORT_SETUP_NOTICE : reader === "verb" && await claimFullSetupNotice() ? step.markdown : SETUP_NUDGE;
|
|
24183
24432
|
process.stderr.write(`
|
package/package.json
CHANGED