memgineering 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -1
- package/README.md +11 -0
- package/assets/MEMGINEERING.md +4 -4
- package/assets/memgineering-setup/SKILL.md +77 -7
- package/dist/index.js +1195 -504
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6865,10 +6865,10 @@ var init_cloud_pointer = __esm({
|
|
|
6865
6865
|
});
|
|
6866
6866
|
|
|
6867
6867
|
// src/commands/link.ts
|
|
6868
|
-
import { readFile as
|
|
6869
|
-
import { join as
|
|
6870
|
-
import { Command as
|
|
6871
|
-
import
|
|
6868
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
6869
|
+
import { join as join20 } from "path";
|
|
6870
|
+
import { Command as Command14 } from "commander";
|
|
6871
|
+
import prompts3 from "prompts";
|
|
6872
6872
|
function sameBasename(all, entry) {
|
|
6873
6873
|
const base2 = (entry.split("/").pop() ?? entry).normalize("NFC").toLowerCase();
|
|
6874
6874
|
return all.filter((p) => (p.split("/").pop() ?? "").normalize("NFC").toLowerCase() === base2);
|
|
@@ -6890,7 +6890,7 @@ function spreadAcrossFolders(rows, max) {
|
|
|
6890
6890
|
return picked;
|
|
6891
6891
|
}
|
|
6892
6892
|
function linkCommand() {
|
|
6893
|
-
return new
|
|
6893
|
+
return new Command14("link").description("read a notes folder as a brain \u2014 audits its rules first").argument("<path>", "path to the notes folder").option("--yes", "skip the confirmation").option("--dry-run", "show exactly what would be stored, then stop").option(
|
|
6894
6894
|
"--observations <dir>",
|
|
6895
6895
|
"where `remember` writes, relative to the folder",
|
|
6896
6896
|
DEFAULT_OBSERVATIONS_DIR
|
|
@@ -7070,7 +7070,7 @@ async function recordLink(adapter, observationsDir) {
|
|
|
7070
7070
|
}
|
|
7071
7071
|
async function refuseSilentRules(path, denied, excluded, unsupported) {
|
|
7072
7072
|
const denyEntries = (await LocalFileReadAdapter.readDenyFile(path, ".memgdeny")).entries;
|
|
7073
|
-
const rawDenyLines = await
|
|
7073
|
+
const rawDenyLines = await readFile20(join20(path, ".memgdeny"), "utf8").then(
|
|
7074
7074
|
(t) => t.split("\n").map((l) => l.trim()).filter((l) => l !== "" && !l.startsWith("#"))
|
|
7075
7075
|
).catch(() => []);
|
|
7076
7076
|
const asWritten = (folded) => rawDenyLines.find(
|
|
@@ -7216,7 +7216,7 @@ Brain: ${root}`));
|
|
|
7216
7216
|
"\n This tool writes into your notes without asking each time \u2014 new\n observations, and changes to the memory block of a note. Every one is\n recorded and reversible: memgineering log \xB7 memgineering undo"
|
|
7217
7217
|
)
|
|
7218
7218
|
);
|
|
7219
|
-
const { ok } = await
|
|
7219
|
+
const { ok } = await prompts3({
|
|
7220
7220
|
type: "confirm",
|
|
7221
7221
|
name: "ok",
|
|
7222
7222
|
message: "Build the index for this folder?",
|
|
@@ -7297,9 +7297,9 @@ var init_open_browser = __esm({
|
|
|
7297
7297
|
});
|
|
7298
7298
|
|
|
7299
7299
|
// src/commands/login.ts
|
|
7300
|
-
import { Command as
|
|
7300
|
+
import { Command as Command22 } from "commander";
|
|
7301
7301
|
function loginCommand() {
|
|
7302
|
-
return new
|
|
7302
|
+
return new Command22("login").description("sign in to a hosted brain").option(
|
|
7303
7303
|
"--emit-only",
|
|
7304
7304
|
"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"
|
|
7305
7305
|
).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(
|
|
@@ -7499,9 +7499,9 @@ var push_exports = {};
|
|
|
7499
7499
|
__export(push_exports, {
|
|
7500
7500
|
pushCommand: () => pushCommand
|
|
7501
7501
|
});
|
|
7502
|
-
import { Command as
|
|
7502
|
+
import { Command as Command37 } from "commander";
|
|
7503
7503
|
function pushCommand() {
|
|
7504
|
-
return new
|
|
7504
|
+
return new Command37("push").description("upload the brain on this machine to your hosted one").option("--vault <path>", "which local brain to upload").option("--brain <name>", "the hosted brain to upload into (created if it does not exist)").option("--dry-run", "list what would be uploaded and send nothing").action(async (opts) => {
|
|
7505
7505
|
const { brain: local } = await resolveBrain(opts.vault);
|
|
7506
7506
|
const adapter = await openBrainOrExplain(local.root);
|
|
7507
7507
|
const scan = await adapter.scan();
|
|
@@ -7753,8 +7753,8 @@ var init_guard_shell = __esm({
|
|
|
7753
7753
|
});
|
|
7754
7754
|
|
|
7755
7755
|
// src/lib/hook-refresh.ts
|
|
7756
|
-
import { chmod, lstat as lstat5, readFile as
|
|
7757
|
-
import { join as
|
|
7756
|
+
import { chmod, lstat as lstat5, readFile as readFile26 } from "fs/promises";
|
|
7757
|
+
import { join as join26 } from "path";
|
|
7758
7758
|
function moveShippedCommands(parsed, moves) {
|
|
7759
7759
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
7760
7760
|
const settings = parsed;
|
|
@@ -7785,10 +7785,10 @@ function moveShippedCommands(parsed, moves) {
|
|
|
7785
7785
|
return { next: changed === 0 ? settings : { ...settings, hooks: nextHooks }, changed };
|
|
7786
7786
|
}
|
|
7787
7787
|
function withHookFileLock(fn, waitMs = 0) {
|
|
7788
|
-
return withPathLock(
|
|
7788
|
+
return withPathLock(join26(brandHome(), "hook-files.lock"), fn, waitMs);
|
|
7789
7789
|
}
|
|
7790
7790
|
async function readSettings(path) {
|
|
7791
|
-
const raw = await
|
|
7791
|
+
const raw = await readFile26(path, "utf8").catch(() => null);
|
|
7792
7792
|
if (raw === null) return void 0;
|
|
7793
7793
|
try {
|
|
7794
7794
|
return JSON.parse(raw);
|
|
@@ -8024,7 +8024,7 @@ var init_setup_brain = __esm({
|
|
|
8024
8024
|
});
|
|
8025
8025
|
|
|
8026
8026
|
// src/commands/setup-signin.ts
|
|
8027
|
-
import
|
|
8027
|
+
import prompts5 from "prompts";
|
|
8028
8028
|
async function runSignInStep(opts) {
|
|
8029
8029
|
const summary = await signInSummary();
|
|
8030
8030
|
if (summary.signedIn) return { status: "signed-in", displayName: summary.displayName };
|
|
@@ -8051,7 +8051,7 @@ async function runSignInStep(opts) {
|
|
|
8051
8051
|
}
|
|
8052
8052
|
if (opts.interactive) {
|
|
8053
8053
|
printHuman(c.bold("\nmemgineering \u2014 setup\n"));
|
|
8054
|
-
const { wants } = await
|
|
8054
|
+
const { wants } = await prompts5({
|
|
8055
8055
|
type: "toggle",
|
|
8056
8056
|
name: "wants",
|
|
8057
8057
|
message: "Sign in to a memgineering account first?",
|
|
@@ -8181,37 +8181,37 @@ var init_setup_signin = __esm({
|
|
|
8181
8181
|
});
|
|
8182
8182
|
|
|
8183
8183
|
// src/lib/next-step.ts
|
|
8184
|
-
import { mkdir as
|
|
8185
|
-
import { join as
|
|
8184
|
+
import { mkdir as mkdir17, readFile as readFile27, writeFile as writeFile14 } from "fs/promises";
|
|
8185
|
+
import { join as join27 } from "path";
|
|
8186
8186
|
function isSetUp(cfg) {
|
|
8187
8187
|
return Object.keys(cfg.system.installed).length > 0;
|
|
8188
8188
|
}
|
|
8189
8189
|
function connectedMarker(at) {
|
|
8190
|
-
return
|
|
8190
|
+
return join27(brandHome(), `setup-connected-shown-${at.replace(/[^0-9A-Za-z]/g, "")}`);
|
|
8191
8191
|
}
|
|
8192
8192
|
async function connectedNoticeShown(at) {
|
|
8193
|
-
return
|
|
8193
|
+
return readFile27(connectedMarker(at), "utf8").then(
|
|
8194
8194
|
() => true,
|
|
8195
8195
|
() => false
|
|
8196
8196
|
);
|
|
8197
8197
|
}
|
|
8198
8198
|
async function markConnectedNoticeShown(at) {
|
|
8199
8199
|
try {
|
|
8200
|
-
await
|
|
8200
|
+
await mkdir17(brandHome(), { recursive: true });
|
|
8201
8201
|
await writeFile14(connectedMarker(at), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
8202
8202
|
`);
|
|
8203
8203
|
} catch {
|
|
8204
8204
|
}
|
|
8205
8205
|
}
|
|
8206
8206
|
async function claimFullSetupNotice() {
|
|
8207
|
-
const marker =
|
|
8207
|
+
const marker = join27(brandHome(), "setup-notice-shown");
|
|
8208
8208
|
try {
|
|
8209
|
-
await
|
|
8209
|
+
await readFile27(marker, "utf8");
|
|
8210
8210
|
return false;
|
|
8211
8211
|
} catch {
|
|
8212
8212
|
}
|
|
8213
8213
|
try {
|
|
8214
|
-
await
|
|
8214
|
+
await mkdir17(brandHome(), { recursive: true });
|
|
8215
8215
|
await writeFile14(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
8216
8216
|
`);
|
|
8217
8217
|
} catch {
|
|
@@ -8378,7 +8378,7 @@ Synced instead? \`memgineering login\`, then \`memgineering create <name>\`.`;
|
|
|
8378
8378
|
// src/lib/provenance.ts
|
|
8379
8379
|
import { execFile, spawn as spawn2 } from "child_process";
|
|
8380
8380
|
import { realpath as realpath3 } from "fs/promises";
|
|
8381
|
-
import { dirname as dirname9, join as
|
|
8381
|
+
import { dirname as dirname9, join as join28, resolve as resolve14 } from "path";
|
|
8382
8382
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8383
8383
|
import { promisify } from "util";
|
|
8384
8384
|
async function detectProvenance(selfPath, opts = {}) {
|
|
@@ -8516,7 +8516,7 @@ var init_provenance = __esm({
|
|
|
8516
8516
|
kind: "npm",
|
|
8517
8517
|
bin: "npm",
|
|
8518
8518
|
prefixArgs: ["prefix", "-g"],
|
|
8519
|
-
moduleRoot: (prefix) => process.platform === "win32" ?
|
|
8519
|
+
moduleRoot: (prefix) => process.platform === "win32" ? join28(prefix, "node_modules") : join28(prefix, "lib", "node_modules"),
|
|
8520
8520
|
install: "npm i -g memgineering@latest"
|
|
8521
8521
|
},
|
|
8522
8522
|
{
|
|
@@ -8553,9 +8553,9 @@ __export(setup_cloud_exports, {
|
|
|
8553
8553
|
});
|
|
8554
8554
|
import { spawn as spawn3 } from "child_process";
|
|
8555
8555
|
import { realpathSync as realpathSync2 } from "fs";
|
|
8556
|
-
import { readdir as readdir9, stat as
|
|
8556
|
+
import { readdir as readdir9, stat as stat9 } from "fs/promises";
|
|
8557
8557
|
import { homedir as homedir5 } from "os";
|
|
8558
|
-
import { basename as basename4, dirname as dirname10, join as
|
|
8558
|
+
import { basename as basename4, dirname as dirname10, join as join29, resolve as resolve15, sep as sep4 } from "path";
|
|
8559
8559
|
async function report2(setupToken, state, detail, code) {
|
|
8560
8560
|
try {
|
|
8561
8561
|
await apiRequest({
|
|
@@ -8573,7 +8573,7 @@ async function report2(setupToken, state, detail, code) {
|
|
|
8573
8573
|
}
|
|
8574
8574
|
async function folderCandidates() {
|
|
8575
8575
|
const home = homedir5();
|
|
8576
|
-
const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) =>
|
|
8576
|
+
const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) => join29(home, d));
|
|
8577
8577
|
const found = [];
|
|
8578
8578
|
const countNotes = async (dir) => {
|
|
8579
8579
|
let count = 0;
|
|
@@ -8582,7 +8582,7 @@ async function folderCandidates() {
|
|
|
8582
8582
|
if (entry.name.startsWith(".")) continue;
|
|
8583
8583
|
if (entry.isFile() && /\.(md|markdown)$/i.test(entry.name)) count += 1;
|
|
8584
8584
|
else if (entry.isDirectory()) {
|
|
8585
|
-
const inner = await readdir9(
|
|
8585
|
+
const inner = await readdir9(join29(dir, entry.name)).catch(() => []);
|
|
8586
8586
|
count += inner.filter((n) => /\.(md|markdown)$/i.test(n)).length;
|
|
8587
8587
|
}
|
|
8588
8588
|
if (count > 999) break;
|
|
@@ -8590,7 +8590,7 @@ async function folderCandidates() {
|
|
|
8590
8590
|
return count;
|
|
8591
8591
|
};
|
|
8592
8592
|
for (const root of roots) {
|
|
8593
|
-
const info = await
|
|
8593
|
+
const info = await stat9(root).catch(() => null);
|
|
8594
8594
|
if (!info?.isDirectory()) continue;
|
|
8595
8595
|
const notes = await countNotes(root);
|
|
8596
8596
|
if (notes > 0) found.push({ path: root, note_count: notes });
|
|
@@ -8935,8 +8935,8 @@ function insideFolder(target, folder) {
|
|
|
8935
8935
|
return landed === root || landed.startsWith(root + sep4);
|
|
8936
8936
|
}
|
|
8937
8937
|
async function askBeforeUpload(q) {
|
|
8938
|
-
const { default:
|
|
8939
|
-
const { yes } = await
|
|
8938
|
+
const { default: prompts7 } = await import("prompts");
|
|
8939
|
+
const { yes } = await prompts7({
|
|
8940
8940
|
type: "confirm",
|
|
8941
8941
|
name: "yes",
|
|
8942
8942
|
message: `Upload the notes in ${q.path} to the brain on ${q.email ?? "this account"}?`,
|
|
@@ -8974,14 +8974,14 @@ var init_setup_cloud = __esm({
|
|
|
8974
8974
|
|
|
8975
8975
|
// src/commands/setup.ts
|
|
8976
8976
|
import { createHash as createHash9 } from "crypto";
|
|
8977
|
-
import { access as access2, mkdir as
|
|
8978
|
-
import { dirname as dirname11, join as
|
|
8977
|
+
import { access as access2, mkdir as mkdir18, readFile as readFile28 } from "fs/promises";
|
|
8978
|
+
import { dirname as dirname11, join as join30 } from "path";
|
|
8979
8979
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8980
|
-
import { Command as
|
|
8981
|
-
import
|
|
8980
|
+
import { Command as Command39 } from "commander";
|
|
8981
|
+
import prompts6 from "prompts";
|
|
8982
8982
|
import { homedir as homedir6 } from "os";
|
|
8983
8983
|
function setupCommand() {
|
|
8984
|
-
return new
|
|
8984
|
+
return new Command39("setup").description("install into your agents, and point at a brain (--dry-run to only report)").option("--agent", "non-interactive: take every answer from flags").option("--human", "interactive: choose from a list").option("--web", "open a screen in the browser and answer it there").option("--print-url", "with --web: print the URL instead of opening a browser").option(
|
|
8985
8985
|
"--wait",
|
|
8986
8986
|
"with --web: block until the screen is finished, even with nobody watching the terminal"
|
|
8987
8987
|
).option(
|
|
@@ -9002,9 +9002,11 @@ function setupCommand() {
|
|
|
9002
9002
|
"",
|
|
9003
9003
|
"Chat apps with no shell need no setup. They read and write the brain on your",
|
|
9004
9004
|
`account (not a folder on this machine) at ${HOSTED_CONNECTOR_URL}:`,
|
|
9005
|
-
" ChatGPT: Settings \u2192 Developer mode \u2192 add an MCP server with that URL, sign in.",
|
|
9006
9005
|
" Claude.ai, Claude Desktop, Cowork: Customize \u2192 Connectors \u2192 Add custom",
|
|
9007
|
-
" connector with that URL, sign in."
|
|
9006
|
+
" connector with that URL, sign in.",
|
|
9007
|
+
" ChatGPT: Settings \u2192 Developer mode \u2192 add an MCP server with that URL, sign in",
|
|
9008
|
+
" (paused for now: the connect screen refuses ChatGPT while its notice is",
|
|
9009
|
+
" verified; use Claude or Gemini meanwhile)."
|
|
9008
9010
|
].join("\n")
|
|
9009
9011
|
).action(async (opts) => {
|
|
9010
9012
|
const detected = await detectTools();
|
|
@@ -9277,7 +9279,7 @@ function setupCommand() {
|
|
|
9277
9279
|
}
|
|
9278
9280
|
});
|
|
9279
9281
|
if (mode.kind === "human" && brains.length === 0 && !opts.dryRun && isInteractive()) {
|
|
9280
|
-
const { path } = await
|
|
9282
|
+
const { path } = await prompts6({
|
|
9281
9283
|
type: "text",
|
|
9282
9284
|
name: "path",
|
|
9283
9285
|
message: "Path to your notes folder (blank to skip)"
|
|
@@ -9342,7 +9344,7 @@ async function applySetup(choice, dryRun) {
|
|
|
9342
9344
|
}
|
|
9343
9345
|
async function write2() {
|
|
9344
9346
|
for (const tool of choice.tools) {
|
|
9345
|
-
const hubPath =
|
|
9347
|
+
const hubPath = join30(agentHome(), tool.hubFile);
|
|
9346
9348
|
const written = await injectBlock(
|
|
9347
9349
|
hubPath,
|
|
9348
9350
|
hub + tailFor(tool, choice.hook),
|
|
@@ -9357,8 +9359,8 @@ async function applySetup(choice, dryRun) {
|
|
|
9357
9359
|
});
|
|
9358
9360
|
if (!dryRun && written.after !== null) mark(hubPath, written.after);
|
|
9359
9361
|
for (const skill of BRAND.subSkills) {
|
|
9360
|
-
const content = await loadAsset(
|
|
9361
|
-
const path =
|
|
9362
|
+
const content = await loadAsset(join30(skill, "SKILL.md"));
|
|
9363
|
+
const path = join30(agentHome(), tool.skillsDir, skill, "SKILL.md");
|
|
9362
9364
|
const out = await writeIfDifferent(path, content, dryRun);
|
|
9363
9365
|
results.push({
|
|
9364
9366
|
tool: tool.label,
|
|
@@ -9448,7 +9450,7 @@ async function hooksArmed(tools) {
|
|
|
9448
9450
|
const capable = tools.filter((t) => t.hooks && t.hooksFile);
|
|
9449
9451
|
if (capable.length === 0) return void 0;
|
|
9450
9452
|
for (const tool of capable) {
|
|
9451
|
-
const raw = await
|
|
9453
|
+
const raw = await readFile28(join30(agentHome(), ...(tool.hooksFile ?? "").split("/")), "utf8").catch(
|
|
9452
9454
|
() => ""
|
|
9453
9455
|
);
|
|
9454
9456
|
if (hooksFor([tool]).some((spec) => raw.includes(spec.signature))) return "on";
|
|
@@ -9468,7 +9470,7 @@ async function resolveMode(opts) {
|
|
|
9468
9470
|
return { kind: "human", chosen: true };
|
|
9469
9471
|
}
|
|
9470
9472
|
if (!isInteractive() || isJsonMode()) return { kind: "agent", chosen: false };
|
|
9471
|
-
const { mode } = await
|
|
9473
|
+
const { mode } = await prompts6({
|
|
9472
9474
|
type: "select",
|
|
9473
9475
|
name: "mode",
|
|
9474
9476
|
message: "How would you like to set this up?",
|
|
@@ -9488,7 +9490,7 @@ async function detectTools() {
|
|
|
9488
9490
|
const home = agentHome();
|
|
9489
9491
|
const found = [];
|
|
9490
9492
|
for (const tool of TOOLS) {
|
|
9491
|
-
if (await exists2(
|
|
9493
|
+
if (await exists2(join30(home, tool.dir))) found.push(tool);
|
|
9492
9494
|
}
|
|
9493
9495
|
return found;
|
|
9494
9496
|
}
|
|
@@ -9535,7 +9537,7 @@ async function askHuman(detected, opts) {
|
|
|
9535
9537
|
printHuman(c.bold("\nmemgineering \u2014 setup\n"));
|
|
9536
9538
|
printHuman(c.gray(" Found: ") + detected.map((t) => t.label).join(", "));
|
|
9537
9539
|
printHuman("");
|
|
9538
|
-
const answers = await
|
|
9540
|
+
const answers = await prompts6([
|
|
9539
9541
|
{
|
|
9540
9542
|
type: "multiselect",
|
|
9541
9543
|
name: "tools",
|
|
@@ -9588,10 +9590,10 @@ async function injectBlock(filePath, content, dryRun, weWroteThis) {
|
|
|
9588
9590
|
const block = `${BRAND.hubMarkerStart}
|
|
9589
9591
|
${content.trimEnd()}
|
|
9590
9592
|
${BRAND.hubMarkerEnd}`;
|
|
9591
|
-
const existing = await
|
|
9593
|
+
const existing = await readFile28(filePath, "utf8").catch(() => null);
|
|
9592
9594
|
if (existing === null) {
|
|
9593
9595
|
if (!dryRun) {
|
|
9594
|
-
await
|
|
9596
|
+
await mkdir18(dirname11(filePath), { recursive: true });
|
|
9595
9597
|
await writeThenRename(filePath, `${block}
|
|
9596
9598
|
`);
|
|
9597
9599
|
}
|
|
@@ -9631,11 +9633,11 @@ function looksLikeOurGuide(text) {
|
|
|
9631
9633
|
return text.includes("memgineering recall") && text.includes("memgineering remember");
|
|
9632
9634
|
}
|
|
9633
9635
|
async function writeIfDifferent(filePath, content, dryRun) {
|
|
9634
|
-
const existing = await
|
|
9636
|
+
const existing = await readFile28(filePath, "utf8").catch(() => null);
|
|
9635
9637
|
const after = fingerprint(content);
|
|
9636
9638
|
if (existing === content) return { outcome: "unchanged", before: existing, after };
|
|
9637
9639
|
if (!dryRun) {
|
|
9638
|
-
await
|
|
9640
|
+
await mkdir18(dirname11(filePath), { recursive: true });
|
|
9639
9641
|
await writeThenRename(filePath, content);
|
|
9640
9642
|
}
|
|
9641
9643
|
return { outcome: existing === null ? "injected" : "updated", before: existing, after };
|
|
@@ -9652,7 +9654,7 @@ async function refreshOwnHookCommands() {
|
|
|
9652
9654
|
const changed = [];
|
|
9653
9655
|
const skipped = [];
|
|
9654
9656
|
for (const tool of TOOLS.filter((t) => t.hooks && t.hooksFile && DEFAULT_HOOK_TOOLS.includes(t.id))) {
|
|
9655
|
-
const path =
|
|
9657
|
+
const path = join30(agentHome(), ...(tool.hooksFile ?? "").split("/"));
|
|
9656
9658
|
const outcome = await refreshHookFile(path, moves);
|
|
9657
9659
|
if (outcome === "changed") changed.push(path);
|
|
9658
9660
|
else if (outcome === "symlink" || outcome === "refused") skipped.push({ path, why: outcome });
|
|
@@ -9679,8 +9681,8 @@ function hooksFor(tools) {
|
|
|
9679
9681
|
);
|
|
9680
9682
|
}
|
|
9681
9683
|
async function installHook(spec, tool, dryRun) {
|
|
9682
|
-
const settingsPath =
|
|
9683
|
-
const raw = await
|
|
9684
|
+
const settingsPath = join30(agentHome(), ...(tool.hooksFile ?? "").split("/"));
|
|
9685
|
+
const raw = await readFile28(settingsPath, "utf8").catch(() => null);
|
|
9684
9686
|
let settings = {};
|
|
9685
9687
|
if (raw !== null) {
|
|
9686
9688
|
try {
|
|
@@ -9722,15 +9724,15 @@ async function installHook(spec, tool, dryRun) {
|
|
|
9722
9724
|
}
|
|
9723
9725
|
const next = { ...settings, hooks: { ...hooks, [spec.event]: existingGroups } };
|
|
9724
9726
|
if (!dryRun) {
|
|
9725
|
-
await
|
|
9727
|
+
await mkdir18(dirname11(settingsPath), { recursive: true });
|
|
9726
9728
|
await writeThenRename(settingsPath, `${JSON.stringify(next, null, 2)}
|
|
9727
9729
|
`);
|
|
9728
9730
|
}
|
|
9729
9731
|
return raw === null ? "injected" : "updated";
|
|
9730
9732
|
}
|
|
9731
9733
|
async function removeHook(spec, tool, dryRun) {
|
|
9732
|
-
const settingsPath =
|
|
9733
|
-
const raw = await
|
|
9734
|
+
const settingsPath = join30(agentHome(), ...(tool.hooksFile ?? "").split("/"));
|
|
9735
|
+
const raw = await readFile28(settingsPath, "utf8").catch(() => null);
|
|
9734
9736
|
if (raw === null) return "unchanged";
|
|
9735
9737
|
let parsed;
|
|
9736
9738
|
try {
|
|
@@ -9754,7 +9756,7 @@ async function removeHook(spec, tool, dryRun) {
|
|
|
9754
9756
|
return "removed";
|
|
9755
9757
|
}
|
|
9756
9758
|
async function loadAsset(relPath) {
|
|
9757
|
-
const raw = await
|
|
9759
|
+
const raw = await readFile28(join30(await assetRoot(), relPath), "utf8");
|
|
9758
9760
|
if (relPath === BRAND.hubFileName && raw.startsWith("---")) {
|
|
9759
9761
|
const end = raw.indexOf("\n---", 4);
|
|
9760
9762
|
if (end !== -1) return raw.slice(end + 4).trimStart();
|
|
@@ -9763,12 +9765,12 @@ async function loadAsset(relPath) {
|
|
|
9763
9765
|
}
|
|
9764
9766
|
async function assetRoot() {
|
|
9765
9767
|
const here = dirname11(fileURLToPath3(import.meta.url));
|
|
9766
|
-
const bundled =
|
|
9767
|
-
if (await exists2(
|
|
9768
|
+
const bundled = join30(here, "..", "assets");
|
|
9769
|
+
if (await exists2(join30(bundled, BRAND.hubFileName))) return bundled;
|
|
9768
9770
|
let dir = here;
|
|
9769
9771
|
for (; ; ) {
|
|
9770
|
-
const candidate =
|
|
9771
|
-
if (await exists2(
|
|
9772
|
+
const candidate = join30(dir, "packages", "skills");
|
|
9773
|
+
if (await exists2(join30(candidate, BRAND.hubFileName))) return candidate;
|
|
9772
9774
|
const parent = dirname11(dir);
|
|
9773
9775
|
if (parent === dir) break;
|
|
9774
9776
|
dir = parent;
|
|
@@ -9963,7 +9965,7 @@ For a guide: put ${BRAND.hubMarkerStart} and ${BRAND.hubMarkerEnd} back around t
|
|
|
9963
9965
|
import net from "net";
|
|
9964
9966
|
|
|
9965
9967
|
// src/program.ts
|
|
9966
|
-
import { Command as
|
|
9968
|
+
import { Command as Command47 } from "commander";
|
|
9967
9969
|
|
|
9968
9970
|
// src/help.ts
|
|
9969
9971
|
init_ui();
|
|
@@ -10031,6 +10033,7 @@ var COMMAND_GROUPS = [
|
|
|
10031
10033
|
"create",
|
|
10032
10034
|
"rename",
|
|
10033
10035
|
"delete-brain",
|
|
10036
|
+
"delete-account",
|
|
10034
10037
|
"push",
|
|
10035
10038
|
"pull"
|
|
10036
10039
|
]
|
|
@@ -11525,6 +11528,11 @@ var YankResponseSchema = z19.object({
|
|
|
11525
11528
|
yank_reason: z19.string().max(280).nullable(),
|
|
11526
11529
|
effective_current_version: SemverSchema
|
|
11527
11530
|
});
|
|
11531
|
+
var RestorePackageResponseSchema = z19.object({
|
|
11532
|
+
full_slug: FullSlugSchema,
|
|
11533
|
+
status: z19.literal("restored"),
|
|
11534
|
+
restored_at: IsoDateTimeSchema
|
|
11535
|
+
});
|
|
11528
11536
|
var ResolveVersionQuerySchema = z19.object({
|
|
11529
11537
|
range: SemverRangeSchema
|
|
11530
11538
|
});
|
|
@@ -11569,7 +11577,15 @@ var UserProfileResponseSchema = PublicUserSchema.extend({
|
|
|
11569
11577
|
* responses omit both. Optional so pre-P1 clients ignore them.
|
|
11570
11578
|
*/
|
|
11571
11579
|
viewer_is_following: z19.boolean().optional(),
|
|
11572
|
-
viewer_is_self: z19.boolean().optional()
|
|
11580
|
+
viewer_is_self: z19.boolean().optional(),
|
|
11581
|
+
/**
|
|
11582
|
+
* Whether other people can see this profile (migration 0031). A profile
|
|
11583
|
+
* becomes public with its owner's first public publish, comment or star,
|
|
11584
|
+
* and stays public. Anyone else asking for a non-public profile gets the
|
|
11585
|
+
* unknown-user `404`, so `false` only ever reaches the owner. Absent from
|
|
11586
|
+
* servers that predate it; treat absent as public.
|
|
11587
|
+
*/
|
|
11588
|
+
is_public: z19.boolean().optional()
|
|
11573
11589
|
});
|
|
11574
11590
|
var UserPackagesQuerySchema = z19.object({
|
|
11575
11591
|
sort: SortModeSchema.default("stars"),
|
|
@@ -11597,6 +11613,81 @@ var ListFollowersResponseSchema = z19.object({
|
|
|
11597
11613
|
results: PublicUserSchema.array(),
|
|
11598
11614
|
next_cursor: z19.string().nullable()
|
|
11599
11615
|
});
|
|
11616
|
+
var MeResponseSchema = SelfUserSchema.extend({
|
|
11617
|
+
/**
|
|
11618
|
+
* Whether the caller's registry profile is visible to other people
|
|
11619
|
+
* (migration 0031): false until their first public publish, comment or
|
|
11620
|
+
* star. Absent from servers that predate it; treat absent as public.
|
|
11621
|
+
*/
|
|
11622
|
+
profile_public: z19.boolean().optional()
|
|
11623
|
+
});
|
|
11624
|
+
var DeleteMeRequestSchema = z19.object({
|
|
11625
|
+
confirm_username: z19.string().min(1).max(32)
|
|
11626
|
+
});
|
|
11627
|
+
var AccountExportUserSchema = z19.object({
|
|
11628
|
+
id: z19.string(),
|
|
11629
|
+
username: z19.string(),
|
|
11630
|
+
display_name: z19.string().nullable(),
|
|
11631
|
+
email: z19.string().nullable(),
|
|
11632
|
+
bio: z19.string().nullable(),
|
|
11633
|
+
avatar_url: z19.string().nullable(),
|
|
11634
|
+
primary_provider: z19.string(),
|
|
11635
|
+
role: z19.string(),
|
|
11636
|
+
created_at: z19.string(),
|
|
11637
|
+
updated_at: z19.string(),
|
|
11638
|
+
last_active_at: z19.string().nullable()
|
|
11639
|
+
});
|
|
11640
|
+
var AccountExportSchema = z19.object({
|
|
11641
|
+
schema_version: z19.literal(1),
|
|
11642
|
+
exported_at: z19.string(),
|
|
11643
|
+
user: AccountExportUserSchema,
|
|
11644
|
+
packages: z19.array(
|
|
11645
|
+
z19.object({
|
|
11646
|
+
id: z19.string(),
|
|
11647
|
+
slug: z19.string(),
|
|
11648
|
+
full_slug: z19.string(),
|
|
11649
|
+
type: z19.string(),
|
|
11650
|
+
tier: z19.string(),
|
|
11651
|
+
title: z19.string(),
|
|
11652
|
+
description: z19.string(),
|
|
11653
|
+
content: z19.string(),
|
|
11654
|
+
current_version: z19.string(),
|
|
11655
|
+
visibility: z19.string(),
|
|
11656
|
+
created_at: z19.string(),
|
|
11657
|
+
updated_at: z19.string(),
|
|
11658
|
+
deleted_at: z19.string().nullable()
|
|
11659
|
+
})
|
|
11660
|
+
),
|
|
11661
|
+
package_versions: z19.array(
|
|
11662
|
+
z19.object({
|
|
11663
|
+
id: z19.string(),
|
|
11664
|
+
package_id: z19.string(),
|
|
11665
|
+
version: z19.string(),
|
|
11666
|
+
content: z19.string(),
|
|
11667
|
+
review_status: z19.string(),
|
|
11668
|
+
published_at: z19.string(),
|
|
11669
|
+
yanked_at: z19.string().nullable()
|
|
11670
|
+
})
|
|
11671
|
+
),
|
|
11672
|
+
comments: z19.array(
|
|
11673
|
+
z19.object({
|
|
11674
|
+
id: z19.string(),
|
|
11675
|
+
package_id: z19.string(),
|
|
11676
|
+
body: z19.string(),
|
|
11677
|
+
kind: z19.string(),
|
|
11678
|
+
title: z19.string().nullable(),
|
|
11679
|
+
status: z19.string().nullable(),
|
|
11680
|
+
parent_id: z19.string().nullable(),
|
|
11681
|
+
created_at: z19.string(),
|
|
11682
|
+
edited_at: z19.string().nullable(),
|
|
11683
|
+
deleted_at: z19.string().nullable()
|
|
11684
|
+
})
|
|
11685
|
+
),
|
|
11686
|
+
stars: z19.array(z19.object({ package_id: z19.string(), created_at: z19.string() })),
|
|
11687
|
+
follows: z19.array(
|
|
11688
|
+
z19.object({ follower_id: z19.string(), following_id: z19.string(), created_at: z19.string() })
|
|
11689
|
+
)
|
|
11690
|
+
});
|
|
11600
11691
|
|
|
11601
11692
|
// ../../packages/contracts/src/context.ts
|
|
11602
11693
|
import { z as z20 } from "zod";
|
|
@@ -12078,6 +12169,23 @@ var KordisClient = class {
|
|
|
12078
12169
|
me() {
|
|
12079
12170
|
return this.request("/api/me");
|
|
12080
12171
|
}
|
|
12172
|
+
/**
|
|
12173
|
+
* The account's own data — the one registry answer that carries its `email`.
|
|
12174
|
+
* A GET, so it is retried once on a failed fetch like every other GET.
|
|
12175
|
+
*/
|
|
12176
|
+
exportMe() {
|
|
12177
|
+
return this.request("/api/me/export");
|
|
12178
|
+
}
|
|
12179
|
+
/**
|
|
12180
|
+
* Delete the signed-in registry account. Irreversible: the server compares
|
|
12181
|
+
* `confirm_username` with the current username and answers 204. The account
|
|
12182
|
+
* is renamed `deleted-<id>`; its published packages and versions stay at their
|
|
12183
|
+
* existing addresses, which still contain the old username. Never retried
|
|
12184
|
+
* (a DELETE), like every write here.
|
|
12185
|
+
*/
|
|
12186
|
+
deleteMe(body) {
|
|
12187
|
+
return this.request("/api/me", { method: "DELETE", body });
|
|
12188
|
+
}
|
|
12081
12189
|
listMyPackages(opts = {}) {
|
|
12082
12190
|
return this.request("/api/me/packages", {
|
|
12083
12191
|
query: {
|
|
@@ -12109,6 +12217,18 @@ var KordisClient = class {
|
|
|
12109
12217
|
deletePackage(username, slug) {
|
|
12110
12218
|
return this.request(`/api/packages/${seg(username)}/${seg(slug)}`, { method: "DELETE" });
|
|
12111
12219
|
}
|
|
12220
|
+
/**
|
|
12221
|
+
* Undo a delete. Author only, within 30 days of the deletion and before the
|
|
12222
|
+
* package's content is deleted; refusals are `conflict` (not deleted, too
|
|
12223
|
+
* late, content already deleted), `not_authorized` and `not_found`, each
|
|
12224
|
+
* with a hint.
|
|
12225
|
+
*/
|
|
12226
|
+
restorePackage(username, slug) {
|
|
12227
|
+
return this.request(
|
|
12228
|
+
`/api/packages/${seg(username)}/${seg(slug)}/restore`,
|
|
12229
|
+
{ method: "POST" }
|
|
12230
|
+
);
|
|
12231
|
+
}
|
|
12112
12232
|
getContent(username, slug, opts = {}) {
|
|
12113
12233
|
return this.request(
|
|
12114
12234
|
`/api/packages/${seg(username)}/${seg(slug)}/content`,
|
|
@@ -16890,88 +17010,316 @@ function nextStep3(renamed) {
|
|
|
16890
17010
|
return `Other machines that point at this brain keep showing the old name until their next command against it; nothing on them breaks \u2014 the pointer holds the id. \`--brain "${oneLine(renamed.renamed_from, WIDTH.title)}"\` no longer matches anywhere; use the new name or the id.`;
|
|
16891
17011
|
}
|
|
16892
17012
|
|
|
16893
|
-
// src/commands/delete-
|
|
17013
|
+
// src/commands/delete-account.ts
|
|
17014
|
+
import { Command as Command12 } from "commander";
|
|
17015
|
+
import prompts from "prompts";
|
|
16894
17016
|
init_errors();
|
|
16895
17017
|
init_api_client();
|
|
16896
|
-
|
|
17018
|
+
|
|
17019
|
+
// src/lib/account-deletion-marker.ts
|
|
17020
|
+
init_atomic();
|
|
17021
|
+
init_brand();
|
|
17022
|
+
import { mkdir as mkdir12, readFile as readFile18, stat as stat5, unlink as unlink5 } from "fs/promises";
|
|
17023
|
+
import { join as join19 } from "path";
|
|
17024
|
+
import { z as z25 } from "zod";
|
|
17025
|
+
var SideSchema = z25.object({
|
|
17026
|
+
api_url: z25.string().min(1),
|
|
17027
|
+
/** When the delete request was sent — set before it goes, so a lost answer is visible. */
|
|
17028
|
+
requested_at: z25.string().min(1).nullable().default(null),
|
|
17029
|
+
/** When the server confirmed it. Null while that side is still to do. */
|
|
17030
|
+
deleted_at: z25.string().min(1).nullable().default(null)
|
|
17031
|
+
});
|
|
17032
|
+
var MarkerSchema = z25.object({
|
|
17033
|
+
version: z25.literal(1),
|
|
17034
|
+
started_at: z25.string().min(1),
|
|
17035
|
+
/** How the two accounts were matched before anything was deleted. */
|
|
17036
|
+
matched_by: z25.enum(["email", "federation"]),
|
|
17037
|
+
brain: SideSchema.extend({ account_id: z25.string().min(1) }),
|
|
17038
|
+
registry: SideSchema.extend({
|
|
17039
|
+
user_id: z25.string().min(1),
|
|
17040
|
+
username: z25.string().min(1)
|
|
17041
|
+
})
|
|
17042
|
+
});
|
|
17043
|
+
function accountDeletionMarkerPath() {
|
|
17044
|
+
return join19(brandHome(), "delete-account.json");
|
|
17045
|
+
}
|
|
17046
|
+
async function readAccountDeletionMarker() {
|
|
17047
|
+
try {
|
|
17048
|
+
const parsed = MarkerSchema.safeParse(
|
|
17049
|
+
JSON.parse(await readFile18(accountDeletionMarkerPath(), "utf8"))
|
|
17050
|
+
);
|
|
17051
|
+
return parsed.success ? parsed.data : null;
|
|
17052
|
+
} catch {
|
|
17053
|
+
return null;
|
|
17054
|
+
}
|
|
17055
|
+
}
|
|
17056
|
+
async function accountDeletionMarkerExists() {
|
|
17057
|
+
return stat5(accountDeletionMarkerPath()).then(
|
|
17058
|
+
() => true,
|
|
17059
|
+
() => false
|
|
17060
|
+
);
|
|
17061
|
+
}
|
|
17062
|
+
async function saveAccountDeletionMarker(marker) {
|
|
17063
|
+
await mkdir12(brandHome(), { recursive: true });
|
|
17064
|
+
await writeThenRename(accountDeletionMarkerPath(), `${JSON.stringify(marker, null, 2)}
|
|
17065
|
+
`);
|
|
17066
|
+
}
|
|
17067
|
+
async function clearAccountDeletionMarker() {
|
|
17068
|
+
await unlink5(accountDeletionMarkerPath()).catch(() => void 0);
|
|
17069
|
+
}
|
|
17070
|
+
|
|
17071
|
+
// src/commands/delete-account.ts
|
|
16897
17072
|
init_config();
|
|
17073
|
+
init_credentials();
|
|
17074
|
+
init_pending_login();
|
|
17075
|
+
|
|
17076
|
+
// src/lib/registry-session.ts
|
|
17077
|
+
init_errors();
|
|
17078
|
+
init_credentials();
|
|
17079
|
+
init_version();
|
|
17080
|
+
function registryApiUrl(stored) {
|
|
17081
|
+
const override = process.env["KORDIS_API_URL"];
|
|
17082
|
+
return override !== void 0 && override !== "" ? override : stored;
|
|
17083
|
+
}
|
|
17084
|
+
async function requireRegistryCredentials() {
|
|
17085
|
+
const credentials = await readCredentials();
|
|
17086
|
+
const registry = credentials?.registry;
|
|
17087
|
+
if (!registry) {
|
|
17088
|
+
throw memgError(
|
|
17089
|
+
"not_linked",
|
|
17090
|
+
"this machine holds no registry identity",
|
|
17091
|
+
credentials === null ? "Nothing was sent. Sign in with `memgineering login` \u2014 it links the registry identity at the same time, so one sign-in covers both." : "Nothing was sent. This machine is signed in to a brain, and the registry half of that sign-in is missing \u2014 which is what a credential file written before federation looks like. Run `memgineering login` again; it links the registry identity too."
|
|
17092
|
+
);
|
|
17093
|
+
}
|
|
17094
|
+
return registry;
|
|
17095
|
+
}
|
|
17096
|
+
var RACE_WAIT_MS = 2e3;
|
|
17097
|
+
var RACE_POLL_MS = 50;
|
|
17098
|
+
function lostRace(err) {
|
|
17099
|
+
return err instanceof KordisApiError && err.status === 401 && isRefreshRace({ error: { code: err.code, flags: err.flags } });
|
|
17100
|
+
}
|
|
17101
|
+
async function newerRegistry(sent) {
|
|
17102
|
+
const deadline = Date.now() + RACE_WAIT_MS;
|
|
17103
|
+
for (; ; ) {
|
|
17104
|
+
const registry = (await readCredentials())?.registry;
|
|
17105
|
+
if (registry !== void 0 && registry.refresh_token !== sent) return registry;
|
|
17106
|
+
if (Date.now() >= deadline) return null;
|
|
17107
|
+
await new Promise((resolve17) => setTimeout(resolve17, RACE_POLL_MS));
|
|
17108
|
+
}
|
|
17109
|
+
}
|
|
17110
|
+
async function refreshRegistrySession(stored, opts = {}) {
|
|
17111
|
+
const attempt = async (registry) => {
|
|
17112
|
+
const baseUrl = (opts.apiUrlOf ?? ((r) => registryApiUrl(r.api_url)))(registry);
|
|
17113
|
+
const anonymous = new KordisClient({
|
|
17114
|
+
baseUrl,
|
|
17115
|
+
userAgent: `memgineering/${VERSION}`,
|
|
17116
|
+
...opts.fetchImpl ? { fetch: opts.fetchImpl } : {},
|
|
17117
|
+
...opts.requestTimeoutMs === void 0 ? {} : { requestTimeoutMs: opts.requestTimeoutMs }
|
|
17118
|
+
});
|
|
17119
|
+
return { registry, baseUrl, tokens: await anonymous.refreshSession(registry.refresh_token) };
|
|
17120
|
+
};
|
|
17121
|
+
try {
|
|
17122
|
+
return await attempt(stored);
|
|
17123
|
+
} catch (err) {
|
|
17124
|
+
if (!lostRace(err)) throw err;
|
|
17125
|
+
const newer = await newerRegistry(stored.refresh_token);
|
|
17126
|
+
if (newer === null) throw err;
|
|
17127
|
+
return attempt(newer);
|
|
17128
|
+
}
|
|
17129
|
+
}
|
|
17130
|
+
function refusal(err) {
|
|
17131
|
+
if (lostRace(err)) {
|
|
17132
|
+
return memgError(
|
|
17133
|
+
"not_authenticated",
|
|
17134
|
+
err.message,
|
|
17135
|
+
"This machine's registry sign-in was refreshed by another request at the same moment, so this command could not use it. Run this command again."
|
|
17136
|
+
);
|
|
17137
|
+
}
|
|
17138
|
+
const busyOrUnreachable = err instanceof KordisNetworkError || err instanceof KordisApiError && (err.status === 429 || err.status >= 500);
|
|
17139
|
+
return (busyOrUnreachable ? adaptRegistryError(err) : null) ?? memgError(
|
|
17140
|
+
"not_linked",
|
|
17141
|
+
`the registry refused this machine's stored sign-in: ${err instanceof Error ? err.message : String(err)}`,
|
|
17142
|
+
"Nothing was sent. The link between this brain and your registry account has expired or been revoked. `memgineering login` re-links both."
|
|
17143
|
+
);
|
|
17144
|
+
}
|
|
17145
|
+
async function openRegistrySession(fetchImpl) {
|
|
17146
|
+
const stored = await requireRegistryCredentials();
|
|
17147
|
+
let refreshed;
|
|
17148
|
+
try {
|
|
17149
|
+
refreshed = await refreshRegistrySession(stored, fetchImpl ? { fetchImpl } : {});
|
|
17150
|
+
} catch (err) {
|
|
17151
|
+
throw refusal(err);
|
|
17152
|
+
}
|
|
17153
|
+
const { registry, baseUrl, tokens } = refreshed;
|
|
17154
|
+
const credentialsRotated = await persistRotatedRefresh(
|
|
17155
|
+
registry,
|
|
17156
|
+
tokens.refresh_token,
|
|
17157
|
+
tokens.session_id
|
|
17158
|
+
);
|
|
17159
|
+
const client = new KordisClient({
|
|
17160
|
+
baseUrl,
|
|
17161
|
+
userAgent: `memgineering/${VERSION}`,
|
|
17162
|
+
getAccessToken: () => tokens.access_token,
|
|
17163
|
+
...fetchImpl ? { fetch: fetchImpl } : {}
|
|
17164
|
+
});
|
|
17165
|
+
return { client, username: registry.username, apiUrl: baseUrl, credentialsRotated };
|
|
17166
|
+
}
|
|
17167
|
+
async function persistRotatedRefresh(sent, refreshToken, sessionId) {
|
|
17168
|
+
if (refreshToken === sent.refresh_token && sessionId === sent.session_id) return false;
|
|
17169
|
+
try {
|
|
17170
|
+
return await saveRotatedRegistry(sent.refresh_token, {
|
|
17171
|
+
refresh_token: refreshToken,
|
|
17172
|
+
session_id: sessionId
|
|
17173
|
+
});
|
|
17174
|
+
} catch {
|
|
17175
|
+
return false;
|
|
17176
|
+
}
|
|
17177
|
+
}
|
|
17178
|
+
|
|
17179
|
+
// src/commands/delete-account.ts
|
|
16898
17180
|
init_ui();
|
|
16899
|
-
|
|
16900
|
-
|
|
16901
|
-
function
|
|
16902
|
-
return new Command12("delete-
|
|
17181
|
+
var DELETE_ACCOUNT_PHRASE = "delete my account";
|
|
17182
|
+
var SUPPORT_ADDRESS = "support@memgineering.com";
|
|
17183
|
+
function deleteAccountCommand() {
|
|
17184
|
+
return new Command12("delete-account").description("delete your hosted brain account and your registry account, for good").option(
|
|
17185
|
+
"--confirm <phrase>",
|
|
17186
|
+
`the sentence "${DELETE_ACCOUNT_PHRASE}", typed exactly, authorising the deletion`
|
|
17187
|
+
).addHelpText(
|
|
16903
17188
|
"after",
|
|
16904
|
-
|
|
16905
|
-
|
|
16906
|
-
|
|
17189
|
+
`
|
|
17190
|
+
Irreversible, and server-side, in this order: the hosted brain account (every
|
|
17191
|
+
brain on it, every note, every token), then the registry account made by the
|
|
17192
|
+
same Google sign-in (profile, stars, follows, comment text, sign-ins; the account
|
|
17193
|
+
is renamed deleted-\u2026, and published packages stay at their existing addresses).
|
|
17194
|
+
Then this machine is signed out.
|
|
17195
|
+
Folders on this disk are never touched. Run this ONLY when the person has
|
|
17196
|
+
explicitly asked for their account to be deleted, and pass --confirm "${DELETE_ACCOUNT_PHRASE}".
|
|
17197
|
+
If a run stops halfway, run it again: it finishes the side that is left.
|
|
17198
|
+
\`memgineering delete-brain\` deletes one brain and keeps the account;
|
|
17199
|
+
\`memgineering logout\` only signs this machine out.`
|
|
17200
|
+
).action(async (opts) => {
|
|
17201
|
+
const confirmFlag = opts.confirm;
|
|
17202
|
+
if (confirmFlag !== void 0 && confirmFlag.startsWith("--")) {
|
|
16907
17203
|
throw memgError(
|
|
16908
17204
|
"invalid_input",
|
|
16909
|
-
|
|
16910
|
-
|
|
17205
|
+
`\`--confirm\` was given the flag \`${oneLine(confirmFlag, WIDTH.title)}\` as its sentence`,
|
|
17206
|
+
`Nothing was deleted. \`--confirm\` takes the sentence "${DELETE_ACCOUNT_PHRASE}", and the flag written after it was read as that sentence. Put the sentence, in quotes, immediately after \`--confirm\`.`
|
|
16911
17207
|
);
|
|
16912
17208
|
}
|
|
16913
|
-
const
|
|
16914
|
-
|
|
17209
|
+
const server = apiUrl();
|
|
17210
|
+
const stored = await readCredentials();
|
|
17211
|
+
const saved = await readAccountDeletionMarker();
|
|
17212
|
+
const marker = saved !== null && (stored === null || stored.account_id === saved.brain.account_id) ? saved : null;
|
|
17213
|
+
const brainDone = marker?.brain.deleted_at != null;
|
|
17214
|
+
const registryDone = marker?.registry.deleted_at != null;
|
|
17215
|
+
if (!brainDone) requireBrainSignIn(stored, server);
|
|
17216
|
+
if (!registryDone && stored?.registry === void 0) {
|
|
17217
|
+
throw noRegistrySignIn(marker);
|
|
17218
|
+
}
|
|
17219
|
+
if (confirmFlag !== void 0 && confirmFlag !== DELETE_ACCOUNT_PHRASE) {
|
|
16915
17220
|
throw memgError(
|
|
16916
17221
|
"invalid_input",
|
|
16917
|
-
|
|
16918
|
-
|
|
17222
|
+
"the confirmation is not the sentence this needs",
|
|
17223
|
+
`Nothing was deleted. The sentence is "${DELETE_ACCOUNT_PHRASE}" \u2014 exactly that, lower case, no punctuation. It deletes the hosted brain account with every brain and note on it, and the registry account; none of it comes back. Pass --confirm "${DELETE_ACCOUNT_PHRASE}" only if the person explicitly asked for their account to be deleted.`
|
|
16919
17224
|
);
|
|
16920
17225
|
}
|
|
16921
|
-
|
|
16922
|
-
|
|
16923
|
-
|
|
16924
|
-
|
|
16925
|
-
|
|
16926
|
-
|
|
16927
|
-
if (authorised === void 0) {
|
|
16928
|
-
printHuman(c.yellow("Cancelled \u2014 nothing was deleted."));
|
|
16929
|
-
return;
|
|
17226
|
+
if (confirmFlag === void 0 && !isInteractive()) {
|
|
17227
|
+
throw memgError(
|
|
17228
|
+
"invalid_input",
|
|
17229
|
+
"cannot ask for confirmation here \u2014 this is not an interactive terminal",
|
|
17230
|
+
`Nothing was deleted. Two ways forward: run this in a terminal the person can see and let them type "${DELETE_ACCOUNT_PHRASE}"; or, once they have explicitly asked for their account to be deleted, pass --confirm "${DELETE_ACCOUNT_PHRASE}". Deciding for them is the one thing this gate exists to prevent.`
|
|
17231
|
+
);
|
|
16930
17232
|
}
|
|
16931
|
-
const
|
|
16932
|
-
const
|
|
16933
|
-
const
|
|
16934
|
-
|
|
16935
|
-
|
|
16936
|
-
|
|
16937
|
-
|
|
16938
|
-
|
|
16939
|
-
|
|
16940
|
-
...cfg.brain.default === void 0 ? {} : { default: cfg.brain.default }
|
|
16941
|
-
}
|
|
16942
|
-
});
|
|
17233
|
+
const brain = brainDone ? null : await brainAccount(stored, marker);
|
|
17234
|
+
const registry = registryDone ? null : await registryAccount(stored?.registry, marker);
|
|
17235
|
+
const match = matchAccounts({ stored, marker, brain, registry });
|
|
17236
|
+
if (confirmFlag === void 0) {
|
|
17237
|
+
const typed = await retypeThePhrase({ brain, registry, marker, match });
|
|
17238
|
+
if (typed !== DELETE_ACCOUNT_PHRASE) {
|
|
17239
|
+
printHuman(c.yellow("Cancelled \u2014 nothing was deleted."));
|
|
17240
|
+
return;
|
|
17241
|
+
}
|
|
16943
17242
|
}
|
|
16944
|
-
const
|
|
17243
|
+
const record = marker ?? {
|
|
17244
|
+
version: 1,
|
|
17245
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17246
|
+
matched_by: match.by === "earlier-run" ? match.matched_by : match.by,
|
|
17247
|
+
brain: {
|
|
17248
|
+
api_url: stored.api_url,
|
|
17249
|
+
account_id: brain.id,
|
|
17250
|
+
requested_at: null,
|
|
17251
|
+
deleted_at: null
|
|
17252
|
+
},
|
|
17253
|
+
registry: {
|
|
17254
|
+
api_url: registry.apiUrl,
|
|
17255
|
+
user_id: registry.id,
|
|
17256
|
+
username: registry.username,
|
|
17257
|
+
requested_at: null,
|
|
17258
|
+
deleted_at: null
|
|
17259
|
+
}
|
|
17260
|
+
};
|
|
17261
|
+
try {
|
|
17262
|
+
await saveAccountDeletionMarker(record);
|
|
17263
|
+
} catch (err) {
|
|
17264
|
+
throw memgError(
|
|
17265
|
+
"filesystem_error",
|
|
17266
|
+
`could not write ${accountDeletionMarkerPath()}`,
|
|
17267
|
+
`Nothing was deleted. This command records its progress there so a run that stops halfway can be finished, and it will not start without it. Free the disk or fix the permissions of that folder, then run this again. Underlying cause: ${err instanceof Error ? err.message : String(err)}`
|
|
17268
|
+
);
|
|
17269
|
+
}
|
|
17270
|
+
if (brain !== null) {
|
|
17271
|
+
record.brain.requested_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
17272
|
+
await saveBestEffort(record);
|
|
17273
|
+
await deleteBrainAccount(stored, registry);
|
|
17274
|
+
record.brain.deleted_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
17275
|
+
await saveBestEffort(record);
|
|
17276
|
+
}
|
|
17277
|
+
if (registry !== null) {
|
|
17278
|
+
record.registry.requested_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
17279
|
+
await saveBestEffort(record);
|
|
17280
|
+
await deleteRegistryAccount(registry);
|
|
17281
|
+
record.registry.deleted_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
17282
|
+
await saveBestEffort(record);
|
|
17283
|
+
}
|
|
17284
|
+
const pointerCleared = await signOutHere(record.brain.api_url);
|
|
17285
|
+
await clearAccountDeletionMarker();
|
|
17286
|
+
const hint = "Both accounts are gone and this machine is signed out. Signing in again with the same Google account creates a new, empty account. Folders on this disk were not touched.";
|
|
16945
17287
|
printDual({
|
|
16946
17288
|
json: {
|
|
16947
|
-
deleted:
|
|
16948
|
-
|
|
16949
|
-
|
|
16950
|
-
|
|
17289
|
+
deleted: true,
|
|
17290
|
+
brain: {
|
|
17291
|
+
status: brain === null ? "deleted-earlier" : "deleted",
|
|
17292
|
+
account_id: record.brain.account_id,
|
|
17293
|
+
api_url: record.brain.api_url
|
|
17294
|
+
},
|
|
17295
|
+
registry: {
|
|
17296
|
+
status: registry === null ? "deleted-earlier" : "deleted",
|
|
17297
|
+
user_id: record.registry.user_id,
|
|
17298
|
+
username: record.registry.username,
|
|
17299
|
+
api_url: record.registry.api_url
|
|
17300
|
+
},
|
|
17301
|
+
matched: match,
|
|
17302
|
+
credentials_cleared: true,
|
|
16951
17303
|
pointer_cleared: pointerCleared,
|
|
17304
|
+
local_folders: "untouched",
|
|
16952
17305
|
hint
|
|
16953
17306
|
},
|
|
16954
17307
|
human: () => {
|
|
17308
|
+
printHuman(c.green("\u2713 Account deleted."));
|
|
16955
17309
|
printHuman(
|
|
16956
|
-
c.
|
|
17310
|
+
c.gray(
|
|
17311
|
+
brain === null ? " brain the hosted account was deleted by an earlier run" : " brain the hosted account, every brain and note on it, and every token \u2014 gone"
|
|
17312
|
+
)
|
|
16957
17313
|
);
|
|
16958
17314
|
printHuman(
|
|
16959
17315
|
c.gray(
|
|
16960
|
-
|
|
17316
|
+
` registry ${oneLine(record.registry.username, WIDTH.title)} \u2014 ` + (registry === null ? "deleted by an earlier run" : "profile, stars, follows, comment text and sign-ins removed; renamed deleted-\u2026; published packages stay at their existing addresses")
|
|
16961
17317
|
)
|
|
16962
17318
|
);
|
|
17319
|
+
printHuman(c.gray(` matched ${matchLine(match)}`));
|
|
17320
|
+
printHuman(c.gray(" here signed out; this machine\u2019s credentials are removed"));
|
|
16963
17321
|
if (pointerCleared) {
|
|
16964
|
-
printHuman(
|
|
16965
|
-
c.yellow(
|
|
16966
|
-
`
|
|
16967
|
-
\u26A0 THIS MACHINE NO LONGER READS A HOSTED BRAIN \u2014 it was reading \u201C${oneLine(deleted.name, WIDTH.title)}\u201D, which is now deleted.`
|
|
16968
|
-
)
|
|
16969
|
-
);
|
|
16970
|
-
printHuman(
|
|
16971
|
-
c.gray(
|
|
16972
|
-
" `memgineering brains` lists what is left; `use --brain <name>` points at one.\n `recall --local` still reads a folder, if one is linked."
|
|
16973
|
-
)
|
|
16974
|
-
);
|
|
17322
|
+
printHuman(c.gray(" and it no longer points at a hosted brain"));
|
|
16975
17323
|
}
|
|
16976
17324
|
printHuman(c.gray(`
|
|
16977
17325
|
${hint}`));
|
|
@@ -16979,7 +17327,408 @@ ${hint}`));
|
|
|
16979
17327
|
});
|
|
16980
17328
|
});
|
|
16981
17329
|
}
|
|
16982
|
-
|
|
17330
|
+
function requireBrainSignIn(stored, server) {
|
|
17331
|
+
if (stored !== null && stored.api_url === server) return;
|
|
17332
|
+
if (stored !== null) {
|
|
17333
|
+
throw memgError(
|
|
17334
|
+
"not_signed_in",
|
|
17335
|
+
`this machine is signed in to ${oneLine(stored.api_url, WIDTH.path)}, not ${oneLine(server, WIDTH.path)}`,
|
|
17336
|
+
"Nothing was deleted. The account to delete is on the server this machine signed in to. If MEMGINEERING_API_URL points somewhere else, unset it and run this again."
|
|
17337
|
+
);
|
|
17338
|
+
}
|
|
17339
|
+
throw memgError(
|
|
17340
|
+
"not_signed_in",
|
|
17341
|
+
"this machine is not signed in to a memgineering account",
|
|
17342
|
+
`Nothing was deleted, and there may be nothing to delete. A brain kept in a folder on this machine has no account \u2014 it is the person\u2019s own files, and this command never touches them; deleting the folder deletes it (\`memgineering unlink <folder>\` first makes this tool forget it). To delete a hosted account, sign in to it first \u2014 \`memgineering login --emit-only\`, show the person the code it prints, and run this again once they approve. Without a sign-in, mail ${SUPPORT_ADDRESS} from the Google address of the account.`
|
|
17343
|
+
);
|
|
17344
|
+
}
|
|
17345
|
+
function noRegistrySignIn(marker) {
|
|
17346
|
+
if (marker !== null) {
|
|
17347
|
+
return memgError(
|
|
17348
|
+
"not_linked",
|
|
17349
|
+
"the brain account is already deleted, and this machine holds no registry sign-in to finish with",
|
|
17350
|
+
`The hosted brain account was deleted by an earlier run of this command and cannot be undone. The registry account \`${oneLine(marker.registry.username, WIDTH.title)}\` still needs deleting, and that needs this machine's registry sign-in, which is gone. Mail ${SUPPORT_ADDRESS} from the Google address of the account, naming \`${oneLine(marker.registry.username, WIDTH.title)}\`, and it is deleted by hand.`
|
|
17351
|
+
);
|
|
17352
|
+
}
|
|
17353
|
+
return memgError(
|
|
17354
|
+
"not_linked",
|
|
17355
|
+
"this machine is signed in to a brain account but holds no registry sign-in",
|
|
17356
|
+
`Nothing was deleted. This command deletes the brain account and the registry account together, and will not delete one alone. The registry half of the sign-in is missing \u2014 which is what a sign-in made before the two were linked looks like. Run \`memgineering logout\` and then \`memgineering login\` (it links the registry account too), then run this again. Or mail ${SUPPORT_ADDRESS} from the Google address of the account.`
|
|
17357
|
+
);
|
|
17358
|
+
}
|
|
17359
|
+
async function brainAccount(stored, marker) {
|
|
17360
|
+
let body;
|
|
17361
|
+
try {
|
|
17362
|
+
body = (await apiRequest({
|
|
17363
|
+
method: "GET",
|
|
17364
|
+
path: "/v1/me",
|
|
17365
|
+
token: stored.token,
|
|
17366
|
+
baseUrl: stored.api_url
|
|
17367
|
+
})).body;
|
|
17368
|
+
} catch (err) {
|
|
17369
|
+
if (!(err instanceof MemgError)) throw err;
|
|
17370
|
+
if (err.code === "not_authenticated" && marker?.brain.requested_at) {
|
|
17371
|
+
throw memgError(
|
|
17372
|
+
"not_authenticated",
|
|
17373
|
+
"this machine\u2019s brain sign-in no longer works, after an earlier run asked for the account to be deleted",
|
|
17374
|
+
`Nothing more was deleted. An earlier run of this command asked the brain server to delete this account at ${marker.brain.requested_at} and did not hear the answer. A sign-in that stops working is what a deleted account looks like, and also what a revoked sign-in looks like, so this command will not guess and delete the registry account on that basis. Mail ${SUPPORT_ADDRESS} from the Google address of the account, naming registry account \`${oneLine(marker.registry.username, WIDTH.title)}\`, and both are finished by hand.`
|
|
17375
|
+
);
|
|
17376
|
+
}
|
|
17377
|
+
if (err.code === "not_authenticated") {
|
|
17378
|
+
const name = stored.registry === void 0 ? "" : ` \`${oneLine(stored.registry.username, WIDTH.title)}\``;
|
|
17379
|
+
const file = await accountDeletionMarkerExists() ? ` A progress file from an earlier run is on disk (${accountDeletionMarkerPath()}) but could not be used for this account.` : "";
|
|
17380
|
+
throw memgError(
|
|
17381
|
+
"not_authenticated",
|
|
17382
|
+
"the brain server no longer accepts this machine\u2019s sign-in",
|
|
17383
|
+
`This run deleted nothing. Either the sign-in was revoked or has expired \u2014 then \`memgineering login --emit-only\`, show the person the code it prints, and run this again once they approve \u2014 or this account was already deleted by an earlier run of this command whose progress file is gone.${file} If that may be so, the registry account${name} may still exist: mail ${SUPPORT_ADDRESS} from the Google address of the account${name === "" ? "" : `, naming${name}`}.`
|
|
17384
|
+
);
|
|
17385
|
+
}
|
|
17386
|
+
if (err.code === "server_unreachable") {
|
|
17387
|
+
throw memgError(
|
|
17388
|
+
"server_unreachable",
|
|
17389
|
+
err.message,
|
|
17390
|
+
`This run deleted nothing \u2014 neither the brain account nor the registry account. The brain server could not be reached: check the connection, or MEMGINEERING_API_URL if the brain server is somewhere else, then run this again.`
|
|
17391
|
+
);
|
|
17392
|
+
}
|
|
17393
|
+
throw new MemgError({
|
|
17394
|
+
code: err.code,
|
|
17395
|
+
message: err.message,
|
|
17396
|
+
hint: `This run deleted nothing \u2014 neither the brain account nor the registry account. ${err.hint}`
|
|
17397
|
+
});
|
|
17398
|
+
}
|
|
17399
|
+
const account = body?.account;
|
|
17400
|
+
if (!account || typeof account.id !== "string") {
|
|
17401
|
+
throw memgError(
|
|
17402
|
+
"server_refused",
|
|
17403
|
+
`${stored.api_url} answered /v1/me without an account`,
|
|
17404
|
+
"Nothing was deleted. This is not an answer a memgineering brain server gives. If MEMGINEERING_API_URL points at something else, unset it and run this again."
|
|
17405
|
+
);
|
|
17406
|
+
}
|
|
17407
|
+
if (account.id !== stored.account_id) {
|
|
17408
|
+
throw mismatch(
|
|
17409
|
+
`this machine's credentials name brain account ${oneLine(stored.account_id, WIDTH.handle)}, and its token belongs to ${oneLine(account.id, WIDTH.handle)}`
|
|
17410
|
+
);
|
|
17411
|
+
}
|
|
17412
|
+
return {
|
|
17413
|
+
id: account.id,
|
|
17414
|
+
email: typeof account.email === "string" && account.email !== "" ? account.email : null,
|
|
17415
|
+
display_name: typeof account.display_name === "string" ? account.display_name : null
|
|
17416
|
+
};
|
|
17417
|
+
}
|
|
17418
|
+
async function registryAccount(snapshot, marker) {
|
|
17419
|
+
let session;
|
|
17420
|
+
try {
|
|
17421
|
+
session = await openRegistrySession();
|
|
17422
|
+
} catch (err) {
|
|
17423
|
+
if (err instanceof MemgError && err.code === "not_linked") {
|
|
17424
|
+
throw memgError(
|
|
17425
|
+
"not_linked",
|
|
17426
|
+
err.message,
|
|
17427
|
+
marker !== null && marker.brain.deleted_at !== null ? `The hosted brain account was deleted by an earlier run and cannot be undone. The registry account \`${oneLine(marker.registry.username, WIDTH.title)}\` still needs deleting, and this machine's registry sign-in no longer works \u2014 if an earlier run's registry request reached the server, the account may already be gone, which ends sign-ins the same way. Mail ${SUPPORT_ADDRESS} from the Google address of the account, naming \`${oneLine(marker.registry.username, WIDTH.title)}\`, and it is finished by hand.` : `Nothing was deleted. The registry sign-in on this machine has expired or been revoked. Run \`memgineering logout\` and then \`memgineering login\` (it links the registry account again), then run this again. Or mail ${SUPPORT_ADDRESS} from the Google address of the account.`
|
|
17428
|
+
);
|
|
17429
|
+
}
|
|
17430
|
+
throw withNothingDeleted(err, marker);
|
|
17431
|
+
}
|
|
17432
|
+
let exported;
|
|
17433
|
+
try {
|
|
17434
|
+
exported = await session.client.exportMe();
|
|
17435
|
+
} catch (err) {
|
|
17436
|
+
throw withNothingDeleted(err, marker);
|
|
17437
|
+
}
|
|
17438
|
+
const user = exported?.user;
|
|
17439
|
+
if (!user || typeof user.id !== "string" || typeof user.username !== "string") {
|
|
17440
|
+
throw memgError(
|
|
17441
|
+
"server_refused",
|
|
17442
|
+
`${session.apiUrl} answered /api/me/export without an account`,
|
|
17443
|
+
"Nothing was deleted. This is not an answer the registry gives. If KORDIS_API_URL points at something else, unset it and run this again."
|
|
17444
|
+
);
|
|
17445
|
+
}
|
|
17446
|
+
if (snapshot.user_id !== user.id) {
|
|
17447
|
+
throw mismatch(
|
|
17448
|
+
`this run began with a registry sign-in for registry account ${oneLine(snapshot.user_id, WIDTH.handle)}, and the registry answered for ${oneLine(user.id, WIDTH.handle)}`
|
|
17449
|
+
);
|
|
17450
|
+
}
|
|
17451
|
+
return {
|
|
17452
|
+
client: session.client,
|
|
17453
|
+
apiUrl: session.apiUrl,
|
|
17454
|
+
id: user.id,
|
|
17455
|
+
username: user.username,
|
|
17456
|
+
email: typeof user.email === "string" && user.email !== "" ? user.email : null,
|
|
17457
|
+
linkedAt: snapshot.linked_at
|
|
17458
|
+
};
|
|
17459
|
+
}
|
|
17460
|
+
function withNothingDeleted(err, marker) {
|
|
17461
|
+
const adapted = err instanceof MemgError ? err : adaptRegistryError(err);
|
|
17462
|
+
if (adapted === null) return err;
|
|
17463
|
+
const prefix = marker !== null && marker.brain.deleted_at !== null ? "Nothing more was deleted: the brain account is already gone, and the registry account is not deleted yet \u2014 run this again once the registry answers." : "Nothing was deleted \u2014 neither the brain account nor the registry account.";
|
|
17464
|
+
return new MemgError({
|
|
17465
|
+
code: adapted.code,
|
|
17466
|
+
message: adapted.message,
|
|
17467
|
+
hint: `${prefix} ${adapted.hint}`
|
|
17468
|
+
});
|
|
17469
|
+
}
|
|
17470
|
+
function matchAccounts(args) {
|
|
17471
|
+
const { marker, brain, registry } = args;
|
|
17472
|
+
if (marker !== null) {
|
|
17473
|
+
if (registry !== null && registry.id !== marker.registry.user_id) {
|
|
17474
|
+
throw mismatch(
|
|
17475
|
+
`an earlier run matched registry account \`${oneLine(marker.registry.username, WIDTH.title)}\`, and this machine's registry sign-in is now for \`${oneLine(registry.username, WIDTH.title)}\``
|
|
17476
|
+
);
|
|
17477
|
+
}
|
|
17478
|
+
return { by: "earlier-run", matched_by: marker.matched_by };
|
|
17479
|
+
}
|
|
17480
|
+
const b = brain;
|
|
17481
|
+
const r = registry;
|
|
17482
|
+
if (b.email !== null && r.email !== null) {
|
|
17483
|
+
if (normaliseEmail(b.email) !== normaliseEmail(r.email)) {
|
|
17484
|
+
throw mismatch(
|
|
17485
|
+
`the brain account's email is ${oneLine(b.email, WIDTH.title)} and the registry account's is ${oneLine(r.email, WIDTH.title)}`
|
|
17486
|
+
);
|
|
17487
|
+
}
|
|
17488
|
+
return { by: "email", email: b.email };
|
|
17489
|
+
}
|
|
17490
|
+
return { by: "federation", linked_at: r.linkedAt };
|
|
17491
|
+
}
|
|
17492
|
+
function normaliseEmail(email) {
|
|
17493
|
+
return email.trim().toLowerCase();
|
|
17494
|
+
}
|
|
17495
|
+
function mismatch(detail) {
|
|
17496
|
+
return memgError(
|
|
17497
|
+
"account_mismatch",
|
|
17498
|
+
`the brain account and the registry account do not look like the same person: ${detail}`,
|
|
17499
|
+
`Nothing was deleted. This command deletes both accounts together and only when they are one person's. Run \`memgineering logout\` and then \`memgineering login\` with the Google account whose accounts should be deleted, then run this again. If they really are one person's, mail ${SUPPORT_ADDRESS} from that Google address.`
|
|
17500
|
+
);
|
|
17501
|
+
}
|
|
17502
|
+
function matchLine(match) {
|
|
17503
|
+
switch (match.by) {
|
|
17504
|
+
case "email":
|
|
17505
|
+
return `same person \u2014 both accounts carry ${oneLine(match.email, WIDTH.title)}`;
|
|
17506
|
+
case "federation":
|
|
17507
|
+
return `same person \u2014 one of the two accounts has no email on record, so the match is this machine's registry sign-in, which the brain sign-in issued for the same Google account (linked ${oneLine(match.linked_at, WIDTH.title)})`;
|
|
17508
|
+
case "earlier-run":
|
|
17509
|
+
return `same person \u2014 matched by ${match.matched_by} before an earlier run deleted anything`;
|
|
17510
|
+
}
|
|
17511
|
+
}
|
|
17512
|
+
async function retypeThePhrase(args) {
|
|
17513
|
+
const { brain, registry, marker, match } = args;
|
|
17514
|
+
printHuman(c.bold("\nDelete your account?"));
|
|
17515
|
+
if (brain !== null) {
|
|
17516
|
+
const who = brain.email ?? brain.display_name ?? brain.id;
|
|
17517
|
+
printHuman(
|
|
17518
|
+
c.gray(
|
|
17519
|
+
` brain ${oneLine(who, WIDTH.title)} \u2014 every brain on it, every note, every token`
|
|
17520
|
+
)
|
|
17521
|
+
);
|
|
17522
|
+
} else {
|
|
17523
|
+
printHuman(c.gray(" brain already deleted by an earlier run"));
|
|
17524
|
+
}
|
|
17525
|
+
const username = registry?.username ?? marker?.registry.username ?? "";
|
|
17526
|
+
printHuman(
|
|
17527
|
+
c.gray(
|
|
17528
|
+
registry !== null ? ` registry ${oneLine(username, WIDTH.title)} \u2014 profile, stars, follows, comment text, sign-ins` : ` registry ${oneLine(username, WIDTH.title)} \u2014 already deleted by an earlier run`
|
|
17529
|
+
)
|
|
17530
|
+
);
|
|
17531
|
+
printHuman(c.gray(` matched ${matchLine(match)}`));
|
|
17532
|
+
printHuman(
|
|
17533
|
+
c.yellow(
|
|
17534
|
+
"\n None of this comes back. The registry account is renamed deleted-\u2026; published\n packages stay at their existing addresses. Folders on this disk are not\n touched. Signing in again later makes a new, empty account."
|
|
17535
|
+
)
|
|
17536
|
+
);
|
|
17537
|
+
const { typed } = await prompts({
|
|
17538
|
+
type: "text",
|
|
17539
|
+
name: "typed",
|
|
17540
|
+
message: `Type "${DELETE_ACCOUNT_PHRASE}" to delete both accounts`
|
|
17541
|
+
});
|
|
17542
|
+
return typed;
|
|
17543
|
+
}
|
|
17544
|
+
async function deleteBrainAccount(stored, registry) {
|
|
17545
|
+
try {
|
|
17546
|
+
await apiRequest({
|
|
17547
|
+
method: "DELETE",
|
|
17548
|
+
path: "/v1/account",
|
|
17549
|
+
token: stored.token,
|
|
17550
|
+
baseUrl: stored.api_url,
|
|
17551
|
+
body: { confirm: DELETE_ACCOUNT_PHRASE }
|
|
17552
|
+
});
|
|
17553
|
+
return;
|
|
17554
|
+
} catch (err) {
|
|
17555
|
+
if (!(err instanceof MemgError)) throw err;
|
|
17556
|
+
if (err.code === "server_unreachable" && await brainTokenIsDead(stored)) {
|
|
17557
|
+
return;
|
|
17558
|
+
}
|
|
17559
|
+
const registryLine = registry === null ? "" : ` The registry account \`${oneLine(registry.username, WIDTH.title)}\` was not touched.`;
|
|
17560
|
+
if (err.code === "server_unreachable") {
|
|
17561
|
+
throw new MemgError({
|
|
17562
|
+
code: err.code,
|
|
17563
|
+
message: err.message,
|
|
17564
|
+
hint: `The brain server did not confirm the deletion, and the request may have reached it.${registryLine} Check the connection, then run this command again: it asks the server first. If it then says this machine's brain sign-in no longer works, the brain account was deleted \u2014 it will not delete the registry account on that basis; it says how to finish by mail.`
|
|
17565
|
+
});
|
|
17566
|
+
}
|
|
17567
|
+
throw new MemgError({
|
|
17568
|
+
code: err.code,
|
|
17569
|
+
message: err.message,
|
|
17570
|
+
hint: `The brain account was not confirmed deleted.${registryLine} ${err.hint} Then run this again.`
|
|
17571
|
+
});
|
|
17572
|
+
}
|
|
17573
|
+
}
|
|
17574
|
+
async function brainTokenIsDead(stored) {
|
|
17575
|
+
try {
|
|
17576
|
+
await apiRequest({
|
|
17577
|
+
method: "GET",
|
|
17578
|
+
path: "/v1/me",
|
|
17579
|
+
token: stored.token,
|
|
17580
|
+
baseUrl: stored.api_url
|
|
17581
|
+
});
|
|
17582
|
+
return false;
|
|
17583
|
+
} catch (err) {
|
|
17584
|
+
return err instanceof MemgError && err.code === "not_authenticated";
|
|
17585
|
+
}
|
|
17586
|
+
}
|
|
17587
|
+
async function deleteRegistryAccount(registry) {
|
|
17588
|
+
try {
|
|
17589
|
+
await registry.client.deleteMe({ confirm_username: registry.username });
|
|
17590
|
+
return;
|
|
17591
|
+
} catch (err) {
|
|
17592
|
+
if (err instanceof KordisNetworkError && await registrySessionIsDead(registry)) {
|
|
17593
|
+
return;
|
|
17594
|
+
}
|
|
17595
|
+
const adapted = adaptRegistryError(err) ?? memgError(
|
|
17596
|
+
"server_refused",
|
|
17597
|
+
err instanceof Error ? err.message : String(err),
|
|
17598
|
+
"The registry refused the request without saying why."
|
|
17599
|
+
);
|
|
17600
|
+
const name = oneLine(registry.username, WIDTH.title);
|
|
17601
|
+
const unconfirmed = err instanceof KordisNetworkError;
|
|
17602
|
+
const said = err instanceof KordisApiError && err.hint !== void 0 && err.hint !== "" ? ` The registry said: ${err.hint}` : "";
|
|
17603
|
+
throw memgError(
|
|
17604
|
+
"account_partly_deleted",
|
|
17605
|
+
unconfirmed ? `the hosted brain account is deleted; the registry did not confirm deleting \`${name}\` \u2014 ${adapted.code}: ${oneLine(adapted.message, WIDTH.text)}` : `the hosted brain account is deleted; the registry account \`${name}\` is not \u2014 ${adapted.code}: ${oneLine(adapted.message, WIDTH.text)}`,
|
|
17606
|
+
`The brain half is done and cannot be undone. ${unconfirmed ? `The registry did not answer, so it is not known whether \`${name}\` was deleted.` : `The registry account \`${name}\` still exists.`}${said}
|
|
17607
|
+
Run \`memgineering delete-account --confirm "${DELETE_ACCOUNT_PHRASE}"\` again: it skips the brain and deletes only the registry account${unconfirmed ? " if it is still there; if this machine's registry sign-in then no longer works, the account is gone or the sign-in was revoked, and it says how to finish by mail" : ""}. If it keeps failing, mail ${SUPPORT_ADDRESS} from the Google address of the account, naming \`${name}\`.`
|
|
17608
|
+
);
|
|
17609
|
+
}
|
|
17610
|
+
}
|
|
17611
|
+
async function registrySessionIsDead(registry) {
|
|
17612
|
+
try {
|
|
17613
|
+
await registry.client.exportMe();
|
|
17614
|
+
return false;
|
|
17615
|
+
} catch (err) {
|
|
17616
|
+
return err instanceof KordisApiError && err.status === 401;
|
|
17617
|
+
}
|
|
17618
|
+
}
|
|
17619
|
+
async function signOutHere(brainApiUrl) {
|
|
17620
|
+
await clearPendingLogin();
|
|
17621
|
+
await clearSetupFollowUps();
|
|
17622
|
+
await clearCredentials();
|
|
17623
|
+
const cfg = await loadConfig();
|
|
17624
|
+
const pointer = cfg.brain.cloud;
|
|
17625
|
+
if (pointer === void 0 || pointer.api_url !== brainApiUrl) return false;
|
|
17626
|
+
await saveConfig({
|
|
17627
|
+
...cfg,
|
|
17628
|
+
brain: {
|
|
17629
|
+
brains: cfg.brain.brains,
|
|
17630
|
+
...cfg.brain.default === void 0 ? {} : { default: cfg.brain.default }
|
|
17631
|
+
}
|
|
17632
|
+
});
|
|
17633
|
+
return true;
|
|
17634
|
+
}
|
|
17635
|
+
async function saveBestEffort(marker) {
|
|
17636
|
+
try {
|
|
17637
|
+
await saveAccountDeletionMarker(marker);
|
|
17638
|
+
} catch {
|
|
17639
|
+
}
|
|
17640
|
+
}
|
|
17641
|
+
|
|
17642
|
+
// src/commands/delete-brain.ts
|
|
17643
|
+
init_errors();
|
|
17644
|
+
init_api_client();
|
|
17645
|
+
init_cloud_brain();
|
|
17646
|
+
init_config();
|
|
17647
|
+
init_ui();
|
|
17648
|
+
import { Command as Command13 } from "commander";
|
|
17649
|
+
import prompts2 from "prompts";
|
|
17650
|
+
function deleteBrainCommand() {
|
|
17651
|
+
return new Command13("delete-brain").description("delete a hosted brain and everything in it, for good").argument("<name-or-id>", "which hosted brain to delete, by its name or id").option("--confirm <exact-name>", "the brain\u2019s name, typed exactly, authorising the deletion").addHelpText(
|
|
17652
|
+
"after",
|
|
17653
|
+
"\nIrreversible, and server-side: the brain, every note in it, its ledger and its\nmeasurements. `memgineering unlink --brain` is the other thing \u2014 it only stops\nthis machine reading a brain and deletes nothing. Run this ONLY when the person\nhas explicitly asked for this brain to be deleted, and pass --confirm with the\nexact name. A wrongly-named brain is a rename, not a delete."
|
|
17654
|
+
).action(async (named, opts) => {
|
|
17655
|
+
if (named.startsWith("--")) {
|
|
17656
|
+
throw memgError(
|
|
17657
|
+
"invalid_input",
|
|
17658
|
+
`\`delete-brain\` was given the flag \`${oneLine(named, 32)}\` as a brain name`,
|
|
17659
|
+
'Nothing was deleted. The first argument is WHICH BRAIN to delete, and the flag written there was read as its name. Put the name or id first \u2014 `memgineering delete-brain <name-or-id> --confirm "<name>"`. If a brain really is called that, pass its id instead; `memgineering brains` lists them.'
|
|
17660
|
+
);
|
|
17661
|
+
}
|
|
17662
|
+
const confirmFlag = opts.confirm;
|
|
17663
|
+
if (confirmFlag !== void 0 && confirmFlag.startsWith("--")) {
|
|
17664
|
+
throw memgError(
|
|
17665
|
+
"invalid_input",
|
|
17666
|
+
`\`--confirm\` was given the flag \`${oneLine(confirmFlag, WIDTH.title)}\` as a name`,
|
|
17667
|
+
"Nothing was deleted. `--confirm` takes the brain\u2019s own name, typed exactly, and the flag written after it was read as that name. Put the name immediately after `--confirm`, then pass the other flag separately."
|
|
17668
|
+
);
|
|
17669
|
+
}
|
|
17670
|
+
const server = apiUrl();
|
|
17671
|
+
const token = await tokenFor2(server);
|
|
17672
|
+
const brains = await listCloudBrains(token, server);
|
|
17673
|
+
const target = findCloudBrain(brains, named);
|
|
17674
|
+
if (!target) throw noSuchBrain2(named, brains);
|
|
17675
|
+
const authorised = await authorise(target, confirmFlag);
|
|
17676
|
+
if (authorised === void 0) {
|
|
17677
|
+
printHuman(c.yellow("Cancelled \u2014 nothing was deleted."));
|
|
17678
|
+
return;
|
|
17679
|
+
}
|
|
17680
|
+
const deleted = await deleteCloudBrain({ token, server, id: target.id, authorised });
|
|
17681
|
+
const cfg = await loadConfig();
|
|
17682
|
+
const pointer = cfg.brain.cloud;
|
|
17683
|
+
const pointerCleared = pointer !== void 0 && pointer.id === deleted.deleted && pointer.api_url === server;
|
|
17684
|
+
if (pointerCleared) {
|
|
17685
|
+
await saveConfig({
|
|
17686
|
+
...cfg,
|
|
17687
|
+
brain: {
|
|
17688
|
+
brains: cfg.brain.brains,
|
|
17689
|
+
...cfg.brain.default === void 0 ? {} : { default: cfg.brain.default }
|
|
17690
|
+
}
|
|
17691
|
+
});
|
|
17692
|
+
}
|
|
17693
|
+
const hint = deleted.hint ?? "The brain, its notes and its ledger are gone. `memgineering brains` lists what is left.";
|
|
17694
|
+
printDual({
|
|
17695
|
+
json: {
|
|
17696
|
+
deleted: { id: deleted.deleted, name: deleted.name },
|
|
17697
|
+
/** Null when the server said nothing — which is not the same as zero. */
|
|
17698
|
+
notes_deleted: deleted.notes_deleted ?? null,
|
|
17699
|
+
/** Whether this machine had been reading it, and has now stopped. */
|
|
17700
|
+
pointer_cleared: pointerCleared,
|
|
17701
|
+
hint
|
|
17702
|
+
},
|
|
17703
|
+
human: () => {
|
|
17704
|
+
printHuman(
|
|
17705
|
+
c.green(`\u2713 deleted \u201C${oneLine(deleted.name, WIDTH.title)}\u201D`) + c.gray(` (${oneLine(deleted.deleted, WIDTH.handle)})`)
|
|
17706
|
+
);
|
|
17707
|
+
printHuman(
|
|
17708
|
+
c.gray(
|
|
17709
|
+
deleted.notes_deleted === void 0 ? " its notes, its ledger and its measurements went with it" : ` ${deleted.notes_deleted} note(s) went with it, along with its ledger and its measurements`
|
|
17710
|
+
)
|
|
17711
|
+
);
|
|
17712
|
+
if (pointerCleared) {
|
|
17713
|
+
printHuman(
|
|
17714
|
+
c.yellow(
|
|
17715
|
+
`
|
|
17716
|
+
\u26A0 THIS MACHINE NO LONGER READS A HOSTED BRAIN \u2014 it was reading \u201C${oneLine(deleted.name, WIDTH.title)}\u201D, which is now deleted.`
|
|
17717
|
+
)
|
|
17718
|
+
);
|
|
17719
|
+
printHuman(
|
|
17720
|
+
c.gray(
|
|
17721
|
+
" `memgineering brains` lists what is left; `use --brain <name>` points at one.\n `recall --local` still reads a folder, if one is linked."
|
|
17722
|
+
)
|
|
17723
|
+
);
|
|
17724
|
+
}
|
|
17725
|
+
printHuman(c.gray(`
|
|
17726
|
+
${hint}`));
|
|
17727
|
+
}
|
|
17728
|
+
});
|
|
17729
|
+
});
|
|
17730
|
+
}
|
|
17731
|
+
async function tokenFor2(server) {
|
|
16983
17732
|
const credentials = await requireCredentials(server).catch((err) => {
|
|
16984
17733
|
if (err instanceof MemgError && err.code === "not_signed_in") {
|
|
16985
17734
|
throw memgError(
|
|
@@ -17036,7 +17785,7 @@ Delete \u201C${oneLine(target.name, WIDTH.title)}\u201D?`));
|
|
|
17036
17785
|
"\n `memgineering unlink --brain` is the other thing: it stops this machine\n reading a brain and deletes nothing. A wrongly-named brain is a rename."
|
|
17037
17786
|
)
|
|
17038
17787
|
);
|
|
17039
|
-
const { typed } = await
|
|
17788
|
+
const { typed } = await prompts2({
|
|
17040
17789
|
type: "text",
|
|
17041
17790
|
name: "typed",
|
|
17042
17791
|
message: "Type the brain\u2019s name exactly to delete it"
|
|
@@ -17070,21 +17819,21 @@ async function deleteCloudBrain(args) {
|
|
|
17070
17819
|
|
|
17071
17820
|
// src/commands/curation.ts
|
|
17072
17821
|
init_src();
|
|
17073
|
-
import { stat as
|
|
17822
|
+
import { stat as stat6 } from "fs/promises";
|
|
17074
17823
|
import { resolve as resolve8 } from "path";
|
|
17075
|
-
import { Command as
|
|
17824
|
+
import { Command as Command15 } from "commander";
|
|
17076
17825
|
|
|
17077
17826
|
// ../../packages/storage-adapter/src/local-file/ignore-file.ts
|
|
17078
17827
|
init_atomic();
|
|
17079
17828
|
init_exclude();
|
|
17080
17829
|
init_walk();
|
|
17081
17830
|
import { createHash as createHash6 } from "crypto";
|
|
17082
|
-
import { readFile as
|
|
17831
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
17083
17832
|
var FILE = ".memgignore";
|
|
17084
17833
|
var hashOf = (content) => createHash6("sha256").update(Buffer.from(content, "utf8")).digest("hex");
|
|
17085
17834
|
async function readIgnoreFileState(root) {
|
|
17086
17835
|
try {
|
|
17087
|
-
const content = await
|
|
17836
|
+
const content = await readFile19(resolveInside(root, FILE), "utf8");
|
|
17088
17837
|
return { content, exists: true, hash: hashOf(content) };
|
|
17089
17838
|
} catch (err) {
|
|
17090
17839
|
if (err?.code === "ENOENT") {
|
|
@@ -17174,7 +17923,7 @@ init_vault();
|
|
|
17174
17923
|
init_ui();
|
|
17175
17924
|
init_link();
|
|
17176
17925
|
function lifecycleCommand(action) {
|
|
17177
|
-
return new
|
|
17926
|
+
return new Command15(action).description(
|
|
17178
17927
|
action === "retire" ? "mark a memory as no longer current \u2014 the note itself is not changed" : "mark a retired memory as current again"
|
|
17179
17928
|
).argument("<ref>", "handle, id, path, or exact title").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the memory is in").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(async (ref, opts) => {
|
|
17180
17929
|
const where = await resolveTarget({
|
|
@@ -17261,7 +18010,7 @@ async function lifecycleLocal(vault, action, ref, reason) {
|
|
|
17261
18010
|
return { entry, record };
|
|
17262
18011
|
}
|
|
17263
18012
|
function ignoreCommand(mode) {
|
|
17264
|
-
return new
|
|
18013
|
+
return new Command15(mode).description(
|
|
17265
18014
|
mode === "exclude" ? "stop reading a note entirely \u2014 the note itself is not changed" : "read a note again after excluding it"
|
|
17266
18015
|
).argument("<path>", "path inside the brain, e.g. decisions/launch-date.md").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the note is in").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(async (path, opts) => {
|
|
17267
18016
|
const rationale = sanitizeRationale(opts.reason);
|
|
@@ -17386,7 +18135,7 @@ async function mustMatchAFile(root, adapter, path) {
|
|
|
17386
18135
|
const wanted = path.normalize("NFC").replace(/^\/+/, "");
|
|
17387
18136
|
const absolute = resolve8(root, wanted);
|
|
17388
18137
|
if (isInside(absolute, root)) {
|
|
17389
|
-
const found = await
|
|
18138
|
+
const found = await stat6(absolute).catch(() => null);
|
|
17390
18139
|
if (found?.isFile()) return wanted;
|
|
17391
18140
|
}
|
|
17392
18141
|
const hits = sameBasename(await adapter.list(""), path);
|
|
@@ -17403,14 +18152,14 @@ var unexcludeCommand = () => ignoreCommand("unexclude");
|
|
|
17403
18152
|
|
|
17404
18153
|
// src/commands/scope-validity.ts
|
|
17405
18154
|
init_src();
|
|
17406
|
-
import { Command as
|
|
18155
|
+
import { Command as Command16 } from "commander";
|
|
17407
18156
|
init_errors();
|
|
17408
18157
|
init_index_build();
|
|
17409
18158
|
init_cloud_brain();
|
|
17410
18159
|
init_vault();
|
|
17411
18160
|
init_ui();
|
|
17412
18161
|
function rescopeCommand() {
|
|
17413
|
-
return new
|
|
18162
|
+
return new Command16("rescope").description("change which project a memory binds in \u2014 nothing it says changes").argument("<ref>", "handle, id, path, or exact title").option("--scope <name>", "the project it binds in from now on").option("--global", "it binds everywhere \u2014 say this rather than leaving the scope off").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the memory is in").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(
|
|
17414
18163
|
async (ref, opts) => {
|
|
17415
18164
|
const scope = scopeAskedFor(opts);
|
|
17416
18165
|
const where = await resolveTarget({
|
|
@@ -17446,7 +18195,7 @@ Undo: \`${undoHint(record.op_id, opts)}\``));
|
|
|
17446
18195
|
);
|
|
17447
18196
|
}
|
|
17448
18197
|
function expireCommand() {
|
|
17449
|
-
return new
|
|
18198
|
+
return new Command16("expire").description("give a memory an end date \u2014 it is NOT retired, and recall still returns it").argument("<ref>", "handle, id, path, or exact title").requiredOption("--until <date>", "the day it stops being current, e.g. 2026-10-31").option("--reason <text>", "why \u2014 kept in the ledger").option("--vault <path>", "which brain the memory is in").option("--local", "use the brain on this machine, even when signed in to a hosted one").action(
|
|
17450
18199
|
async (ref, opts) => {
|
|
17451
18200
|
const where = await resolveTarget({
|
|
17452
18201
|
...opts.local === void 0 ? {} : { local: opts.local },
|
|
@@ -17665,13 +18414,13 @@ Undo: \`memgineering undo ${result.op_id}\``));
|
|
|
17665
18414
|
|
|
17666
18415
|
// src/commands/evidence.ts
|
|
17667
18416
|
init_src();
|
|
17668
|
-
import { Command as
|
|
18417
|
+
import { Command as Command17 } from "commander";
|
|
17669
18418
|
init_index_build();
|
|
17670
18419
|
init_cloud_brain();
|
|
17671
18420
|
init_vault();
|
|
17672
18421
|
init_ui();
|
|
17673
18422
|
function evidenceCommand() {
|
|
17674
|
-
return new
|
|
18423
|
+
return new Command17("evidence").description("how much a memory has been used, and why it last changed").argument("<ref>", "handle, id, path, or exact title").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option(
|
|
17675
18424
|
"--brain <name>",
|
|
17676
18425
|
"read from one existing hosted brain by name or id, without changing the pointer"
|
|
17677
18426
|
).action(async (ref, opts) => {
|
|
@@ -17960,9 +18709,9 @@ function ago(iso, now) {
|
|
|
17960
18709
|
init_brand();
|
|
17961
18710
|
init_config();
|
|
17962
18711
|
import { createHash as createHash7 } from "crypto";
|
|
17963
|
-
import { mkdir as
|
|
17964
|
-
import { dirname as dirname6, isAbsolute as isAbsolute3, join as
|
|
17965
|
-
import { Command as
|
|
18712
|
+
import { mkdir as mkdir13, readdir as readdir8, stat as stat7, unlink as unlink6, writeFile as writeFile10 } from "fs/promises";
|
|
18713
|
+
import { dirname as dirname6, isAbsolute as isAbsolute3, join as join21, resolve as resolve9 } from "path";
|
|
18714
|
+
import { Command as Command18 } from "commander";
|
|
17966
18715
|
|
|
17967
18716
|
// src/lib/rules.ts
|
|
17968
18717
|
init_src();
|
|
@@ -18034,7 +18783,7 @@ init_deadline();
|
|
|
18034
18783
|
init_cloud_brain();
|
|
18035
18784
|
init_vault();
|
|
18036
18785
|
function guardCommand() {
|
|
18037
|
-
return new
|
|
18786
|
+
return new Command18("guard").description("hook entry \u2014 show the user\u2019s rules before the first edit of a session").option("--context-dir <path>", "where the work is happening, when stdin does not say").option("--at <event>", "pre-tool-use | stop", "pre-tool-use").action(async (opts) => {
|
|
18038
18787
|
try {
|
|
18039
18788
|
const out = await withDeadline(HOOK_DEADLINE_MS, async () => {
|
|
18040
18789
|
const payload = await readPayload();
|
|
@@ -18181,7 +18930,7 @@ async function readPayload() {
|
|
|
18181
18930
|
return null;
|
|
18182
18931
|
}
|
|
18183
18932
|
}
|
|
18184
|
-
var guardDir = () =>
|
|
18933
|
+
var guardDir = () => join21(brandHome(), "guard");
|
|
18185
18934
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
18186
18935
|
"Write",
|
|
18187
18936
|
"Edit",
|
|
@@ -18202,7 +18951,7 @@ async function misdeliveryNotice(toolName, filePath, sessionId) {
|
|
|
18202
18951
|
return 'This file is this harness\u2019s own memory \u2014 no other tool the user works with will ever read it. Durable facts about the user belong in their brain, which every tool shares: memgineering remember "<the fact>" --reason "<why>" (add --rule if they stated a standing rule). Write it there instead, or in addition.';
|
|
18203
18952
|
}
|
|
18204
18953
|
function markerFor(sessionId, brain) {
|
|
18205
|
-
return
|
|
18954
|
+
return join21(
|
|
18206
18955
|
guardDir(),
|
|
18207
18956
|
`${createHash7("sha256").update(`${sessionId}\0${brain}`).digest("hex").slice(0, 32)}`
|
|
18208
18957
|
);
|
|
@@ -18210,7 +18959,7 @@ function markerFor(sessionId, brain) {
|
|
|
18210
18959
|
async function claimSession(sessionId, brain) {
|
|
18211
18960
|
const marker = markerFor(sessionId, brain);
|
|
18212
18961
|
try {
|
|
18213
|
-
await
|
|
18962
|
+
await mkdir13(guardDir(), { recursive: true, mode: 448 });
|
|
18214
18963
|
await writeFile10(marker, "", { flag: "wx", mode: 384 });
|
|
18215
18964
|
} catch {
|
|
18216
18965
|
return false;
|
|
@@ -18219,10 +18968,10 @@ async function claimSession(sessionId, brain) {
|
|
|
18219
18968
|
return true;
|
|
18220
18969
|
}
|
|
18221
18970
|
async function releaseSession(sessionId, brain) {
|
|
18222
|
-
await
|
|
18971
|
+
await unlink6(markerFor(sessionId, brain)).catch(() => void 0);
|
|
18223
18972
|
}
|
|
18224
18973
|
async function claimExists(sessionId, brain) {
|
|
18225
|
-
return
|
|
18974
|
+
return stat7(markerFor(sessionId, brain)).then(() => true).catch(() => false);
|
|
18226
18975
|
}
|
|
18227
18976
|
var MARKER_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
18228
18977
|
async function pruneOldMarkers() {
|
|
@@ -18230,10 +18979,10 @@ async function pruneOldMarkers() {
|
|
|
18230
18979
|
const dir = guardDir();
|
|
18231
18980
|
const now = Date.now();
|
|
18232
18981
|
for (const name of await readdir8(dir)) {
|
|
18233
|
-
const path =
|
|
18982
|
+
const path = join21(dir, name);
|
|
18234
18983
|
if (!isAbsolute3(path)) continue;
|
|
18235
|
-
const info = await
|
|
18236
|
-
if (info && now - info.mtimeMs > MARKER_TTL_MS) await
|
|
18984
|
+
const info = await stat7(path).catch(() => null);
|
|
18985
|
+
if (info && now - info.mtimeMs > MARKER_TTL_MS) await unlink6(path).catch(() => void 0);
|
|
18237
18986
|
}
|
|
18238
18987
|
await pruneFast(now);
|
|
18239
18988
|
} catch {
|
|
@@ -18277,15 +19026,15 @@ init_link();
|
|
|
18277
19026
|
// src/commands/log.ts
|
|
18278
19027
|
init_src();
|
|
18279
19028
|
init_walk();
|
|
18280
|
-
import { lstat as lstat4, readFile as
|
|
18281
|
-
import { Command as
|
|
19029
|
+
import { lstat as lstat4, readFile as readFile21 } from "fs/promises";
|
|
19030
|
+
import { Command as Command20 } from "commander";
|
|
18282
19031
|
init_errors();
|
|
18283
19032
|
init_cloud_brain();
|
|
18284
19033
|
|
|
18285
19034
|
// src/commands/retract.ts
|
|
18286
19035
|
init_errors();
|
|
18287
19036
|
init_cloud_brain();
|
|
18288
|
-
import { Command as
|
|
19037
|
+
import { Command as Command19 } from "commander";
|
|
18289
19038
|
init_vault();
|
|
18290
19039
|
init_ui();
|
|
18291
19040
|
var CONTENT_VERBS = /* @__PURE__ */ new Set([
|
|
@@ -18356,7 +19105,7 @@ function parseInstant(flag2, raw) {
|
|
|
18356
19105
|
return { ms, iso: new Date(ms).toISOString() };
|
|
18357
19106
|
}
|
|
18358
19107
|
function retractCommand() {
|
|
18359
|
-
return new
|
|
19108
|
+
return new Command19("retract").description(
|
|
18360
19109
|
"retire every memory one writer or one stretch of time wrote \u2014 shows the set first; --yes applies it"
|
|
18361
19110
|
).option("--actor <actor>", 'writes recorded under this actor, e.g. "mcp:ChatGPT" or "agent"').option(
|
|
18362
19111
|
"--token <id>",
|
|
@@ -18571,7 +19320,7 @@ ${failed.length} could not be retired:`);
|
|
|
18571
19320
|
init_vault();
|
|
18572
19321
|
init_ui();
|
|
18573
19322
|
function logCommand() {
|
|
18574
|
-
return new
|
|
19323
|
+
return new Command20("log").description("what changed in this brain, and what can still be undone").option("--limit <n>", "how many operations", (v) => parseInt(v, 10), 20).option("--verbose", "include hashes and reverse patches").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").option(
|
|
18575
19324
|
"--brain <name>",
|
|
18576
19325
|
"read one existing hosted brain by name or id, without changing the pointer"
|
|
18577
19326
|
).option("--actor <actor>", 'only operations recorded under this actor, e.g. "mcp:ChatGPT" or "agent"').option("--token <id>", "only operations made with this token (id or 5+ char prefix) \u2014 hosted brains").option("--since <when>", "only operations at or after this date / ISO instant").option("--until <when>", "only operations before this date / ISO instant").action(async (opts) => {
|
|
@@ -18843,16 +19592,16 @@ async function readOrNull(root, path) {
|
|
|
18843
19592
|
if (absolute === null) return null;
|
|
18844
19593
|
const info = await lstat4(absolute).catch(() => null);
|
|
18845
19594
|
if (!info || !info.isFile()) return null;
|
|
18846
|
-
return
|
|
19595
|
+
return readFile21(absolute, "utf8").catch(() => null);
|
|
18847
19596
|
}
|
|
18848
19597
|
|
|
18849
19598
|
// src/commands/project.ts
|
|
18850
19599
|
init_errors();
|
|
18851
19600
|
import { resolve as resolve10 } from "path";
|
|
18852
|
-
import { Command as
|
|
19601
|
+
import { Command as Command21 } from "commander";
|
|
18853
19602
|
init_ui();
|
|
18854
19603
|
function projectCommand() {
|
|
18855
|
-
return new
|
|
19604
|
+
return new Command21("project").description("show or set what the work in this folder is called").argument("[name]", "what to call it \u2014 omit to show what this folder answers to").option("--here <dir>", "act on this directory instead of the current one").option("--unset", "forget the name set here and go back to what the repository says").addHelpText(
|
|
18856
19605
|
"after",
|
|
18857
19606
|
[
|
|
18858
19607
|
"",
|
|
@@ -18979,115 +19728,10 @@ init_api_client();
|
|
|
18979
19728
|
init_credentials();
|
|
18980
19729
|
init_config();
|
|
18981
19730
|
init_pending_login();
|
|
18982
|
-
import { Command as
|
|
18983
|
-
|
|
18984
|
-
// src/lib/registry-session.ts
|
|
18985
|
-
init_errors();
|
|
18986
|
-
init_credentials();
|
|
18987
|
-
init_version();
|
|
18988
|
-
function registryApiUrl(stored) {
|
|
18989
|
-
const override = process.env["KORDIS_API_URL"];
|
|
18990
|
-
return override !== void 0 && override !== "" ? override : stored;
|
|
18991
|
-
}
|
|
18992
|
-
async function requireRegistryCredentials() {
|
|
18993
|
-
const credentials = await readCredentials();
|
|
18994
|
-
const registry = credentials?.registry;
|
|
18995
|
-
if (!registry) {
|
|
18996
|
-
throw memgError(
|
|
18997
|
-
"not_linked",
|
|
18998
|
-
"this machine holds no registry identity",
|
|
18999
|
-
credentials === null ? "Nothing was sent. Sign in with `memgineering login` \u2014 it links the registry identity at the same time, so one sign-in covers both." : "Nothing was sent. This machine is signed in to a brain, and the registry half of that sign-in is missing \u2014 which is what a credential file written before federation looks like. Run `memgineering login` again; it links the registry identity too."
|
|
19000
|
-
);
|
|
19001
|
-
}
|
|
19002
|
-
return registry;
|
|
19003
|
-
}
|
|
19004
|
-
var RACE_WAIT_MS = 2e3;
|
|
19005
|
-
var RACE_POLL_MS = 50;
|
|
19006
|
-
function lostRace(err) {
|
|
19007
|
-
return err instanceof KordisApiError && err.status === 401 && isRefreshRace({ error: { code: err.code, flags: err.flags } });
|
|
19008
|
-
}
|
|
19009
|
-
async function newerRegistry(sent) {
|
|
19010
|
-
const deadline = Date.now() + RACE_WAIT_MS;
|
|
19011
|
-
for (; ; ) {
|
|
19012
|
-
const registry = (await readCredentials())?.registry;
|
|
19013
|
-
if (registry !== void 0 && registry.refresh_token !== sent) return registry;
|
|
19014
|
-
if (Date.now() >= deadline) return null;
|
|
19015
|
-
await new Promise((resolve17) => setTimeout(resolve17, RACE_POLL_MS));
|
|
19016
|
-
}
|
|
19017
|
-
}
|
|
19018
|
-
async function refreshRegistrySession(stored, opts = {}) {
|
|
19019
|
-
const attempt = async (registry) => {
|
|
19020
|
-
const baseUrl = (opts.apiUrlOf ?? ((r) => registryApiUrl(r.api_url)))(registry);
|
|
19021
|
-
const anonymous = new KordisClient({
|
|
19022
|
-
baseUrl,
|
|
19023
|
-
userAgent: `memgineering/${VERSION}`,
|
|
19024
|
-
...opts.fetchImpl ? { fetch: opts.fetchImpl } : {},
|
|
19025
|
-
...opts.requestTimeoutMs === void 0 ? {} : { requestTimeoutMs: opts.requestTimeoutMs }
|
|
19026
|
-
});
|
|
19027
|
-
return { registry, baseUrl, tokens: await anonymous.refreshSession(registry.refresh_token) };
|
|
19028
|
-
};
|
|
19029
|
-
try {
|
|
19030
|
-
return await attempt(stored);
|
|
19031
|
-
} catch (err) {
|
|
19032
|
-
if (!lostRace(err)) throw err;
|
|
19033
|
-
const newer = await newerRegistry(stored.refresh_token);
|
|
19034
|
-
if (newer === null) throw err;
|
|
19035
|
-
return attempt(newer);
|
|
19036
|
-
}
|
|
19037
|
-
}
|
|
19038
|
-
function refusal(err) {
|
|
19039
|
-
if (lostRace(err)) {
|
|
19040
|
-
return memgError(
|
|
19041
|
-
"not_authenticated",
|
|
19042
|
-
err.message,
|
|
19043
|
-
"This machine's registry sign-in was refreshed by another request at the same moment, so this command could not use it. Run this command again."
|
|
19044
|
-
);
|
|
19045
|
-
}
|
|
19046
|
-
const busyOrUnreachable = err instanceof KordisNetworkError || err instanceof KordisApiError && (err.status === 429 || err.status >= 500);
|
|
19047
|
-
return (busyOrUnreachable ? adaptRegistryError(err) : null) ?? memgError(
|
|
19048
|
-
"not_linked",
|
|
19049
|
-
`the registry refused this machine's stored sign-in: ${err instanceof Error ? err.message : String(err)}`,
|
|
19050
|
-
"Nothing was sent. The link between this brain and your registry account has expired or been revoked. `memgineering login` re-links both."
|
|
19051
|
-
);
|
|
19052
|
-
}
|
|
19053
|
-
async function openRegistrySession(fetchImpl) {
|
|
19054
|
-
const stored = await requireRegistryCredentials();
|
|
19055
|
-
let refreshed;
|
|
19056
|
-
try {
|
|
19057
|
-
refreshed = await refreshRegistrySession(stored, fetchImpl ? { fetchImpl } : {});
|
|
19058
|
-
} catch (err) {
|
|
19059
|
-
throw refusal(err);
|
|
19060
|
-
}
|
|
19061
|
-
const { registry, baseUrl, tokens } = refreshed;
|
|
19062
|
-
const credentialsRotated = await persistRotatedRefresh(
|
|
19063
|
-
registry,
|
|
19064
|
-
tokens.refresh_token,
|
|
19065
|
-
tokens.session_id
|
|
19066
|
-
);
|
|
19067
|
-
const client = new KordisClient({
|
|
19068
|
-
baseUrl,
|
|
19069
|
-
userAgent: `memgineering/${VERSION}`,
|
|
19070
|
-
getAccessToken: () => tokens.access_token,
|
|
19071
|
-
...fetchImpl ? { fetch: fetchImpl } : {}
|
|
19072
|
-
});
|
|
19073
|
-
return { client, username: registry.username, apiUrl: baseUrl, credentialsRotated };
|
|
19074
|
-
}
|
|
19075
|
-
async function persistRotatedRefresh(sent, refreshToken, sessionId) {
|
|
19076
|
-
if (refreshToken === sent.refresh_token && sessionId === sent.session_id) return false;
|
|
19077
|
-
try {
|
|
19078
|
-
return await saveRotatedRegistry(sent.refresh_token, {
|
|
19079
|
-
refresh_token: refreshToken,
|
|
19080
|
-
session_id: sessionId
|
|
19081
|
-
});
|
|
19082
|
-
} catch {
|
|
19083
|
-
return false;
|
|
19084
|
-
}
|
|
19085
|
-
}
|
|
19086
|
-
|
|
19087
|
-
// src/commands/logout.ts
|
|
19731
|
+
import { Command as Command23 } from "commander";
|
|
19088
19732
|
init_ui();
|
|
19089
19733
|
function logoutCommand() {
|
|
19090
|
-
return new
|
|
19734
|
+
return new Command23("logout").description("sign out of your account \u2014 the pointer and your notes stay").option("--all", "revoke every token on the account, not just this machine\u2019s").option(
|
|
19091
19735
|
"--local",
|
|
19092
19736
|
"forget the token here without telling the server \u2014 only for a server you cannot reach"
|
|
19093
19737
|
).action(async (opts) => {
|
|
@@ -19096,9 +19740,18 @@ function logoutCommand() {
|
|
|
19096
19740
|
await clearPendingLogin();
|
|
19097
19741
|
await clearSetupFollowUps();
|
|
19098
19742
|
if (!credentials) {
|
|
19743
|
+
const hint = opts.all ? NOT_SIGNED_IN_ALL_HINT : void 0;
|
|
19099
19744
|
printDual({
|
|
19100
|
-
json: {
|
|
19101
|
-
|
|
19745
|
+
json: {
|
|
19746
|
+
signed_out: true,
|
|
19747
|
+
was_signed_in: false,
|
|
19748
|
+
api_url: base2,
|
|
19749
|
+
...hint ? { hint } : {}
|
|
19750
|
+
},
|
|
19751
|
+
human: () => {
|
|
19752
|
+
printHuman("Not signed in \u2014 nothing to do.");
|
|
19753
|
+
if (hint) printHuman(c.gray(` ${hint}`));
|
|
19754
|
+
}
|
|
19102
19755
|
});
|
|
19103
19756
|
return;
|
|
19104
19757
|
}
|
|
@@ -19118,6 +19771,8 @@ function logoutCommand() {
|
|
|
19118
19771
|
return;
|
|
19119
19772
|
}
|
|
19120
19773
|
let revoked = null;
|
|
19774
|
+
let registrySessions = null;
|
|
19775
|
+
let revokedBefore = null;
|
|
19121
19776
|
try {
|
|
19122
19777
|
if (opts.all) {
|
|
19123
19778
|
const res = await apiRequest({
|
|
@@ -19127,6 +19782,8 @@ function logoutCommand() {
|
|
|
19127
19782
|
baseUrl: credentials.api_url
|
|
19128
19783
|
});
|
|
19129
19784
|
revoked = res.body?.revoked ?? null;
|
|
19785
|
+
registrySessions = readRegistrySessions(res.body?.registry_sessions);
|
|
19786
|
+
revokedBefore = typeof res.body?.revoked_before === "string" ? res.body.revoked_before : null;
|
|
19130
19787
|
} else {
|
|
19131
19788
|
const me = await apiRequest({
|
|
19132
19789
|
method: "GET",
|
|
@@ -19156,7 +19813,7 @@ function logoutCommand() {
|
|
|
19156
19813
|
}
|
|
19157
19814
|
}
|
|
19158
19815
|
const registry = (await readCredentials())?.registry ?? credentials.registry;
|
|
19159
|
-
const registryRevoked = await revokeRegistrySession(registry);
|
|
19816
|
+
const registryRevoked = registrySessions?.status === "revoked" ? true : await revokeRegistrySession(registry);
|
|
19160
19817
|
await clearCredentials();
|
|
19161
19818
|
printDual({
|
|
19162
19819
|
json: {
|
|
@@ -19165,7 +19822,9 @@ function logoutCommand() {
|
|
|
19165
19822
|
revoked_on_server: true,
|
|
19166
19823
|
revoked,
|
|
19167
19824
|
all: opts.all === true,
|
|
19168
|
-
api_url: credentials.api_url
|
|
19825
|
+
api_url: credentials.api_url,
|
|
19826
|
+
...registrySessions ? { registry_sessions: registrySessions } : {},
|
|
19827
|
+
...revokedBefore ? { revoked_before: revokedBefore } : {}
|
|
19169
19828
|
},
|
|
19170
19829
|
human: () => {
|
|
19171
19830
|
printHuman(opts.all ? "Signed out everywhere." : "Signed out.");
|
|
@@ -19174,6 +19833,8 @@ function logoutCommand() {
|
|
|
19174
19833
|
opts.all ? " Every token on the account is dead. `memgineering login` starts a new one." : " This machine\u2019s token is revoked. Other machines are unaffected."
|
|
19175
19834
|
)
|
|
19176
19835
|
);
|
|
19836
|
+
const registryLine = registrySessions ? registrySessionsLine(registrySessions) : null;
|
|
19837
|
+
if (registryLine !== null) printHuman(registryLine);
|
|
19177
19838
|
printHuman(
|
|
19178
19839
|
c.gray(
|
|
19179
19840
|
" Your notes are untouched: signing out takes a token, not a brain.\n This machine still remembers which brain it was reading, so `memgineering\n login` picks up where you left off. `memgineering unlink --brain` forgets it."
|
|
@@ -19183,6 +19844,34 @@ function logoutCommand() {
|
|
|
19183
19844
|
});
|
|
19184
19845
|
});
|
|
19185
19846
|
}
|
|
19847
|
+
var NOT_SIGNED_IN_ALL_HINT = 'Nothing is signed in on this machine, so nothing could be ended from here. To disconnect everything, press "disconnect everything" on the website, or run `memgineering login` and then `memgineering logout --all`.';
|
|
19848
|
+
function readRegistrySessions(raw) {
|
|
19849
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
19850
|
+
const r = raw;
|
|
19851
|
+
if (r["status"] === "revoked" && typeof r["count"] === "number") {
|
|
19852
|
+
return { status: "revoked", count: r["count"] };
|
|
19853
|
+
}
|
|
19854
|
+
if (r["status"] === "not_linked") return { status: "not_linked" };
|
|
19855
|
+
if (r["status"] === "unreachable" && typeof r["hint"] === "string") {
|
|
19856
|
+
return { status: "unreachable", hint: r["hint"] };
|
|
19857
|
+
}
|
|
19858
|
+
return null;
|
|
19859
|
+
}
|
|
19860
|
+
function registrySessionsLine(r) {
|
|
19861
|
+
switch (r.status) {
|
|
19862
|
+
case "revoked":
|
|
19863
|
+
if (r.count === 0) return null;
|
|
19864
|
+
return c.gray(
|
|
19865
|
+
` Registry sign-ins ended too: ${r.count}. Anything approved before now cannot sign back in.`
|
|
19866
|
+
);
|
|
19867
|
+
case "not_linked":
|
|
19868
|
+
return c.gray(
|
|
19869
|
+
" No registry account is linked to this Google account; nothing to end there."
|
|
19870
|
+
);
|
|
19871
|
+
case "unreachable":
|
|
19872
|
+
return c.yellow(` Registry sign-ins could not be ended: ${oneLineWhole(r.hint)}`);
|
|
19873
|
+
}
|
|
19874
|
+
}
|
|
19186
19875
|
async function revokeRegistrySession(reg) {
|
|
19187
19876
|
if (!reg) return false;
|
|
19188
19877
|
try {
|
|
@@ -19206,12 +19895,12 @@ async function revokeRegistrySession(reg) {
|
|
|
19206
19895
|
|
|
19207
19896
|
// src/commands/onboard.ts
|
|
19208
19897
|
init_src();
|
|
19209
|
-
import { Command as
|
|
19898
|
+
import { Command as Command24 } from "commander";
|
|
19210
19899
|
init_cloud_brain();
|
|
19211
19900
|
init_vault();
|
|
19212
19901
|
init_ui();
|
|
19213
19902
|
function onboardCommand() {
|
|
19214
|
-
return new
|
|
19903
|
+
return new Command24("onboard").description("what to ask so the brain knows who it belongs to").option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").option(
|
|
19215
19904
|
"--brain <name>",
|
|
19216
19905
|
"read one existing hosted brain by name or id, without changing the pointer"
|
|
19217
19906
|
).action(async (opts) => {
|
|
@@ -19357,11 +20046,11 @@ init_api_client();
|
|
|
19357
20046
|
init_config();
|
|
19358
20047
|
init_cloud_brain();
|
|
19359
20048
|
init_ui();
|
|
19360
|
-
import { mkdir as
|
|
19361
|
-
import { dirname as dirname7, isAbsolute as isAbsolute4, join as
|
|
19362
|
-
import { Command as
|
|
20049
|
+
import { mkdir as mkdir14, readFile as readFile22, writeFile as writeFile11 } from "fs/promises";
|
|
20050
|
+
import { dirname as dirname7, isAbsolute as isAbsolute4, join as join22, resolve as resolve11, sep as sep2 } from "path";
|
|
20051
|
+
import { Command as Command25 } from "commander";
|
|
19363
20052
|
function pullCommand() {
|
|
19364
|
-
return new
|
|
20053
|
+
return new Command25("pull").description("download your hosted brain to a folder on this machine").argument("[destination]", "folder to write into (created if missing)").option("--brain <name>", "which hosted brain to download").option("--dry-run", "say what would be written and write nothing").action(async (destination, opts) => {
|
|
19365
20054
|
const { brain: source, others } = await resolveSource(opts.brain);
|
|
19366
20055
|
const target = resolve11(destination ?? `./${oneLine(source.name, WIDTH.title)}`);
|
|
19367
20056
|
const first = await cloudListNotes(source, { limit: 1 });
|
|
@@ -19426,7 +20115,7 @@ function pullCommand() {
|
|
|
19426
20115
|
already.push(note.path);
|
|
19427
20116
|
continue;
|
|
19428
20117
|
}
|
|
19429
|
-
await
|
|
20118
|
+
await mkdir14(dirname7(file), { recursive: true });
|
|
19430
20119
|
await writeFile11(file, note.content ?? "", "utf8");
|
|
19431
20120
|
written.push(note.path);
|
|
19432
20121
|
if (note.excluded === true) excluded.push(note.path);
|
|
@@ -19541,7 +20230,7 @@ function safeJoin(root, relative3) {
|
|
|
19541
20230
|
"Nothing was written for it. This should not happen \u2014 please report it."
|
|
19542
20231
|
);
|
|
19543
20232
|
}
|
|
19544
|
-
const full = resolve11(
|
|
20233
|
+
const full = resolve11(join22(root, relative3));
|
|
19545
20234
|
if (full !== root && !full.startsWith(root + sep2)) {
|
|
19546
20235
|
throw memgError(
|
|
19547
20236
|
"server_refused",
|
|
@@ -19553,7 +20242,7 @@ function safeJoin(root, relative3) {
|
|
|
19553
20242
|
}
|
|
19554
20243
|
async function exists(file) {
|
|
19555
20244
|
try {
|
|
19556
|
-
await
|
|
20245
|
+
await readFile22(file);
|
|
19557
20246
|
return true;
|
|
19558
20247
|
} catch {
|
|
19559
20248
|
return false;
|
|
@@ -19561,8 +20250,8 @@ async function exists(file) {
|
|
|
19561
20250
|
}
|
|
19562
20251
|
|
|
19563
20252
|
// src/commands/publish.ts
|
|
19564
|
-
import { Command as
|
|
19565
|
-
import { z as
|
|
20253
|
+
import { Command as Command27 } from "commander";
|
|
20254
|
+
import { z as z33 } from "zod";
|
|
19566
20255
|
init_src();
|
|
19567
20256
|
init_errors();
|
|
19568
20257
|
|
|
@@ -19709,9 +20398,9 @@ import semver from "semver";
|
|
|
19709
20398
|
init_parser();
|
|
19710
20399
|
|
|
19711
20400
|
// ../../packages/harness-schema/src/deps.ts
|
|
19712
|
-
import { z as
|
|
20401
|
+
import { z as z26 } from "zod";
|
|
19713
20402
|
var StringDepRe = /^([a-z0-9](-?[a-z0-9])*\/[a-z0-9](-?[a-z0-9])*)(?:@(.+))?$/;
|
|
19714
|
-
var ObjectDepSchema =
|
|
20403
|
+
var ObjectDepSchema = z26.object({
|
|
19715
20404
|
full_slug: FullSlugSchema,
|
|
19716
20405
|
version_range: SemverRangeSchema.default("*")
|
|
19717
20406
|
});
|
|
@@ -19746,33 +20435,33 @@ function normaliseDep(entry, idx) {
|
|
|
19746
20435
|
init_parser();
|
|
19747
20436
|
|
|
19748
20437
|
// ../../packages/harness-schema/src/types/skill.ts
|
|
19749
|
-
import { z as
|
|
20438
|
+
import { z as z27 } from "zod";
|
|
19750
20439
|
var SKILL_DESCRIPTION_SPEC_MAX = 1024;
|
|
19751
20440
|
var SKILL_DESCRIPTION_HARD_MAX = 2048;
|
|
19752
20441
|
var SKILL_COMPATIBILITY_MAX = 500;
|
|
19753
|
-
var MetadataValueSchema =
|
|
19754
|
-
var SkillSpecFrontmatterSchema =
|
|
19755
|
-
name:
|
|
19756
|
-
description:
|
|
20442
|
+
var MetadataValueSchema = z27.union([z27.string(), z27.number(), z27.boolean()]).transform((v) => String(v));
|
|
20443
|
+
var SkillSpecFrontmatterSchema = z27.object({
|
|
20444
|
+
name: z27.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "name must be lowercase-with-hyphens (Agent Skills spec)"),
|
|
20445
|
+
description: z27.string().min(1).max(SKILL_DESCRIPTION_HARD_MAX),
|
|
19757
20446
|
// The spec puts no length limit on license (it is an SPDX identifier or a
|
|
19758
20447
|
// short phrase); the old 64-char cap here was Kordis-invented.
|
|
19759
|
-
license:
|
|
19760
|
-
compatibility:
|
|
20448
|
+
license: z27.string().optional(),
|
|
20449
|
+
compatibility: z27.string().max(SKILL_COMPATIBILITY_MAX).optional(),
|
|
19761
20450
|
// Spec form is a space-separated STRING. The array form is what Kordis
|
|
19762
20451
|
// accepted before the spec settled — still parsed, flagged
|
|
19763
20452
|
// `legacy_frontmatter`, and re-emitted unchanged by the translators.
|
|
19764
|
-
"allowed-tools":
|
|
19765
|
-
metadata:
|
|
20453
|
+
"allowed-tools": z27.union([z27.string(), z27.array(z27.string())]).optional(),
|
|
20454
|
+
metadata: z27.record(z27.string(), MetadataValueSchema).optional()
|
|
19766
20455
|
}).strict();
|
|
19767
|
-
var LegacySkillExtrasSchema =
|
|
20456
|
+
var LegacySkillExtrasSchema = z27.object({
|
|
19768
20457
|
version: SemverSchema.optional(),
|
|
19769
|
-
homepage:
|
|
19770
|
-
model:
|
|
20458
|
+
homepage: z27.string().url().optional(),
|
|
20459
|
+
model: z27.string().optional(),
|
|
19771
20460
|
// model override hint
|
|
19772
|
-
"disable-model-invocation":
|
|
19773
|
-
title:
|
|
19774
|
-
tags:
|
|
19775
|
-
tools:
|
|
20461
|
+
"disable-model-invocation": z27.boolean().optional(),
|
|
20462
|
+
title: z27.string().min(1).max(200).optional(),
|
|
20463
|
+
tags: z27.array(TagSchema).max(16).optional(),
|
|
20464
|
+
tools: z27.array(ToolSlugSchema).optional()
|
|
19776
20465
|
});
|
|
19777
20466
|
var LEGACY_SKILL_EXTRA_KEYS = [
|
|
19778
20467
|
"version",
|
|
@@ -19787,10 +20476,10 @@ var LEGACY_KEY_SET = new Set(LEGACY_SKILL_EXTRA_KEYS);
|
|
|
19787
20476
|
var NONSTANDARD_HINT = 'These are not Agent Skills fields. Move them to `metadata` (string values: `metadata: {version: "1.2.3", tags: "a, b", tools: "claude-code, codex"}`). Accepted with a warning for now; a future release will make this an error and will say so in its changelog.';
|
|
19788
20477
|
var METADATA_READBACK = [
|
|
19789
20478
|
{ field: "version", schema: SemverSchema, list: false },
|
|
19790
|
-
{ field: "homepage", schema:
|
|
19791
|
-
{ field: "model", schema:
|
|
19792
|
-
{ field: "tags", schema:
|
|
19793
|
-
{ field: "tools", schema:
|
|
20479
|
+
{ field: "homepage", schema: z27.string().url(), list: false },
|
|
20480
|
+
{ field: "model", schema: z27.string().min(1), list: false },
|
|
20481
|
+
{ field: "tags", schema: z27.array(TagSchema).max(16), list: true },
|
|
20482
|
+
{ field: "tools", schema: z27.array(ToolSlugSchema), list: true }
|
|
19794
20483
|
];
|
|
19795
20484
|
function splitList(raw) {
|
|
19796
20485
|
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
@@ -19885,30 +20574,30 @@ function parseSkillFrontmatter(raw) {
|
|
|
19885
20574
|
}
|
|
19886
20575
|
|
|
19887
20576
|
// ../../packages/harness-schema/src/types/mcp-server.ts
|
|
19888
|
-
import { z as
|
|
19889
|
-
var McpTransportSchema =
|
|
19890
|
-
|
|
19891
|
-
type:
|
|
19892
|
-
command:
|
|
19893
|
-
args:
|
|
19894
|
-
env:
|
|
20577
|
+
import { z as z28 } from "zod";
|
|
20578
|
+
var McpTransportSchema = z28.union([
|
|
20579
|
+
z28.object({
|
|
20580
|
+
type: z28.literal("stdio"),
|
|
20581
|
+
command: z28.string().min(1),
|
|
20582
|
+
args: z28.array(z28.string()).optional().default([]),
|
|
20583
|
+
env: z28.record(z28.string()).optional().default({})
|
|
19895
20584
|
}),
|
|
19896
|
-
|
|
19897
|
-
type:
|
|
19898
|
-
url:
|
|
19899
|
-
headers:
|
|
20585
|
+
z28.object({
|
|
20586
|
+
type: z28.enum(["http", "sse"]),
|
|
20587
|
+
url: z28.string().url(),
|
|
20588
|
+
headers: z28.record(z28.string()).optional().default({})
|
|
19900
20589
|
})
|
|
19901
20590
|
]);
|
|
19902
|
-
var McpCapabilitiesSchema =
|
|
19903
|
-
tools:
|
|
19904
|
-
resources:
|
|
19905
|
-
prompts:
|
|
20591
|
+
var McpCapabilitiesSchema = z28.object({
|
|
20592
|
+
tools: z28.array(z28.string()).optional().default([]),
|
|
20593
|
+
resources: z28.array(z28.string()).optional().default([]),
|
|
20594
|
+
prompts: z28.array(z28.string()).optional().default([])
|
|
19906
20595
|
});
|
|
19907
|
-
var McpServerFrontmatterSchema =
|
|
19908
|
-
name:
|
|
19909
|
-
description:
|
|
20596
|
+
var McpServerFrontmatterSchema = z28.object({
|
|
20597
|
+
name: z28.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
|
|
20598
|
+
description: z28.string().min(1).max(2048),
|
|
19910
20599
|
version: SemverSchema,
|
|
19911
|
-
mcp_spec_version:
|
|
20600
|
+
mcp_spec_version: z28.string().default("2026-03"),
|
|
19912
20601
|
transport: McpTransportSchema,
|
|
19913
20602
|
capabilities: McpCapabilitiesSchema.optional().default({
|
|
19914
20603
|
tools: [],
|
|
@@ -19916,34 +20605,34 @@ var McpServerFrontmatterSchema = z27.object({
|
|
|
19916
20605
|
prompts: []
|
|
19917
20606
|
}),
|
|
19918
20607
|
/** Human-readable setup / auth instructions. Surfaced in `kordis info`. */
|
|
19919
|
-
auth_notes:
|
|
20608
|
+
auth_notes: z28.string().optional(),
|
|
19920
20609
|
/** Required env vars with description + whether they must be secret. */
|
|
19921
|
-
env_schema:
|
|
19922
|
-
|
|
19923
|
-
description:
|
|
19924
|
-
required:
|
|
19925
|
-
secret:
|
|
20610
|
+
env_schema: z28.record(
|
|
20611
|
+
z28.object({
|
|
20612
|
+
description: z28.string(),
|
|
20613
|
+
required: z28.boolean().default(true),
|
|
20614
|
+
secret: z28.boolean().default(false)
|
|
19926
20615
|
})
|
|
19927
20616
|
).optional(),
|
|
19928
|
-
homepage:
|
|
19929
|
-
repository:
|
|
19930
|
-
license:
|
|
19931
|
-
title:
|
|
20617
|
+
homepage: z28.string().url().optional(),
|
|
20618
|
+
repository: z28.string().url().optional(),
|
|
20619
|
+
license: z28.string().max(64).optional(),
|
|
20620
|
+
title: z28.string().min(1).max(200).optional()
|
|
19932
20621
|
}).strict();
|
|
19933
20622
|
|
|
19934
20623
|
// ../../packages/harness-schema/src/types/project-rules.ts
|
|
19935
|
-
import { z as
|
|
19936
|
-
var ProjectRulesFrontmatterSchema =
|
|
19937
|
-
title:
|
|
19938
|
-
description:
|
|
20624
|
+
import { z as z29 } from "zod";
|
|
20625
|
+
var ProjectRulesFrontmatterSchema = z29.object({
|
|
20626
|
+
title: z29.string().min(1).max(200),
|
|
20627
|
+
description: z29.string().min(1).max(2048),
|
|
19939
20628
|
version: SemverSchema.optional(),
|
|
19940
|
-
scope:
|
|
19941
|
-
target_path:
|
|
19942
|
-
globs:
|
|
19943
|
-
target_tools:
|
|
20629
|
+
scope: z29.enum(["root", "nested", "globs"]).default("root"),
|
|
20630
|
+
target_path: z29.string().optional(),
|
|
20631
|
+
globs: z29.array(z29.string()).optional(),
|
|
20632
|
+
target_tools: z29.array(ToolSlugSchema).optional().default([]),
|
|
19944
20633
|
/** When true, installation is idempotent via `<!-- kordis:... -->` markers. */
|
|
19945
|
-
mergeable:
|
|
19946
|
-
license:
|
|
20634
|
+
mergeable: z29.boolean().default(true),
|
|
20635
|
+
license: z29.string().max(64).optional()
|
|
19947
20636
|
}).strict().refine((v) => v.scope !== "nested" || !!v.target_path, {
|
|
19948
20637
|
message: 'scope="nested" requires target_path'
|
|
19949
20638
|
}).refine((v) => v.scope !== "globs" || v.globs && v.globs.length > 0, {
|
|
@@ -19951,32 +20640,32 @@ var ProjectRulesFrontmatterSchema = z28.object({
|
|
|
19951
20640
|
});
|
|
19952
20641
|
|
|
19953
20642
|
// ../../packages/harness-schema/src/types/custom-agent.ts
|
|
19954
|
-
import { z as
|
|
19955
|
-
var CustomAgentFrontmatterSchema =
|
|
19956
|
-
name:
|
|
19957
|
-
description:
|
|
19958
|
-
title:
|
|
20643
|
+
import { z as z30 } from "zod";
|
|
20644
|
+
var CustomAgentFrontmatterSchema = z30.object({
|
|
20645
|
+
name: z30.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
|
|
20646
|
+
description: z30.string().min(1).max(2048),
|
|
20647
|
+
title: z30.string().min(1).max(200).optional(),
|
|
19959
20648
|
version: SemverSchema.optional(),
|
|
19960
20649
|
/** System prompt that defines the agent's persona + rules. Required. */
|
|
19961
|
-
system_prompt:
|
|
20650
|
+
system_prompt: z30.string().min(1).max(5e4),
|
|
19962
20651
|
/** Optional model override — string passed verbatim to the host. */
|
|
19963
|
-
model:
|
|
20652
|
+
model: z30.string().optional(),
|
|
19964
20653
|
/** Tools this agent can call. Host-specific names (no uniform namespace). */
|
|
19965
|
-
allowed_tools:
|
|
19966
|
-
disallowed_tools:
|
|
20654
|
+
allowed_tools: z30.array(z30.string()).optional().default([]),
|
|
20655
|
+
disallowed_tools: z30.array(z30.string()).optional().default([]),
|
|
19967
20656
|
/** Fork/context behaviour hint (Claude: `context: fork` etc.). */
|
|
19968
|
-
context:
|
|
20657
|
+
context: z30.enum(["inherit", "fork", "isolated"]).optional(),
|
|
19969
20658
|
/** Known-good tool runtimes. Empty = untested everywhere. */
|
|
19970
|
-
tool_compat:
|
|
20659
|
+
tool_compat: z30.array(ToolSlugSchema).optional().default([]),
|
|
19971
20660
|
/** Pre-load skills when this agent activates. */
|
|
19972
|
-
preload_skills:
|
|
19973
|
-
license:
|
|
19974
|
-
homepage:
|
|
20661
|
+
preload_skills: z30.array(z30.string()).optional().default([]),
|
|
20662
|
+
license: z30.string().max(64).optional(),
|
|
20663
|
+
homepage: z30.string().url().optional()
|
|
19975
20664
|
}).strict();
|
|
19976
20665
|
|
|
19977
20666
|
// ../../packages/harness-schema/src/types/hook.ts
|
|
19978
|
-
import { z as
|
|
19979
|
-
var HookEventSchema =
|
|
20667
|
+
import { z as z31 } from "zod";
|
|
20668
|
+
var HookEventSchema = z31.enum([
|
|
19980
20669
|
"PreToolUse",
|
|
19981
20670
|
"PostToolUse",
|
|
19982
20671
|
"UserPromptSubmit",
|
|
@@ -19988,78 +20677,78 @@ var HookEventSchema = z30.enum([
|
|
|
19988
20677
|
"SubagentStop",
|
|
19989
20678
|
"Background"
|
|
19990
20679
|
]);
|
|
19991
|
-
var HookActionSchema =
|
|
19992
|
-
|
|
19993
|
-
type:
|
|
19994
|
-
command:
|
|
19995
|
-
timeout_ms:
|
|
19996
|
-
blocking:
|
|
20680
|
+
var HookActionSchema = z31.union([
|
|
20681
|
+
z31.object({
|
|
20682
|
+
type: z31.literal("command"),
|
|
20683
|
+
command: z31.string().min(1),
|
|
20684
|
+
timeout_ms: z31.number().int().positive().max(6e5).optional(),
|
|
20685
|
+
blocking: z31.boolean().default(false)
|
|
19997
20686
|
}),
|
|
19998
|
-
|
|
19999
|
-
type:
|
|
20000
|
-
url:
|
|
20001
|
-
method:
|
|
20002
|
-
headers:
|
|
20003
|
-
timeout_ms:
|
|
20004
|
-
blocking:
|
|
20687
|
+
z31.object({
|
|
20688
|
+
type: z31.literal("http"),
|
|
20689
|
+
url: z31.string().url(),
|
|
20690
|
+
method: z31.enum(["GET", "POST", "PUT", "PATCH"]).default("POST"),
|
|
20691
|
+
headers: z31.record(z31.string()).optional().default({}),
|
|
20692
|
+
timeout_ms: z31.number().int().positive().max(6e5).optional(),
|
|
20693
|
+
blocking: z31.boolean().default(false)
|
|
20005
20694
|
})
|
|
20006
20695
|
]);
|
|
20007
|
-
var HookBindingSchema =
|
|
20696
|
+
var HookBindingSchema = z31.object({
|
|
20008
20697
|
event: HookEventSchema,
|
|
20009
|
-
matcher:
|
|
20698
|
+
matcher: z31.string().optional(),
|
|
20010
20699
|
action: HookActionSchema
|
|
20011
20700
|
});
|
|
20012
|
-
var HookFrontmatterSchema =
|
|
20013
|
-
name:
|
|
20014
|
-
description:
|
|
20015
|
-
title:
|
|
20701
|
+
var HookFrontmatterSchema = z31.object({
|
|
20702
|
+
name: z31.string().min(1).max(64).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/),
|
|
20703
|
+
description: z31.string().min(1).max(2048),
|
|
20704
|
+
title: z31.string().min(1).max(200).optional(),
|
|
20016
20705
|
version: SemverSchema.optional(),
|
|
20017
20706
|
/** One or more hook bindings. */
|
|
20018
|
-
hooks:
|
|
20707
|
+
hooks: z31.array(HookBindingSchema).min(1),
|
|
20019
20708
|
/** Declared to surface in search filters — always "claude-code" for now. */
|
|
20020
|
-
tool_compat:
|
|
20021
|
-
license:
|
|
20709
|
+
tool_compat: z31.array(z31.enum(["claude-code"])).default(["claude-code"]),
|
|
20710
|
+
license: z31.string().max(64).optional()
|
|
20022
20711
|
}).strict();
|
|
20023
20712
|
|
|
20024
20713
|
// ../../packages/harness-schema/src/types/judgment-pattern.ts
|
|
20025
|
-
import { z as
|
|
20026
|
-
var FailureModeSchema =
|
|
20027
|
-
symptom:
|
|
20028
|
-
correction:
|
|
20714
|
+
import { z as z32 } from "zod";
|
|
20715
|
+
var FailureModeSchema = z32.object({
|
|
20716
|
+
symptom: z32.string().min(1).max(600).describe("the observable sign the judgment was ignored"),
|
|
20717
|
+
correction: z32.string().min(1).max(1200).describe("what to do once the symptom shows")
|
|
20029
20718
|
}).strict();
|
|
20030
|
-
var AdaptHintsSchema =
|
|
20719
|
+
var AdaptHintsSchema = z32.object({
|
|
20031
20720
|
/**
|
|
20032
20721
|
* Parts that transplant verbatim into ANY harness — the invariant core.
|
|
20033
20722
|
* Required, min 1: a judgment-pattern with no universal part has nothing
|
|
20034
20723
|
* to transplant, which makes it a skill/project-rules in disguise, not a
|
|
20035
20724
|
* judgment worth the dedicated type.
|
|
20036
20725
|
*/
|
|
20037
|
-
universal:
|
|
20726
|
+
universal: z32.array(z32.string().min(1).max(400)).min(1, "adapt_hints.universal must list at least one transplantable, harness-agnostic part").max(20),
|
|
20038
20727
|
/** Parts the adapting agent must re-express in the host's stack/conventions. */
|
|
20039
|
-
context_specific:
|
|
20728
|
+
context_specific: z32.array(z32.string().min(1).max(400)).max(20).optional()
|
|
20040
20729
|
}).strict();
|
|
20041
|
-
var JudgmentPatternFrontmatterSchema =
|
|
20042
|
-
name:
|
|
20730
|
+
var JudgmentPatternFrontmatterSchema = z32.object({
|
|
20731
|
+
name: z32.string().min(1).max(64).regex(
|
|
20043
20732
|
/^[a-z0-9]+(-[a-z0-9]+)*$/,
|
|
20044
20733
|
'name must be lowercase-with-hyphens, e.g. "additive-only-migrations"'
|
|
20045
20734
|
),
|
|
20046
|
-
description:
|
|
20735
|
+
description: z32.string().min(1).max(2048),
|
|
20047
20736
|
version: SemverSchema.optional(),
|
|
20048
|
-
title:
|
|
20737
|
+
title: z32.string().min(1).max(200).optional(),
|
|
20049
20738
|
/** The recurring situation / trap this judgment addresses. */
|
|
20050
|
-
problem:
|
|
20739
|
+
problem: z32.string().min(1).max(4e3),
|
|
20051
20740
|
/**
|
|
20052
20741
|
* The judgment itself — free-form Markdown. The agent is the interpreter,
|
|
20053
20742
|
* so this stays unstructured prose (decision tree, heuristics, rationale).
|
|
20054
20743
|
* This is the part that gets transplanted.
|
|
20055
20744
|
*/
|
|
20056
|
-
decision_logic:
|
|
20745
|
+
decision_logic: z32.string().min(1).max(2e4),
|
|
20057
20746
|
/** Triggers — when this judgment should fire. At least one. */
|
|
20058
|
-
when_to_apply:
|
|
20747
|
+
when_to_apply: z32.array(z32.string().min(1).max(400)).min(1).max(12),
|
|
20059
20748
|
/** Counter-triggers — when applying it would be wrong. */
|
|
20060
|
-
when_not_to_apply:
|
|
20749
|
+
when_not_to_apply: z32.array(z32.string().min(1).max(400)).max(12).optional(),
|
|
20061
20750
|
/** How it fails in practice, and how to recover — the hard-won part. */
|
|
20062
|
-
failure_modes:
|
|
20751
|
+
failure_modes: z32.array(FailureModeSchema).max(12).optional(),
|
|
20063
20752
|
/**
|
|
20064
20753
|
* Transplant steering — the field unique to judgment-pattern. Required:
|
|
20065
20754
|
* `adapt_hints.universal` (min 1) is what makes this a judgment-pattern
|
|
@@ -20067,13 +20756,13 @@ var JudgmentPatternFrontmatterSchema = z31.object({
|
|
|
20067
20756
|
*/
|
|
20068
20757
|
adapt_hints: AdaptHintsSchema,
|
|
20069
20758
|
/** Where this judgment came from (project, incident, paper). Provenance. */
|
|
20070
|
-
origin:
|
|
20759
|
+
origin: z32.string().min(1).max(600).optional(),
|
|
20071
20760
|
/** Proof it works (anti-spam lever for AI review). */
|
|
20072
|
-
evidence:
|
|
20761
|
+
evidence: z32.string().min(1).max(2e3).optional(),
|
|
20073
20762
|
// Kordis-forwarded cross-type fields (kept minimal, like skill).
|
|
20074
|
-
tags:
|
|
20075
|
-
tools:
|
|
20076
|
-
license:
|
|
20763
|
+
tags: z32.array(TagSchema).max(16).optional(),
|
|
20764
|
+
tools: z32.array(ToolSlugSchema).optional(),
|
|
20765
|
+
license: z32.string().max(64).optional()
|
|
20077
20766
|
}).strict();
|
|
20078
20767
|
|
|
20079
20768
|
// ../../packages/harness-schema/src/validator.ts
|
|
@@ -20392,7 +21081,7 @@ function isSameBrainNote(publishedOrigin, memoryId) {
|
|
|
20392
21081
|
}
|
|
20393
21082
|
|
|
20394
21083
|
// src/commands/registry/read.ts
|
|
20395
|
-
import { Command as
|
|
21084
|
+
import { Command as Command26 } from "commander";
|
|
20396
21085
|
init_errors();
|
|
20397
21086
|
init_credentials();
|
|
20398
21087
|
|
|
@@ -20415,7 +21104,7 @@ function untrustedFenceNotice(sentinel) {
|
|
|
20415
21104
|
init_ui();
|
|
20416
21105
|
init_version();
|
|
20417
21106
|
function readCommand() {
|
|
20418
|
-
return new
|
|
21107
|
+
return new Command26("read").description("read a judgment from the public layer into your context (nothing is installed)").argument("<full_slug>", "package identifier {username}/{slug} or {username}/{slug}@{version}").option("--allow-pending", "allow versions whose AI review has not passed", false).option("--vault <path>", "which brain records that this was read").option("--local", "record it beside the brain on this machine, not the hosted one").addHelpText(
|
|
20419
21108
|
"after",
|
|
20420
21109
|
"\nEnvironment:\n KORDIS_API_URL=<url> read from this registry instead of the linked one\n"
|
|
20421
21110
|
).action(async (rawSlug, opts) => {
|
|
@@ -20563,7 +21252,7 @@ async function recordUsage(verb, detail, where = {}) {
|
|
|
20563
21252
|
init_vault();
|
|
20564
21253
|
init_ui();
|
|
20565
21254
|
function publishCommand() {
|
|
20566
|
-
return new
|
|
21255
|
+
return new Command27("publish").description("publish one decision from this brain to the public layer").argument("<ref>", "handle, id, path, or exact title of the note to publish").option("--slug <name>", "the package name (default: derived from the note title)").option("--yes", "actually send it (without this, nothing leaves this machine)").option("--reason <text>", "why this is being published \u2014 the user's own words").option("--no-lineage", "do not propose lineage from what this machine has adapted").option(
|
|
20567
21256
|
"--bump <kind>",
|
|
20568
21257
|
"patch | minor | major \u2014 publish the next version of a package this note already produced"
|
|
20569
21258
|
).option("--wait <ms>", "how long to wait for the AI review before answering (default 3000)").option("--vault <path>", "which brain to read").option("--local", "read the brain on this machine, even when signed in to a hosted one").addHelpText(
|
|
@@ -20704,7 +21393,7 @@ async function hostedAssertion(target, ref) {
|
|
|
20704
21393
|
const cloud = await openCloudBrain(target);
|
|
20705
21394
|
const found = await cloudOpen(cloud, ref, await projectAskedFrom());
|
|
20706
21395
|
const memory = MemorySchema.safeParse(found.memory);
|
|
20707
|
-
const claims =
|
|
21396
|
+
const claims = z33.array(ClaimSchema).safeParse(found.claims);
|
|
20708
21397
|
if (!memory.success || !claims.success) {
|
|
20709
21398
|
throw memgError(
|
|
20710
21399
|
"server_refused",
|
|
@@ -20889,13 +21578,13 @@ function parseWait(raw) {
|
|
|
20889
21578
|
}
|
|
20890
21579
|
|
|
20891
21580
|
// src/commands/registry/adapt.ts
|
|
20892
|
-
import { Command as
|
|
21581
|
+
import { Command as Command28 } from "commander";
|
|
20893
21582
|
init_errors();
|
|
20894
21583
|
|
|
20895
21584
|
// src/lib/harness-context.ts
|
|
20896
21585
|
init_brand();
|
|
20897
|
-
import { access, readFile as
|
|
20898
|
-
import { join as
|
|
21586
|
+
import { access, readFile as readFile23 } from "fs/promises";
|
|
21587
|
+
import { join as join23 } from "path";
|
|
20899
21588
|
var PER_FILE_CAP = 16 * 1024;
|
|
20900
21589
|
var TOTAL_CAP = 48 * 1024;
|
|
20901
21590
|
var TRUNCATION_RESERVE = 64;
|
|
@@ -20940,13 +21629,13 @@ function extractHeadings(content) {
|
|
|
20940
21629
|
}
|
|
20941
21630
|
async function readHarnessContext(cwd = process.cwd(), home = agentHome()) {
|
|
20942
21631
|
const candidates = [
|
|
20943
|
-
{ path:
|
|
20944
|
-
{ path:
|
|
20945
|
-
{ path:
|
|
20946
|
-
{ path:
|
|
20947
|
-
{ path:
|
|
20948
|
-
{ path:
|
|
20949
|
-
{ path:
|
|
21632
|
+
{ path: join23(cwd, "CLAUDE.md"), display: "CLAUDE.md", scope: "project" },
|
|
21633
|
+
{ path: join23(cwd, "AGENTS.md"), display: "AGENTS.md", scope: "project" },
|
|
21634
|
+
{ path: join23(cwd, "GEMINI.md"), display: "GEMINI.md", scope: "project" },
|
|
21635
|
+
{ path: join23(cwd, ".claude", "CLAUDE.md"), display: ".claude/CLAUDE.md", scope: "project" },
|
|
21636
|
+
{ path: join23(home, ".claude", "CLAUDE.md"), display: "~/.claude/CLAUDE.md", scope: "global" },
|
|
21637
|
+
{ path: join23(home, ".codex", "AGENTS.md"), display: "~/.codex/AGENTS.md", scope: "global" },
|
|
21638
|
+
{ path: join23(home, ".gemini", "GEMINI.md"), display: "~/.gemini/GEMINI.md", scope: "global" }
|
|
20950
21639
|
];
|
|
20951
21640
|
const files2 = [];
|
|
20952
21641
|
let totalBytes = 0;
|
|
@@ -20954,7 +21643,7 @@ async function readHarnessContext(cwd = process.cwd(), home = agentHome()) {
|
|
|
20954
21643
|
if (!await pathExists(cand.path)) continue;
|
|
20955
21644
|
let raw;
|
|
20956
21645
|
try {
|
|
20957
|
-
raw = await
|
|
21646
|
+
raw = await readFile23(cand.path, "utf8");
|
|
20958
21647
|
} catch {
|
|
20959
21648
|
continue;
|
|
20960
21649
|
}
|
|
@@ -21025,7 +21714,7 @@ function renderBrief(res, harness) {
|
|
|
21025
21714
|
return out.join("\n");
|
|
21026
21715
|
}
|
|
21027
21716
|
function adaptCommand() {
|
|
21028
|
-
return new
|
|
21717
|
+
return new Command28("adapt").description("fit a public judgment to your existing harness (prints a brief; writes nothing)").argument("<full_slug>", "package identifier {username}/{slug} or {username}/{slug}@{version}").option("--allow-pending", "allow versions whose AI review has not passed", false).option("--vault <path>", "which brain records that this was adapted").option("--local", "record it beside the brain on this machine, not the hosted one").addHelpText(
|
|
21029
21718
|
"after",
|
|
21030
21719
|
"\nEnvironment:\n KORDIS_API_URL=<url> read from this registry instead of the linked one\n"
|
|
21031
21720
|
).action(async (rawSlug, opts) => {
|
|
@@ -21073,10 +21762,10 @@ function adaptCommand() {
|
|
|
21073
21762
|
}
|
|
21074
21763
|
|
|
21075
21764
|
// src/commands/registry/info.ts
|
|
21076
|
-
import { Command as
|
|
21765
|
+
import { Command as Command29 } from "commander";
|
|
21077
21766
|
init_ui();
|
|
21078
21767
|
function infoCommand() {
|
|
21079
|
-
return new
|
|
21768
|
+
return new Command29("info").description("what a package in the public layer is, without reading it").argument("<full_slug>", "package identifier {username}/{slug} or {username}/{slug}@{version}").addHelpText(
|
|
21080
21769
|
"after",
|
|
21081
21770
|
"\nEnvironment:\n KORDIS_API_URL=<url> ask this registry instead of the linked one\n"
|
|
21082
21771
|
).action(async (rawSlug) => {
|
|
@@ -21162,7 +21851,7 @@ function renderCanonical(component) {
|
|
|
21162
21851
|
}
|
|
21163
21852
|
|
|
21164
21853
|
// src/commands/registry/search.ts
|
|
21165
|
-
import { Command as
|
|
21854
|
+
import { Command as Command30 } from "commander";
|
|
21166
21855
|
init_errors();
|
|
21167
21856
|
init_ui();
|
|
21168
21857
|
var SEARCHABLE_TYPES = PHASE_1_TYPES;
|
|
@@ -21170,7 +21859,7 @@ var LIMIT_MIN = 1;
|
|
|
21170
21859
|
var LIMIT_MAX = 100;
|
|
21171
21860
|
var LIMIT_DEFAULT = 20;
|
|
21172
21861
|
function searchCommand() {
|
|
21173
|
-
return new
|
|
21862
|
+
return new Command30("search").description("find a judgment in the public layer (titles only; nothing is read or installed)").argument("[query...]", "words to match against title and description").option("--type <type>", `narrow by kind: ${SEARCHABLE_TYPES.join("|")}`).option("--limit <n>", `how many results (${LIMIT_MIN}-${LIMIT_MAX})`, String(LIMIT_DEFAULT)).addHelpText(
|
|
21174
21863
|
"after",
|
|
21175
21864
|
"\nEnvironment:\n KORDIS_API_URL=<url> search this registry instead of the linked one\n"
|
|
21176
21865
|
).action(async (query, opts) => {
|
|
@@ -21348,11 +22037,11 @@ function parseLimit2(raw) {
|
|
|
21348
22037
|
}
|
|
21349
22038
|
|
|
21350
22039
|
// src/commands/registry/pull.ts
|
|
21351
|
-
import { Command as
|
|
21352
|
-
import
|
|
22040
|
+
import { Command as Command31 } from "commander";
|
|
22041
|
+
import prompts4 from "prompts";
|
|
21353
22042
|
import { existsSync, lstatSync, realpathSync } from "fs";
|
|
21354
|
-
import { mkdir as
|
|
21355
|
-
import { dirname as dirname8, join as
|
|
22043
|
+
import { mkdir as mkdir16, readFile as readFile25, stat as stat8, writeFile as writeFile13 } from "fs/promises";
|
|
22044
|
+
import { dirname as dirname8, join as join25, resolve as resolve12, sep as sep3 } from "path";
|
|
21356
22045
|
|
|
21357
22046
|
// ../../packages/translators/src/skill/identity.ts
|
|
21358
22047
|
function buildSkillFile(input, outputPath) {
|
|
@@ -22077,17 +22766,17 @@ init_ui();
|
|
|
22077
22766
|
|
|
22078
22767
|
// src/lib/lockfile.ts
|
|
22079
22768
|
init_brand();
|
|
22080
|
-
import { copyFile, mkdir as
|
|
22769
|
+
import { copyFile, mkdir as mkdir15, readFile as readFile24, writeFile as writeFile12, constants as fsConstants } from "fs/promises";
|
|
22081
22770
|
import { homedir as homedir4 } from "os";
|
|
22082
|
-
import { join as
|
|
22771
|
+
import { join as join24 } from "path";
|
|
22083
22772
|
function effectiveRange(entry) {
|
|
22084
22773
|
return entry.requestedRange ?? `^${entry.version}`;
|
|
22085
22774
|
}
|
|
22086
22775
|
function lockfilePath() {
|
|
22087
|
-
return
|
|
22776
|
+
return join24(brandHome(), "installed.json");
|
|
22088
22777
|
}
|
|
22089
22778
|
function kordisLockfilePath() {
|
|
22090
|
-
return
|
|
22779
|
+
return join24(process.env["KORDIS_HOME"] ?? join24(homedir4(), ".kordis"), "installed.json");
|
|
22091
22780
|
}
|
|
22092
22781
|
function lockfileDisplayPath() {
|
|
22093
22782
|
const path = lockfilePath();
|
|
@@ -22098,13 +22787,13 @@ async function migrateFromKordis() {
|
|
|
22098
22787
|
const source = kordisLockfilePath();
|
|
22099
22788
|
const destination = lockfilePath();
|
|
22100
22789
|
try {
|
|
22101
|
-
await
|
|
22790
|
+
await mkdir15(brandHome(), { recursive: true });
|
|
22102
22791
|
await copyFile(source, destination, fsConstants.COPYFILE_EXCL);
|
|
22103
22792
|
} catch {
|
|
22104
22793
|
return;
|
|
22105
22794
|
}
|
|
22106
22795
|
try {
|
|
22107
|
-
const raw = await
|
|
22796
|
+
const raw = await readFile24(destination, "utf8");
|
|
22108
22797
|
const parsed = JSON.parse(raw);
|
|
22109
22798
|
if (parsed.migratedFrom === void 0) {
|
|
22110
22799
|
parsed.migratedFrom = source;
|
|
@@ -22115,18 +22804,18 @@ async function migrateFromKordis() {
|
|
|
22115
22804
|
}
|
|
22116
22805
|
async function readLockfile() {
|
|
22117
22806
|
try {
|
|
22118
|
-
return JSON.parse(await
|
|
22807
|
+
return JSON.parse(await readFile24(lockfilePath(), "utf8"));
|
|
22119
22808
|
} catch {
|
|
22120
22809
|
await migrateFromKordis();
|
|
22121
22810
|
}
|
|
22122
22811
|
try {
|
|
22123
|
-
return JSON.parse(await
|
|
22812
|
+
return JSON.parse(await readFile24(lockfilePath(), "utf8"));
|
|
22124
22813
|
} catch {
|
|
22125
22814
|
return { packages: {} };
|
|
22126
22815
|
}
|
|
22127
22816
|
}
|
|
22128
22817
|
async function writeLockfile(lf) {
|
|
22129
|
-
await
|
|
22818
|
+
await mkdir15(brandHome(), { recursive: true });
|
|
22130
22819
|
await writeFile12(lockfilePath(), JSON.stringify(lf, null, 2) + "\n", {
|
|
22131
22820
|
encoding: "utf8",
|
|
22132
22821
|
mode: 384
|
|
@@ -22134,7 +22823,7 @@ async function writeLockfile(lf) {
|
|
|
22134
22823
|
}
|
|
22135
22824
|
async function recordInstalled(fullSlug, entry) {
|
|
22136
22825
|
const home = brandHome();
|
|
22137
|
-
await withFileLock(
|
|
22826
|
+
await withFileLock(join24(home, "installed.lock"), home, async () => {
|
|
22138
22827
|
const lf = await readLockfile();
|
|
22139
22828
|
lf.packages[fullSlug] = entry;
|
|
22140
22829
|
await writeLockfile(lf);
|
|
@@ -22197,7 +22886,7 @@ async function promptForExecutableApproval(review) {
|
|
|
22197
22886
|
);
|
|
22198
22887
|
printHuman(c.gray(" \u2500\u2500 END EXECUTABLE CONFIG \u2500\u2500"));
|
|
22199
22888
|
}
|
|
22200
|
-
const { confirmed } = await
|
|
22889
|
+
const { confirmed } = await prompts4({
|
|
22201
22890
|
type: "confirm",
|
|
22202
22891
|
name: "confirmed",
|
|
22203
22892
|
message: "Install and activate these exact executable settings?",
|
|
@@ -22290,7 +22979,7 @@ async function doPullInstall(fullSlug, tool, installRoot, opts = {}) {
|
|
|
22290
22979
|
);
|
|
22291
22980
|
for (const f of translated.files) {
|
|
22292
22981
|
const abs = safeInstallPath(installRoot, f.path);
|
|
22293
|
-
await
|
|
22982
|
+
await mkdir16(dirname8(abs), { recursive: true });
|
|
22294
22983
|
await applyEmittedFile(abs, f, fullSlug, notes);
|
|
22295
22984
|
written.push(abs);
|
|
22296
22985
|
}
|
|
@@ -22319,7 +23008,7 @@ async function doPullInstall(fullSlug, tool, installRoot, opts = {}) {
|
|
|
22319
23008
|
};
|
|
22320
23009
|
}
|
|
22321
23010
|
function registryPullCommand() {
|
|
22322
|
-
return new
|
|
23011
|
+
return new Command31("pull").description("install a package from the public layer into this project").argument(
|
|
22323
23012
|
"<full_slug>",
|
|
22324
23013
|
"package identifier {username}/{slug}, optionally @{version} or @{range}"
|
|
22325
23014
|
).option("--tool <name>", "target tool format (default: work it out from this directory)").option("--target <path>", "install into this directory instead of the current one").option("--with-recommends", "also install recommended (depth-0) dependencies", false).option(
|
|
@@ -22446,7 +23135,7 @@ var TOOL_MARKERS = [
|
|
|
22446
23135
|
async function detectTool(root) {
|
|
22447
23136
|
for (const [path, tool] of TOOL_MARKERS) {
|
|
22448
23137
|
try {
|
|
22449
|
-
await
|
|
23138
|
+
await stat8(join25(root, path));
|
|
22450
23139
|
return tool;
|
|
22451
23140
|
} catch {
|
|
22452
23141
|
}
|
|
@@ -22462,7 +23151,7 @@ function safeInstallPath(installRoot, relPath) {
|
|
|
22462
23151
|
"Nothing was written. The package emits an absolute file path. Tell the author to publish it with relative paths only; if you did not publish it, treat it as an attempt to write outside your project."
|
|
22463
23152
|
);
|
|
22464
23153
|
}
|
|
22465
|
-
const abs =
|
|
23154
|
+
const abs = join25(installRoot, relPath);
|
|
22466
23155
|
const resolvedRoot = resolve12(installRoot);
|
|
22467
23156
|
const resolvedAbs = resolve12(abs);
|
|
22468
23157
|
if (resolvedAbs !== resolvedRoot && !resolvedAbs.startsWith(resolvedRoot + sep3)) {
|
|
@@ -22528,7 +23217,7 @@ async function applyEmittedFile(abs, f, fullSlug, notes) {
|
|
|
22528
23217
|
}
|
|
22529
23218
|
let existingRaw = null;
|
|
22530
23219
|
try {
|
|
22531
|
-
existingRaw = await
|
|
23220
|
+
existingRaw = await readFile25(abs, "utf8");
|
|
22532
23221
|
} catch (err) {
|
|
22533
23222
|
if (err.code !== "ENOENT") throw err;
|
|
22534
23223
|
}
|
|
@@ -22559,10 +23248,10 @@ async function applyEmittedFile(abs, f, fullSlug, notes) {
|
|
|
22559
23248
|
// src/commands/registry/update.ts
|
|
22560
23249
|
init_errors();
|
|
22561
23250
|
init_ui();
|
|
22562
|
-
import { Command as
|
|
23251
|
+
import { Command as Command32 } from "commander";
|
|
22563
23252
|
import semver2 from "semver";
|
|
22564
23253
|
function registryUpdateCommand() {
|
|
22565
|
-
return new
|
|
23254
|
+
return new Command32("update").description("update installed packages to their latest in-range versions").argument("[full_slug]", "package identifier {username}/{slug} (omit to update all)").option("--dry-run", "show what would be updated without doing it", false).addHelpText(
|
|
22566
23255
|
"after",
|
|
22567
23256
|
"\nEnvironment:\n KORDIS_API_URL=<url> ask this registry instead of the linked one\n"
|
|
22568
23257
|
).action(async (fullSlugArg, opts) => {
|
|
@@ -22696,10 +23385,10 @@ function registryUpdateCommand() {
|
|
|
22696
23385
|
|
|
22697
23386
|
// src/commands/registry/outdated.ts
|
|
22698
23387
|
init_ui();
|
|
22699
|
-
import { Command as
|
|
23388
|
+
import { Command as Command33 } from "commander";
|
|
22700
23389
|
import semver3 from "semver";
|
|
22701
23390
|
function outdatedCommand() {
|
|
22702
|
-
return new
|
|
23391
|
+
return new Command33("outdated").description("check installed packages for available updates").addHelpText(
|
|
22703
23392
|
"after",
|
|
22704
23393
|
"\nEnvironment:\n KORDIS_API_URL=<url> ask this registry instead of the linked one\n"
|
|
22705
23394
|
).action(async () => {
|
|
@@ -22791,9 +23480,9 @@ function outdatedCommand() {
|
|
|
22791
23480
|
// src/commands/registry/versions.ts
|
|
22792
23481
|
init_errors();
|
|
22793
23482
|
init_ui();
|
|
22794
|
-
import { Command as
|
|
23483
|
+
import { Command as Command34 } from "commander";
|
|
22795
23484
|
function versionsCommand() {
|
|
22796
|
-
return new
|
|
23485
|
+
return new Command34("versions").description("list every published version of a package").argument("<full_slug>", "package identifier {username}/{slug}").addHelpText(
|
|
22797
23486
|
"after",
|
|
22798
23487
|
"\nEnvironment:\n KORDIS_API_URL=<url> ask this registry instead of the linked one\n"
|
|
22799
23488
|
).action(async (fullSlug) => {
|
|
@@ -22857,9 +23546,9 @@ function relativeTime(isoString) {
|
|
|
22857
23546
|
// src/commands/registry/diff.ts
|
|
22858
23547
|
init_errors();
|
|
22859
23548
|
init_ui();
|
|
22860
|
-
import { Command as
|
|
23549
|
+
import { Command as Command35 } from "commander";
|
|
22861
23550
|
function diffCommand() {
|
|
22862
|
-
return new
|
|
23551
|
+
return new Command35("diff").description("show what changed between two versions of a package").argument(
|
|
22863
23552
|
"<full_slug>",
|
|
22864
23553
|
"package identifier {username}/{slug}, or {username}/{slug}@{from}..{to}"
|
|
22865
23554
|
).argument("[from]", "older version (omit to use the second-latest)").argument("[to]", "newer version (omit to use the latest)").addHelpText(
|
|
@@ -22936,7 +23625,7 @@ function diffCommand() {
|
|
|
22936
23625
|
}
|
|
22937
23626
|
|
|
22938
23627
|
// src/commands/registry/list.ts
|
|
22939
|
-
import { Command as
|
|
23628
|
+
import { Command as Command36 } from "commander";
|
|
22940
23629
|
init_errors();
|
|
22941
23630
|
init_ui();
|
|
22942
23631
|
var SORT_KEYWORDS = ["latest", "popular", "recent", "updated", "stars", "downloads"];
|
|
@@ -22959,7 +23648,7 @@ function parseUsername(raw) {
|
|
|
22959
23648
|
return parsed.data;
|
|
22960
23649
|
}
|
|
22961
23650
|
function listCommand() {
|
|
22962
|
-
return new
|
|
23651
|
+
return new Command36("list").description("list packages \u2014 your own with no argument, or a public user\u2019s").argument("[username]", "whose packages to list (omit for your own)").option("--limit <n>", "maximum results per page", (v) => parseInt(v, 10), 20).option("--page <n>", "page number for your own packages (1-based)", (v) => parseInt(v, 10), 1).option("--cursor <token>", "continue a public listing from a previous result").option(
|
|
22963
23652
|
"--sort <mode>",
|
|
22964
23653
|
"latest|popular for your own; stars|downloads|recent|updated for a public listing",
|
|
22965
23654
|
"latest"
|
|
@@ -23058,7 +23747,7 @@ init_push();
|
|
|
23058
23747
|
init_src();
|
|
23059
23748
|
init_exclude();
|
|
23060
23749
|
init_config();
|
|
23061
|
-
import { Command as
|
|
23750
|
+
import { Command as Command38 } from "commander";
|
|
23062
23751
|
import { basename as basename3, resolve as resolve13 } from "path";
|
|
23063
23752
|
init_index_build();
|
|
23064
23753
|
init_cloud_brain();
|
|
@@ -23067,7 +23756,7 @@ init_held_back();
|
|
|
23067
23756
|
init_vault();
|
|
23068
23757
|
init_ui();
|
|
23069
23758
|
function recallCommand() {
|
|
23070
|
-
return new
|
|
23759
|
+
return new Command38("recall").description("recall memory cards for a question").argument("<query>", "what you want to remember").option("--vault <path>", "which brain to search").option("--local", "search the brain on this machine, even when signed in to a hosted one").option(
|
|
23071
23760
|
"--brain <name>",
|
|
23072
23761
|
"search one existing hosted brain by name or id, without changing the pointer"
|
|
23073
23762
|
).option("--scope <scope>", "restrict to one scope").option("--limit <n>", "max cards", (v) => parseInt(v, 10), 8).option(
|
|
@@ -23742,13 +24431,13 @@ init_setup();
|
|
|
23742
24431
|
init_ui();
|
|
23743
24432
|
init_deadline();
|
|
23744
24433
|
init_src();
|
|
23745
|
-
import { Command as
|
|
24434
|
+
import { Command as Command40 } from "commander";
|
|
23746
24435
|
init_index_build();
|
|
23747
24436
|
init_cloud_brain();
|
|
23748
24437
|
init_vault();
|
|
23749
24438
|
init_ui();
|
|
23750
24439
|
function resurfaceCommand() {
|
|
23751
|
-
return new
|
|
24440
|
+
return new Command40("resurface").description("what is worth having in view right now, without being asked").option("--context-dir <path>", "where the work is happening (default: cwd)").option("--limit <n>", "how many cards", (v) => parseInt(v, 10), 5).option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").option(
|
|
23752
24441
|
"--brain <name>",
|
|
23753
24442
|
"read one existing hosted brain by name or id, without changing the pointer"
|
|
23754
24443
|
).option(
|
|
@@ -23975,11 +24664,11 @@ rank by project yet, so that is not one of the signals above.`
|
|
|
23975
24664
|
}
|
|
23976
24665
|
|
|
23977
24666
|
// src/commands/rules.ts
|
|
23978
|
-
import { Command as
|
|
24667
|
+
import { Command as Command41 } from "commander";
|
|
23979
24668
|
init_cloud_brain();
|
|
23980
24669
|
init_ui();
|
|
23981
24670
|
function rulesCommand() {
|
|
23982
|
-
return new
|
|
24671
|
+
return new Command41("rules").description("the user\u2019s standing decisions \u2014 what binds you here").option("--vault <path>", "which brain").option("--local", "use the brain on this machine, even when signed in to a hosted one").option(
|
|
23983
24672
|
"--brain <name>",
|
|
23984
24673
|
"read one existing hosted brain by name or id, without changing the pointer"
|
|
23985
24674
|
).action(async (opts) => {
|
|
@@ -24146,24 +24835,24 @@ init_next_step();
|
|
|
24146
24835
|
init_provenance();
|
|
24147
24836
|
import { spawn as spawn4 } from "child_process";
|
|
24148
24837
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
24149
|
-
import { mkdir as
|
|
24150
|
-
import { join as
|
|
24151
|
-
import { Command as
|
|
24838
|
+
import { mkdir as mkdir20, open, readFile as readFile30, rename as rename5, rm as rm8, stat as stat10, unlink as unlink7, writeFile as writeFile16 } from "fs/promises";
|
|
24839
|
+
import { join as join32 } from "path";
|
|
24840
|
+
import { Command as Command42 } from "commander";
|
|
24152
24841
|
|
|
24153
24842
|
// src/lib/update-check.ts
|
|
24154
24843
|
init_atomic();
|
|
24155
24844
|
init_brand();
|
|
24156
24845
|
import { execFile as execFile2 } from "child_process";
|
|
24157
|
-
import { mkdir as
|
|
24158
|
-
import { join as
|
|
24846
|
+
import { mkdir as mkdir19, readFile as readFile29, writeFile as writeFile15 } from "fs/promises";
|
|
24847
|
+
import { join as join31 } from "path";
|
|
24159
24848
|
import { promisify as promisify2 } from "util";
|
|
24160
24849
|
import semver4 from "semver";
|
|
24161
|
-
import { z as
|
|
24850
|
+
import { z as z34 } from "zod";
|
|
24162
24851
|
var run2 = promisify2(execFile2);
|
|
24163
|
-
var PolicySchema =
|
|
24164
|
-
latest:
|
|
24165
|
-
minimum_supported:
|
|
24166
|
-
notice:
|
|
24852
|
+
var PolicySchema = z34.object({
|
|
24853
|
+
latest: z34.string().optional(),
|
|
24854
|
+
minimum_supported: z34.string().optional(),
|
|
24855
|
+
notice: z34.string().optional()
|
|
24167
24856
|
});
|
|
24168
24857
|
var POLICY_URL = "https://memgineering.com/version-policy.json";
|
|
24169
24858
|
var NOTICE_MAX = 300;
|
|
@@ -24228,19 +24917,19 @@ function shouldCheck(lastCheck, now) {
|
|
|
24228
24917
|
if (!Number.isFinite(last)) return true;
|
|
24229
24918
|
return now.getTime() - last > 864e5;
|
|
24230
24919
|
}
|
|
24231
|
-
var UpdateStateSchema =
|
|
24232
|
-
at:
|
|
24233
|
-
ok:
|
|
24234
|
-
from:
|
|
24235
|
-
to:
|
|
24236
|
-
message:
|
|
24920
|
+
var UpdateStateSchema = z34.object({
|
|
24921
|
+
at: z34.string(),
|
|
24922
|
+
ok: z34.boolean(),
|
|
24923
|
+
from: z34.string(),
|
|
24924
|
+
to: z34.string().nullable(),
|
|
24925
|
+
message: z34.string().nullable()
|
|
24237
24926
|
});
|
|
24238
24927
|
function statePath() {
|
|
24239
|
-
return
|
|
24928
|
+
return join31(brandHome(), "update-state.json");
|
|
24240
24929
|
}
|
|
24241
24930
|
async function readUpdateState() {
|
|
24242
24931
|
try {
|
|
24243
|
-
const parsed = UpdateStateSchema.safeParse(JSON.parse(await
|
|
24932
|
+
const parsed = UpdateStateSchema.safeParse(JSON.parse(await readFile29(statePath(), "utf8")));
|
|
24244
24933
|
return parsed.success ? parsed.data : null;
|
|
24245
24934
|
} catch {
|
|
24246
24935
|
return null;
|
|
@@ -24248,7 +24937,7 @@ async function readUpdateState() {
|
|
|
24248
24937
|
}
|
|
24249
24938
|
async function writeUpdateState(state) {
|
|
24250
24939
|
try {
|
|
24251
|
-
await
|
|
24940
|
+
await mkdir19(brandHome(), { recursive: true });
|
|
24252
24941
|
await writeThenRename(statePath(), `${JSON.stringify(state, null, 2)}
|
|
24253
24942
|
`);
|
|
24254
24943
|
} catch {
|
|
@@ -24304,7 +24993,7 @@ function shortMessage(err) {
|
|
|
24304
24993
|
// src/commands/update.ts
|
|
24305
24994
|
init_ui();
|
|
24306
24995
|
function updateCommand() {
|
|
24307
|
-
return new
|
|
24996
|
+
return new Command42("update").description("update memgineering itself").option("--check", "report what is available and change nothing").action(async (opts) => {
|
|
24308
24997
|
const inherited = process.env[UPDATE_CLAIM_ENV] || void 0;
|
|
24309
24998
|
try {
|
|
24310
24999
|
await runUpdate(opts, inherited);
|
|
@@ -24360,7 +25049,7 @@ async function runUpdate(opts, inherited) {
|
|
|
24360
25049
|
}
|
|
24361
25050
|
const held = await holdUpdateSlot(inherited);
|
|
24362
25051
|
if (held === null && await blockedBy() === "stuck") {
|
|
24363
|
-
const lock =
|
|
25052
|
+
const lock = join32(brandHome(), RECLAIM_LOCK);
|
|
24364
25053
|
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.`;
|
|
24365
25054
|
printDual({
|
|
24366
25055
|
json: { ...status, provenance: provenance.kind, updated: false, reason: "update_claim_stuck", hint },
|
|
@@ -24475,14 +25164,14 @@ function newClaimToken() {
|
|
|
24475
25164
|
}
|
|
24476
25165
|
async function claimOwner() {
|
|
24477
25166
|
try {
|
|
24478
|
-
return (await
|
|
25167
|
+
return (await readFile30(join32(brandHome(), "update-running"), "utf8")).trim() || null;
|
|
24479
25168
|
} catch {
|
|
24480
25169
|
return null;
|
|
24481
25170
|
}
|
|
24482
25171
|
}
|
|
24483
25172
|
async function releaseUpdateSlot(token) {
|
|
24484
25173
|
if (await claimOwner() !== token) return;
|
|
24485
|
-
await
|
|
25174
|
+
await unlink7(join32(brandHome(), "update-running")).catch(() => void 0);
|
|
24486
25175
|
}
|
|
24487
25176
|
async function holdUpdateSlot(inherited) {
|
|
24488
25177
|
if (inherited !== void 0 && await claimOwner() === inherited) return inherited;
|
|
@@ -24494,9 +25183,9 @@ async function holdUpdateSlot(inherited) {
|
|
|
24494
25183
|
var HOOK_VERBS = /* @__PURE__ */ new Set(["guard", "resurface"]);
|
|
24495
25184
|
var UPDATE_CLAIM_MS = 15 * 6e4;
|
|
24496
25185
|
async function claimUpdateSlot(token = newClaimToken(), hooks = {}) {
|
|
24497
|
-
const path =
|
|
25186
|
+
const path = join32(brandHome(), "update-running");
|
|
24498
25187
|
try {
|
|
24499
|
-
await
|
|
25188
|
+
await mkdir20(brandHome(), { recursive: true });
|
|
24500
25189
|
const handle = await open(path, "wx");
|
|
24501
25190
|
await handle.writeFile(`${token}
|
|
24502
25191
|
`);
|
|
@@ -24506,12 +25195,12 @@ async function claimUpdateSlot(token = newClaimToken(), hooks = {}) {
|
|
|
24506
25195
|
if (err?.code !== "EEXIST") return false;
|
|
24507
25196
|
}
|
|
24508
25197
|
try {
|
|
24509
|
-
if (Date.now() - (await
|
|
25198
|
+
if (Date.now() - (await stat10(path)).mtimeMs >= UPDATE_CLAIM_MS) {
|
|
24510
25199
|
await hooks.afterObserve?.();
|
|
24511
25200
|
await withReclaimLock(async () => {
|
|
24512
|
-
if (Date.now() - (await
|
|
25201
|
+
if (Date.now() - (await stat10(path)).mtimeMs < UPDATE_CLAIM_MS) return;
|
|
24513
25202
|
await hooks.afterRecheck?.();
|
|
24514
|
-
await
|
|
25203
|
+
await unlink7(path);
|
|
24515
25204
|
});
|
|
24516
25205
|
}
|
|
24517
25206
|
} catch {
|
|
@@ -24522,10 +25211,10 @@ var RECLAIM_LOCK = "update-running.reclaim";
|
|
|
24522
25211
|
var RECLAIM_LOCK_MAX_MS = 10 * 6e4;
|
|
24523
25212
|
async function withReclaimLock(fn) {
|
|
24524
25213
|
const home = brandHome();
|
|
24525
|
-
const lock =
|
|
24526
|
-
const staging =
|
|
24527
|
-
await
|
|
24528
|
-
await writeFile16(
|
|
25214
|
+
const lock = join32(home, RECLAIM_LOCK);
|
|
25215
|
+
const staging = join32(home, `${RECLAIM_LOCK}.${process.pid}.${randomBytes2(4).toString("hex")}`);
|
|
25216
|
+
await mkdir20(staging, { recursive: true });
|
|
25217
|
+
await writeFile16(join32(staging, "owner"), `${process.pid} ${Date.now()}
|
|
24529
25218
|
`);
|
|
24530
25219
|
try {
|
|
24531
25220
|
await rename5(staging, lock);
|
|
@@ -24543,11 +25232,11 @@ async function withReclaimLock(fn) {
|
|
|
24543
25232
|
}
|
|
24544
25233
|
async function breakAbandonedLock(lock) {
|
|
24545
25234
|
try {
|
|
24546
|
-
const seen = (await
|
|
25235
|
+
const seen = (await readFile30(join32(lock, "owner"), "utf8")).trim();
|
|
24547
25236
|
if (!lockAbandoned(seen)) return;
|
|
24548
25237
|
const aside = `${lock}.dead.${randomBytes2(4).toString("hex")}`;
|
|
24549
25238
|
await rename5(lock, aside);
|
|
24550
|
-
const moved = (await
|
|
25239
|
+
const moved = (await readFile30(join32(aside, "owner"), "utf8").catch(() => "")).trim();
|
|
24551
25240
|
if (moved !== seen) {
|
|
24552
25241
|
await rename5(aside, lock).catch(() => void 0);
|
|
24553
25242
|
return;
|
|
@@ -24571,9 +25260,9 @@ function lockAbandoned(owner, now = Date.now()) {
|
|
|
24571
25260
|
}
|
|
24572
25261
|
async function blockedBy() {
|
|
24573
25262
|
try {
|
|
24574
|
-
const old = Date.now() - (await
|
|
25263
|
+
const old = Date.now() - (await stat10(join32(brandHome(), "update-running"))).mtimeMs >= UPDATE_CLAIM_MS;
|
|
24575
25264
|
if (old) {
|
|
24576
|
-
await
|
|
25265
|
+
await stat10(join32(brandHome(), RECLAIM_LOCK));
|
|
24577
25266
|
return "stuck";
|
|
24578
25267
|
}
|
|
24579
25268
|
} catch {
|
|
@@ -24605,16 +25294,16 @@ function reportBelowMinimum(below) {
|
|
|
24605
25294
|
|
|
24606
25295
|
// src/commands/undo.ts
|
|
24607
25296
|
init_src();
|
|
24608
|
-
import { readFile as
|
|
24609
|
-
import { join as
|
|
24610
|
-
import { Command as
|
|
25297
|
+
import { readFile as readFile31, rm as rm9 } from "fs/promises";
|
|
25298
|
+
import { join as join33 } from "path";
|
|
25299
|
+
import { Command as Command43 } from "commander";
|
|
24611
25300
|
init_atomic();
|
|
24612
25301
|
init_errors();
|
|
24613
25302
|
init_cloud_brain();
|
|
24614
25303
|
init_vault();
|
|
24615
25304
|
init_ui();
|
|
24616
25305
|
function undoCommand() {
|
|
24617
|
-
return new
|
|
25306
|
+
return new Command43("undo").description("take back the last change, or a named one").argument("[op]", "operation id (default: the most recent)").option("--reason <text>", "why it is being taken back \u2014 kept in the ledger").option("--vault <path>", "which brain to act on").option("--local", "act on the brain on this machine, even when signed in to a hosted one").action(
|
|
24618
25307
|
async (opId, opts) => {
|
|
24619
25308
|
const rationale = sanitizeRationale(opts.reason);
|
|
24620
25309
|
const where = await resolveTarget({
|
|
@@ -24703,8 +25392,8 @@ ${matches.slice(0, 8).map((id) => ` ${id}`).join("\n")}${matches.length > 8 ? "
|
|
|
24703
25392
|
);
|
|
24704
25393
|
}
|
|
24705
25394
|
const record = await withWriteLock(brain.root, async () => {
|
|
24706
|
-
const absolute =
|
|
24707
|
-
const current = await
|
|
25395
|
+
const absolute = join33(brain.root, target.target);
|
|
25396
|
+
const current = await readFile31(absolute, "utf8").catch(() => null);
|
|
24708
25397
|
const actualHash = current === null ? null : hashContent(current);
|
|
24709
25398
|
if (actualHash !== target.hash_after) {
|
|
24710
25399
|
throw memgError(
|
|
@@ -24766,7 +25455,7 @@ function minutesSince(iso) {
|
|
|
24766
25455
|
return Math.max(0, Math.floor((Date.now() - at) / 6e4));
|
|
24767
25456
|
}
|
|
24768
25457
|
async function reverse(brain, op, current) {
|
|
24769
|
-
const absolute =
|
|
25458
|
+
const absolute = join33(brain.root, op.target);
|
|
24770
25459
|
if (op.verb === "remember") {
|
|
24771
25460
|
if (current === null) {
|
|
24772
25461
|
throw memgError(
|
|
@@ -24823,7 +25512,7 @@ async function conflictHint(vaultRoot, op) {
|
|
|
24823
25512
|
Read what it says now, then decide what should stay.`;
|
|
24824
25513
|
const snapshot = await readSnapshot(vaultRoot, op.op_id);
|
|
24825
25514
|
return snapshot === null ? base2 : `${base2}
|
|
24826
|
-
The text from before that change is still on this machine: ${
|
|
25515
|
+
The text from before that change is still on this machine: ${join33(snapshotDir(vaultRoot), `${op.op_id}.txt`)}`;
|
|
24827
25516
|
}
|
|
24828
25517
|
|
|
24829
25518
|
// src/commands/brains.ts
|
|
@@ -24832,11 +25521,11 @@ init_api_client();
|
|
|
24832
25521
|
init_cloud_brain();
|
|
24833
25522
|
init_config();
|
|
24834
25523
|
init_ui();
|
|
24835
|
-
import { Command as
|
|
25524
|
+
import { Command as Command44 } from "commander";
|
|
24836
25525
|
import stringWidth from "string-width";
|
|
24837
25526
|
var NAME_WIDTH2 = 48;
|
|
24838
25527
|
function brainsCommand() {
|
|
24839
|
-
return new
|
|
25528
|
+
return new Command44("brains").description("list the hosted brains on your account, and which one this machine reads").addHelpText(
|
|
24840
25529
|
"after",
|
|
24841
25530
|
"\nReads only. `memgineering use --brain <name>` points this machine at one of\nthem. `memgineering create <name>` makes a new empty one, and\n`push --brain <name>` makes one from a folder on this machine."
|
|
24842
25531
|
).action(async () => {
|
|
@@ -24916,7 +25605,7 @@ init_credentials();
|
|
|
24916
25605
|
init_cloud_brain();
|
|
24917
25606
|
init_config();
|
|
24918
25607
|
import { isAbsolute as isAbsolute5, relative as relative2, resolve as resolve16 } from "path";
|
|
24919
|
-
import { Command as
|
|
25608
|
+
import { Command as Command45 } from "commander";
|
|
24920
25609
|
init_vault();
|
|
24921
25610
|
init_ui();
|
|
24922
25611
|
function lostDefaultLines(missing) {
|
|
@@ -24935,7 +25624,7 @@ var VIA_MEANS = {
|
|
|
24935
25624
|
default: "because it is your default brain (`memgineering use --default <path>` changes it)"
|
|
24936
25625
|
};
|
|
24937
25626
|
function useCommand() {
|
|
24938
|
-
return new
|
|
25627
|
+
return new Command45("use").description(
|
|
24939
25628
|
"say which brain answers here \u2014 `--brain <name>` for the whole machine, or a path to bind this folder"
|
|
24940
25629
|
).argument("[brain]", "path to the brain \u2014 omit to show what this folder resolves to").option("--brain <name>", "point this machine at a hosted brain, by its name or id").option("--here <dir>", "write the pointer in this directory instead of the cwd").option(
|
|
24941
25630
|
"--default",
|
|
@@ -25243,9 +25932,9 @@ async function pointAtHosted(named, opts, path) {
|
|
|
25243
25932
|
// src/commands/whoami.ts
|
|
25244
25933
|
init_api_client();
|
|
25245
25934
|
init_ui();
|
|
25246
|
-
import { Command as
|
|
25935
|
+
import { Command as Command46 } from "commander";
|
|
25247
25936
|
function whoamiCommand() {
|
|
25248
|
-
return new
|
|
25937
|
+
return new Command46("whoami").description("show which account this machine is signed in as").action(async () => {
|
|
25249
25938
|
const base2 = apiUrl();
|
|
25250
25939
|
const credentials = await requireCredentials(base2);
|
|
25251
25940
|
const res = await apiRequest({
|
|
@@ -25320,7 +26009,7 @@ var NO_FLAT_ALIAS = new Set(HOMONYM_VERBS);
|
|
|
25320
26009
|
// src/program.ts
|
|
25321
26010
|
init_version();
|
|
25322
26011
|
function buildProgram() {
|
|
25323
|
-
const program = new
|
|
26012
|
+
const program = new Command47("memgineering").version(VERSION).description("memgineering \u2014 one memory for the AI you connect").option("--json", "emit JSON to stdout instead of markdown", false).hook("preAction", async (thisCommand, actionCommand) => {
|
|
25324
26013
|
const opts = thisCommand.optsWithGlobals();
|
|
25325
26014
|
if (opts.json || process.env[BRAND.jsonEnvVar] === "1") setJsonMode(true);
|
|
25326
26015
|
await announceNextStep(commandPath(actionCommand), "verb");
|
|
@@ -25349,7 +26038,7 @@ function buildProgram() {
|
|
|
25349
26038
|
diffCommand,
|
|
25350
26039
|
listCommand
|
|
25351
26040
|
];
|
|
25352
|
-
const registry = new
|
|
26041
|
+
const registry = new Command47("registry").description(
|
|
25353
26042
|
"the public layer: find, read, adapt, publish, install and keep packages current"
|
|
25354
26043
|
);
|
|
25355
26044
|
for (const factory of publicLayer) registry.addCommand(factory());
|
|
@@ -25387,6 +26076,7 @@ function buildProgram() {
|
|
|
25387
26076
|
program.addCommand(setupCommand());
|
|
25388
26077
|
program.addCommand(loginCommand());
|
|
25389
26078
|
program.addCommand(logoutCommand());
|
|
26079
|
+
program.addCommand(deleteAccountCommand());
|
|
25390
26080
|
program.addCommand(whoamiCommand());
|
|
25391
26081
|
program.addCommand(updateCommand());
|
|
25392
26082
|
makeParseErrorsActionable(program);
|
|
@@ -25406,6 +26096,7 @@ var WITHOUT_NEXT_STEP = /* @__PURE__ */ new Set([
|
|
|
25406
26096
|
"setup",
|
|
25407
26097
|
"login",
|
|
25408
26098
|
"logout",
|
|
26099
|
+
"delete-account",
|
|
25409
26100
|
"whoami",
|
|
25410
26101
|
"update",
|
|
25411
26102
|
"guard",
|