memgineering 0.25.3 → 0.27.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 +182 -0
- package/assets/MEMGINEERING.md +1 -1
- package/assets/memgineering-recall/SKILL.md +64 -10
- package/assets/memgineering-setup/SKILL.md +29 -14
- package/dist/index.js +1320 -528
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1325,8 +1325,8 @@ var init_exclude = __esm({
|
|
|
1325
1325
|
[...ALWAYS_SKIP_DIRS].map((d) => fold(d))
|
|
1326
1326
|
);
|
|
1327
1327
|
ALWAYS_SKIP_SEGMENTS_FOLDED = ALWAYS_SKIP_SEGMENTS.map((s) => fold(s));
|
|
1328
|
-
ALWAYS_SKIP_FILE = (
|
|
1329
|
-
const b = fold(
|
|
1328
|
+
ALWAYS_SKIP_FILE = (basename5) => {
|
|
1329
|
+
const b = fold(basename5);
|
|
1330
1330
|
return b.startsWith(".env") || b === ".ds_store" || b === ".memgdeny" || b === ".memgallow" || b === ".memgignore";
|
|
1331
1331
|
};
|
|
1332
1332
|
EMPTY_IGNORE_SET = { rules: [], unsupported: [] };
|
|
@@ -4971,9 +4971,112 @@ var init_version = __esm({
|
|
|
4971
4971
|
}
|
|
4972
4972
|
});
|
|
4973
4973
|
|
|
4974
|
+
// ../../packages/storage-adapter/src/local-file/atomic.ts
|
|
4975
|
+
import { rename as rename2, rm as rm3, writeFile as writeFile2 } from "fs/promises";
|
|
4976
|
+
async function writeThenRename(abs, content) {
|
|
4977
|
+
const tmp = `${abs}.${process.pid}.tmp`;
|
|
4978
|
+
try {
|
|
4979
|
+
await writeFile2(tmp, content, { encoding: "utf8", mode: 384 });
|
|
4980
|
+
await rename2(tmp, abs);
|
|
4981
|
+
} catch (err) {
|
|
4982
|
+
await rm3(tmp, { force: true });
|
|
4983
|
+
throw err;
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4986
|
+
var init_atomic = __esm({
|
|
4987
|
+
"../../packages/storage-adapter/src/local-file/atomic.ts"() {
|
|
4988
|
+
"use strict";
|
|
4989
|
+
}
|
|
4990
|
+
});
|
|
4991
|
+
|
|
4992
|
+
// src/lib/path-lock.ts
|
|
4993
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
4994
|
+
import { readdir as readdir2, readFile as readFile3, stat, unlink, writeFile as writeFile3 } from "fs/promises";
|
|
4995
|
+
import { basename, dirname as dirname2, join as join5 } from "path";
|
|
4996
|
+
async function lockHolder(path) {
|
|
4997
|
+
try {
|
|
4998
|
+
const [info, token] = await Promise.all([stat(path), readFile3(path, "utf8")]);
|
|
4999
|
+
return { token, ageMs: Date.now() - info.mtimeMs };
|
|
5000
|
+
} catch {
|
|
5001
|
+
return null;
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
async function unlinkIfStill(path, token) {
|
|
5005
|
+
const now = await readFile3(path, "utf8").catch(() => null);
|
|
5006
|
+
if (now === token) await unlink(path).catch(() => void 0);
|
|
5007
|
+
}
|
|
5008
|
+
async function takeOver(path, dead) {
|
|
5009
|
+
const takeoverPath = `${path}.takeover`;
|
|
5010
|
+
try {
|
|
5011
|
+
await writeFile3(takeoverPath, "", { flag: "wx", mode: 384 });
|
|
5012
|
+
} catch (err) {
|
|
5013
|
+
if (err.code === "EEXIST") {
|
|
5014
|
+
const age = await stat(takeoverPath).then(
|
|
5015
|
+
(info) => Date.now() - info.mtimeMs,
|
|
5016
|
+
() => null
|
|
5017
|
+
);
|
|
5018
|
+
if (age !== null && age > STALE_LOCK_MS) await unlink(takeoverPath).catch(() => void 0);
|
|
5019
|
+
}
|
|
5020
|
+
return false;
|
|
5021
|
+
}
|
|
5022
|
+
try {
|
|
5023
|
+
const held = await lockHolder(path);
|
|
5024
|
+
if (held !== null && held.token === dead && held.ageMs > STALE_LOCK_MS) {
|
|
5025
|
+
await unlink(path).catch(() => void 0);
|
|
5026
|
+
}
|
|
5027
|
+
return true;
|
|
5028
|
+
} finally {
|
|
5029
|
+
await unlink(takeoverPath).catch(() => void 0);
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
async function withPathLock(path, fn, waitMs = 0) {
|
|
5033
|
+
const token = `${process.pid}-${randomUUID2()}`;
|
|
5034
|
+
const deadline = Date.now() + waitMs;
|
|
5035
|
+
for (let vanished = 0; ; ) {
|
|
5036
|
+
try {
|
|
5037
|
+
await writeFile3(path, token, { flag: "wx", mode: 384 });
|
|
5038
|
+
break;
|
|
5039
|
+
} catch (err) {
|
|
5040
|
+
if (err.code !== "EEXIST") return null;
|
|
5041
|
+
}
|
|
5042
|
+
const held = await lockHolder(path);
|
|
5043
|
+
if (held === null) {
|
|
5044
|
+
vanished += 1;
|
|
5045
|
+
if (vanished > 20) return null;
|
|
5046
|
+
continue;
|
|
5047
|
+
}
|
|
5048
|
+
if (held.ageMs > STALE_LOCK_MS && await takeOver(path, held.token)) continue;
|
|
5049
|
+
if (Date.now() >= deadline) return null;
|
|
5050
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
5051
|
+
}
|
|
5052
|
+
try {
|
|
5053
|
+
return await fn();
|
|
5054
|
+
} finally {
|
|
5055
|
+
await unlinkIfStill(path, token);
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
async function clearStaleTemps(path) {
|
|
5059
|
+
const dir = dirname2(path);
|
|
5060
|
+
const prefix = `${basename(path)}.`;
|
|
5061
|
+
const names = await readdir2(dir).catch(() => []);
|
|
5062
|
+
await Promise.all(
|
|
5063
|
+
names.filter((n) => n.startsWith(prefix) && /\.\d+\.tmp$/.test(n)).map(async (n) => {
|
|
5064
|
+
const age = await stat(join5(dir, n)).then((s) => Date.now() - s.mtimeMs).catch(() => 0);
|
|
5065
|
+
if (age > STALE_LOCK_MS) await unlink(join5(dir, n)).catch(() => void 0);
|
|
5066
|
+
})
|
|
5067
|
+
);
|
|
5068
|
+
}
|
|
5069
|
+
var STALE_LOCK_MS;
|
|
5070
|
+
var init_path_lock = __esm({
|
|
5071
|
+
"src/lib/path-lock.ts"() {
|
|
5072
|
+
"use strict";
|
|
5073
|
+
STALE_LOCK_MS = 3e4;
|
|
5074
|
+
}
|
|
5075
|
+
});
|
|
5076
|
+
|
|
4974
5077
|
// src/lib/credentials.ts
|
|
4975
|
-
import { mkdir as mkdir3, readFile as
|
|
4976
|
-
import { join as
|
|
5078
|
+
import { mkdir as mkdir3, readFile as readFile4, unlink as unlink2 } from "fs/promises";
|
|
5079
|
+
import { join as join6 } from "path";
|
|
4977
5080
|
import { z as z6 } from "zod";
|
|
4978
5081
|
function registryFromClaim(raw) {
|
|
4979
5082
|
const parsed = RegistryCredentialsSchema.omit({ linked_at: true }).safeParse(raw);
|
|
@@ -4981,34 +5084,53 @@ function registryFromClaim(raw) {
|
|
|
4981
5084
|
return { ...parsed.data, linked_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4982
5085
|
}
|
|
4983
5086
|
function credentialsPath() {
|
|
4984
|
-
return
|
|
5087
|
+
return join6(brandHome(), "credentials.json");
|
|
4985
5088
|
}
|
|
4986
5089
|
async function readCredentials() {
|
|
4987
5090
|
try {
|
|
4988
5091
|
const parsed = CredentialsSchema.safeParse(
|
|
4989
|
-
JSON.parse(await
|
|
5092
|
+
JSON.parse(await readFile4(credentialsPath(), "utf8"))
|
|
4990
5093
|
);
|
|
4991
5094
|
return parsed.success ? parsed.data : null;
|
|
4992
5095
|
} catch {
|
|
4993
5096
|
return null;
|
|
4994
5097
|
}
|
|
4995
5098
|
}
|
|
5099
|
+
async function underLock(change) {
|
|
5100
|
+
const held = await withPathLock(
|
|
5101
|
+
join6(brandHome(), "credentials.lock"),
|
|
5102
|
+
async () => ({ result: await change() }),
|
|
5103
|
+
LOCK_WAIT_MS
|
|
5104
|
+
);
|
|
5105
|
+
return held === null ? change() : held.result;
|
|
5106
|
+
}
|
|
5107
|
+
async function writeWhole(credentials) {
|
|
5108
|
+
await clearStaleTemps(credentialsPath());
|
|
5109
|
+
await writeThenRename(credentialsPath(), `${JSON.stringify(credentials, null, 2)}
|
|
5110
|
+
`);
|
|
5111
|
+
}
|
|
4996
5112
|
async function saveCredentials(credentials) {
|
|
4997
5113
|
await mkdir3(brandHome(), { recursive: true });
|
|
4998
|
-
await
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5114
|
+
await underLock(() => writeWhole(credentials));
|
|
5115
|
+
}
|
|
5116
|
+
async function saveRotatedRegistry(sent, rotated) {
|
|
5117
|
+
return underLock(async () => {
|
|
5118
|
+
const stored = await readCredentials();
|
|
5119
|
+
if (stored?.registry === void 0 || stored.registry.refresh_token !== sent) return false;
|
|
5120
|
+
await writeWhole({ ...stored, registry: { ...stored.registry, ...rotated } });
|
|
5121
|
+
return true;
|
|
5002
5122
|
});
|
|
5003
5123
|
}
|
|
5004
5124
|
async function clearCredentials() {
|
|
5005
|
-
await
|
|
5125
|
+
await underLock(() => unlink2(credentialsPath()).catch(() => void 0));
|
|
5006
5126
|
}
|
|
5007
|
-
var RegistryCredentialsSchema, CredentialsSchema;
|
|
5127
|
+
var RegistryCredentialsSchema, CredentialsSchema, LOCK_WAIT_MS;
|
|
5008
5128
|
var init_credentials = __esm({
|
|
5009
5129
|
"src/lib/credentials.ts"() {
|
|
5010
5130
|
"use strict";
|
|
5131
|
+
init_atomic();
|
|
5011
5132
|
init_brand();
|
|
5133
|
+
init_path_lock();
|
|
5012
5134
|
RegistryCredentialsSchema = z6.object({
|
|
5013
5135
|
api_url: z6.string().min(1),
|
|
5014
5136
|
refresh_token: z6.string().min(1),
|
|
@@ -5032,20 +5154,21 @@ var init_credentials = __esm({
|
|
|
5032
5154
|
*/
|
|
5033
5155
|
registry: RegistryCredentialsSchema.optional().catch(void 0)
|
|
5034
5156
|
});
|
|
5157
|
+
LOCK_WAIT_MS = 2e3;
|
|
5035
5158
|
}
|
|
5036
5159
|
});
|
|
5037
5160
|
|
|
5038
5161
|
// src/lib/pending-login.ts
|
|
5039
|
-
import { mkdir as mkdir4, readFile as
|
|
5040
|
-
import { join as
|
|
5162
|
+
import { mkdir as mkdir4, readFile as readFile5, unlink as unlink3, writeFile as writeFile4 } from "fs/promises";
|
|
5163
|
+
import { join as join7 } from "path";
|
|
5041
5164
|
import { z as z7 } from "zod";
|
|
5042
5165
|
function pendingLoginPath() {
|
|
5043
|
-
return
|
|
5166
|
+
return join7(brandHome(), "pending-login.json");
|
|
5044
5167
|
}
|
|
5045
5168
|
async function readPendingLogin() {
|
|
5046
5169
|
try {
|
|
5047
5170
|
const parsed = PendingLoginSchema.safeParse(
|
|
5048
|
-
JSON.parse(await
|
|
5171
|
+
JSON.parse(await readFile5(pendingLoginPath(), "utf8"))
|
|
5049
5172
|
);
|
|
5050
5173
|
return parsed.success ? parsed.data : null;
|
|
5051
5174
|
} catch {
|
|
@@ -5054,14 +5177,14 @@ async function readPendingLogin() {
|
|
|
5054
5177
|
}
|
|
5055
5178
|
async function savePendingLogin(pending) {
|
|
5056
5179
|
await mkdir4(brandHome(), { recursive: true });
|
|
5057
|
-
await
|
|
5058
|
-
await
|
|
5180
|
+
await unlink3(pendingLoginPath()).catch(() => void 0);
|
|
5181
|
+
await writeFile4(pendingLoginPath(), JSON.stringify(pending, null, 2) + "\n", {
|
|
5059
5182
|
encoding: "utf8",
|
|
5060
5183
|
mode: 384
|
|
5061
5184
|
});
|
|
5062
5185
|
}
|
|
5063
5186
|
async function clearPendingLogin() {
|
|
5064
|
-
await
|
|
5187
|
+
await unlink3(pendingLoginPath()).catch(() => void 0);
|
|
5065
5188
|
}
|
|
5066
5189
|
function pendingExpired(pending, now = Date.now()) {
|
|
5067
5190
|
return Math.floor(now / 1e3) >= pending.expires_at_epoch;
|
|
@@ -5329,27 +5452,9 @@ var init_api_client = __esm({
|
|
|
5329
5452
|
}
|
|
5330
5453
|
});
|
|
5331
5454
|
|
|
5332
|
-
// ../../packages/storage-adapter/src/local-file/atomic.ts
|
|
5333
|
-
import { rename as rename2, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
5334
|
-
async function writeThenRename(abs, content) {
|
|
5335
|
-
const tmp = `${abs}.${process.pid}.tmp`;
|
|
5336
|
-
try {
|
|
5337
|
-
await writeFile4(tmp, content, { encoding: "utf8", mode: 384 });
|
|
5338
|
-
await rename2(tmp, abs);
|
|
5339
|
-
} catch (err) {
|
|
5340
|
-
await rm3(tmp, { force: true });
|
|
5341
|
-
throw err;
|
|
5342
|
-
}
|
|
5343
|
-
}
|
|
5344
|
-
var init_atomic = __esm({
|
|
5345
|
-
"../../packages/storage-adapter/src/local-file/atomic.ts"() {
|
|
5346
|
-
"use strict";
|
|
5347
|
-
}
|
|
5348
|
-
});
|
|
5349
|
-
|
|
5350
5455
|
// src/lib/config.ts
|
|
5351
|
-
import { mkdir as mkdir5, readFile as
|
|
5352
|
-
import { join as
|
|
5456
|
+
import { mkdir as mkdir5, readFile as readFile6 } from "fs/promises";
|
|
5457
|
+
import { join as join8, resolve } from "path";
|
|
5353
5458
|
import { z as z8 } from "zod";
|
|
5354
5459
|
function withDefaultBrain(brain) {
|
|
5355
5460
|
if (brain.default) return brain;
|
|
@@ -5357,11 +5462,11 @@ function withDefaultBrain(brain) {
|
|
|
5357
5462
|
return oldest ? { ...brain, default: oldest.root } : brain;
|
|
5358
5463
|
}
|
|
5359
5464
|
function configPath() {
|
|
5360
|
-
return
|
|
5465
|
+
return join8(brandHome(), "config.json");
|
|
5361
5466
|
}
|
|
5362
5467
|
async function loadConfig() {
|
|
5363
5468
|
try {
|
|
5364
|
-
const parsed = ConfigSchema.safeParse(JSON.parse(await
|
|
5469
|
+
const parsed = ConfigSchema.safeParse(JSON.parse(await readFile6(configPath(), "utf8")));
|
|
5365
5470
|
return parsed.success ? parsed.data : EMPTY_CONFIG;
|
|
5366
5471
|
} catch {
|
|
5367
5472
|
return EMPTY_CONFIG;
|
|
@@ -5541,6 +5646,13 @@ var init_config = __esm({
|
|
|
5541
5646
|
* Absent on a machine set up before it was recorded.
|
|
5542
5647
|
*/
|
|
5543
5648
|
hook: z8.enum(["on", "off"]).optional().catch(void 0),
|
|
5649
|
+
/**
|
|
5650
|
+
* Which "left your hooks alone" notices the automatic hook refresh has
|
|
5651
|
+
* already given, as `<file>:<reason>` — so a symlinked or unparseable
|
|
5652
|
+
* settings file is mentioned once, not at the start of every session
|
|
5653
|
+
* (lib/hook-refresh.ts, `refreshNotice`).
|
|
5654
|
+
*/
|
|
5655
|
+
hookNoticesTold: z8.array(z8.string()).optional().catch(void 0),
|
|
5544
5656
|
/**
|
|
5545
5657
|
* Notes `setup --web` was asked to upload, not yet sent.
|
|
5546
5658
|
*
|
|
@@ -5738,8 +5850,8 @@ var init_cloud_brain = __esm({
|
|
|
5738
5850
|
});
|
|
5739
5851
|
|
|
5740
5852
|
// ../../packages/storage-adapter/src/local-file/walk.ts
|
|
5741
|
-
import { lstat, readdir as
|
|
5742
|
-
import { join as
|
|
5853
|
+
import { lstat, readdir as readdir4, realpath } from "fs/promises";
|
|
5854
|
+
import { join as join10, relative, resolve as resolve2, sep } from "path";
|
|
5743
5855
|
async function walkVault(root, opts = {}) {
|
|
5744
5856
|
const maxDepth = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
5745
5857
|
const rootAbs = resolve2(root);
|
|
@@ -5751,7 +5863,7 @@ async function walkVault(root, opts = {}) {
|
|
|
5751
5863
|
if (depth > maxDepth) return;
|
|
5752
5864
|
let entries;
|
|
5753
5865
|
try {
|
|
5754
|
-
entries = await
|
|
5866
|
+
entries = await readdir4(dirAbs, { withFileTypes: true });
|
|
5755
5867
|
} catch {
|
|
5756
5868
|
return;
|
|
5757
5869
|
}
|
|
@@ -5768,7 +5880,7 @@ async function walkVault(root, opts = {}) {
|
|
|
5768
5880
|
continue;
|
|
5769
5881
|
}
|
|
5770
5882
|
if (isDir) {
|
|
5771
|
-
await visit(
|
|
5883
|
+
await visit(join10(dirAbs, entry.name), rel, depth + 1);
|
|
5772
5884
|
} else if (entry.isFile()) {
|
|
5773
5885
|
files2.push(rel);
|
|
5774
5886
|
}
|
|
@@ -5799,7 +5911,7 @@ async function resolveRealInside(root, relPath, seenDirs) {
|
|
|
5799
5911
|
const segments = relativeTo(rootAbs, abs).split("/");
|
|
5800
5912
|
let cursor = rootAbs;
|
|
5801
5913
|
for (const [i, segment] of segments.entries()) {
|
|
5802
|
-
cursor =
|
|
5914
|
+
cursor = join10(cursor, segment);
|
|
5803
5915
|
const isLast = i === segments.length - 1;
|
|
5804
5916
|
if (!isLast && seenDirs?.has(cursor)) continue;
|
|
5805
5917
|
const st = await lstat(cursor);
|
|
@@ -5823,7 +5935,7 @@ var init_walk = __esm({
|
|
|
5823
5935
|
});
|
|
5824
5936
|
|
|
5825
5937
|
// ../../packages/storage-adapter/src/local-file/rules-file.ts
|
|
5826
|
-
import { readFile as
|
|
5938
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
5827
5939
|
import { parse as parseYaml3 } from "yaml";
|
|
5828
5940
|
function requireStringList(value, key2) {
|
|
5829
5941
|
if (value === void 0 || value === null) return [];
|
|
@@ -5862,7 +5974,7 @@ function ignoreSetFrom(entries) {
|
|
|
5862
5974
|
async function readRulesFile(root) {
|
|
5863
5975
|
let raw;
|
|
5864
5976
|
try {
|
|
5865
|
-
raw = await
|
|
5977
|
+
raw = await readFile8(resolveInside(root, RULES_FILE), "utf8");
|
|
5866
5978
|
} catch (err) {
|
|
5867
5979
|
if (err?.code === "ENOENT") return EMPTY_VAULT_RULES;
|
|
5868
5980
|
throw new RulesFileError(
|
|
@@ -5968,7 +6080,7 @@ edit.
|
|
|
5968
6080
|
|
|
5969
6081
|
// ../../packages/storage-adapter/src/local-file/adapter.ts
|
|
5970
6082
|
import { createHash as createHash4 } from "crypto";
|
|
5971
|
-
import { lstat as lstat2, readFile as
|
|
6083
|
+
import { lstat as lstat2, readFile as readFile9 } from "fs/promises";
|
|
5972
6084
|
var LocalFileReadAdapter;
|
|
5973
6085
|
var init_adapter = __esm({
|
|
5974
6086
|
"../../packages/storage-adapter/src/local-file/adapter.ts"() {
|
|
@@ -6088,7 +6200,7 @@ var init_adapter = __esm({
|
|
|
6088
6200
|
/** Read `.memgdeny` / `.memgallow`. Missing is fine; unreadable is not. */
|
|
6089
6201
|
static async readDenyFile(root, name) {
|
|
6090
6202
|
try {
|
|
6091
|
-
return parseDenyFile(await
|
|
6203
|
+
return parseDenyFile(await readFile9(resolveInside(root, name), "utf8"));
|
|
6092
6204
|
} catch (err) {
|
|
6093
6205
|
if (err?.code === "ENOENT") return EMPTY_DENY_LIST;
|
|
6094
6206
|
throw err;
|
|
@@ -6107,7 +6219,7 @@ var init_adapter = __esm({
|
|
|
6107
6219
|
static async readIgnoreFiles(root) {
|
|
6108
6220
|
const load = async (name) => {
|
|
6109
6221
|
try {
|
|
6110
|
-
return parseIgnoreFile(await
|
|
6222
|
+
return parseIgnoreFile(await readFile9(resolveInside(root, name), "utf8"));
|
|
6111
6223
|
} catch (err) {
|
|
6112
6224
|
if (err?.code === "ENOENT") return EMPTY_IGNORE_SET;
|
|
6113
6225
|
throw err;
|
|
@@ -6143,7 +6255,7 @@ var init_adapter = __esm({
|
|
|
6143
6255
|
}
|
|
6144
6256
|
const st = await lstat2(abs);
|
|
6145
6257
|
if (!st.isFile()) throw new Error(`not a regular file: ${path}`);
|
|
6146
|
-
const bytes = await
|
|
6258
|
+
const bytes = await readFile9(abs);
|
|
6147
6259
|
if (this.#protect) {
|
|
6148
6260
|
let hits = secretContentHits(bytes.toString("utf8"));
|
|
6149
6261
|
if (isExactlyListed(rel, this.#allow)) {
|
|
@@ -6251,8 +6363,8 @@ var init_adapter = __esm({
|
|
|
6251
6363
|
});
|
|
6252
6364
|
|
|
6253
6365
|
// src/lib/vault.ts
|
|
6254
|
-
import { readFile as
|
|
6255
|
-
import { dirname as
|
|
6366
|
+
import { readFile as readFile10, realpath as realpath2, stat as stat2 } from "fs/promises";
|
|
6367
|
+
import { dirname as dirname3, isAbsolute, join as join11, resolve as resolve3 } from "path";
|
|
6256
6368
|
async function resolveBrain(explicit, cwd = process.cwd()) {
|
|
6257
6369
|
const cfg = await loadConfig();
|
|
6258
6370
|
const brains = cfg.brain.brains;
|
|
@@ -6317,7 +6429,7 @@ A folder marked (missing) is not on disk right now. If it is gone for good, \`me
|
|
|
6317
6429
|
}
|
|
6318
6430
|
async function liveBrains(brains) {
|
|
6319
6431
|
const checked = await Promise.all(
|
|
6320
|
-
brains.map(async (b) => (await
|
|
6432
|
+
brains.map(async (b) => (await stat2(b.root).catch(() => null))?.isDirectory() ? b : null)
|
|
6321
6433
|
);
|
|
6322
6434
|
return checked.filter((b) => b !== null);
|
|
6323
6435
|
}
|
|
@@ -6332,9 +6444,9 @@ function isInside(child, parent) {
|
|
|
6332
6444
|
async function readPointer(cwd) {
|
|
6333
6445
|
let dir = resolve3(cwd);
|
|
6334
6446
|
for (; ; ) {
|
|
6335
|
-
const candidate =
|
|
6447
|
+
const candidate = join11(dir, BRAND.pointerFileName);
|
|
6336
6448
|
try {
|
|
6337
|
-
const raw = (await
|
|
6449
|
+
const raw = (await readFile10(candidate, "utf8")).trim();
|
|
6338
6450
|
const target = raw.split("\n")[0]?.trim();
|
|
6339
6451
|
if (target) {
|
|
6340
6452
|
return {
|
|
@@ -6344,7 +6456,7 @@ async function readPointer(cwd) {
|
|
|
6344
6456
|
}
|
|
6345
6457
|
} catch {
|
|
6346
6458
|
}
|
|
6347
|
-
const parent =
|
|
6459
|
+
const parent = dirname3(dir);
|
|
6348
6460
|
if (parent === dir) return null;
|
|
6349
6461
|
dir = parent;
|
|
6350
6462
|
}
|
|
@@ -6401,7 +6513,8 @@ function heldBack(scan) {
|
|
|
6401
6513
|
byName,
|
|
6402
6514
|
explicitly,
|
|
6403
6515
|
byRule,
|
|
6404
|
-
symlinked
|
|
6516
|
+
symlinked,
|
|
6517
|
+
otherSymlinks: scan.skippedSymlinks.filter((path) => !isNote(path))
|
|
6405
6518
|
};
|
|
6406
6519
|
}
|
|
6407
6520
|
function heldBackJson(held, totalKey = "not_sent") {
|
|
@@ -6418,7 +6531,9 @@ function heldBackJson(held, totalKey = "not_sent") {
|
|
|
6418
6531
|
// Present so a zero total can be read correctly. A non-empty list here means
|
|
6419
6532
|
// the count above is a floor, not the answer — nothing inside these was
|
|
6420
6533
|
// walked, so nobody knows how many notes they hold.
|
|
6421
|
-
excluded_folders: held.folders
|
|
6534
|
+
excluded_folders: held.folders,
|
|
6535
|
+
// The same floor for links: a symlinked folder of notes was never walked.
|
|
6536
|
+
skipped_other_symlinks: held.otherSymlinks
|
|
6422
6537
|
};
|
|
6423
6538
|
}
|
|
6424
6539
|
function heldBackGroups(held) {
|
|
@@ -6444,11 +6559,14 @@ function describeHeldBack(held) {
|
|
|
6444
6559
|
),
|
|
6445
6560
|
...held.folders.map(
|
|
6446
6561
|
(dir) => `${oneLine(dir, WIDTH.path)}/ \u2014 an excluded folder, whose contents were never walked`
|
|
6562
|
+
),
|
|
6563
|
+
...held.otherSymlinks.map(
|
|
6564
|
+
(path) => `${oneLine(path, WIDTH.path)} \u2014 a symlink, never followed; a folder behind it was never walked`
|
|
6447
6565
|
)
|
|
6448
6566
|
];
|
|
6449
6567
|
}
|
|
6450
6568
|
function printHeldBack(held, wording) {
|
|
6451
|
-
if (held.total === 0 && held.folders.length === 0) return;
|
|
6569
|
+
if (held.total === 0 && held.folders.length === 0 && held.otherSymlinks.length === 0) return;
|
|
6452
6570
|
const guessed = held.byName;
|
|
6453
6571
|
const chosen = heldBackGroups(held).filter(({ label }) => label !== NAME_GUESS_LABEL);
|
|
6454
6572
|
const chosenCount = chosen.reduce((n, g) => n + g.rows.length, 0);
|
|
@@ -6502,6 +6620,21 @@ ${chosenCount} note(s) are not in ${wording.where}, because your rules say so:`
|
|
|
6502
6620
|
)
|
|
6503
6621
|
);
|
|
6504
6622
|
}
|
|
6623
|
+
if (held.otherSymlinks.length > 0) {
|
|
6624
|
+
printHuman(
|
|
6625
|
+
c.gray(
|
|
6626
|
+
`
|
|
6627
|
+
${held.otherSymlinks.length} symlink(s) were not followed, so nothing behind them is in ${wording.where} \u2014
|
|
6628
|
+
a folder of notes behind one is not counted above:`
|
|
6629
|
+
)
|
|
6630
|
+
);
|
|
6631
|
+
for (const path of held.otherSymlinks.slice(0, 10)) {
|
|
6632
|
+
printHuman(c.gray(` ${oneLine(path, WIDTH.path)}`));
|
|
6633
|
+
}
|
|
6634
|
+
if (held.otherSymlinks.length > 10) {
|
|
6635
|
+
printHuman(c.gray(` \u2026 and ${held.otherSymlinks.length - 10} more`));
|
|
6636
|
+
}
|
|
6637
|
+
}
|
|
6505
6638
|
}
|
|
6506
6639
|
var isNote, NAME_GUESS_LABEL;
|
|
6507
6640
|
var init_held_back = __esm({
|
|
@@ -6530,8 +6663,8 @@ var init_index_build = __esm({
|
|
|
6530
6663
|
});
|
|
6531
6664
|
|
|
6532
6665
|
// src/commands/init.ts
|
|
6533
|
-
import { mkdir as mkdir9, readdir as
|
|
6534
|
-
import { join as
|
|
6666
|
+
import { mkdir as mkdir9, readdir as readdir6, writeFile as writeFile7 } from "fs/promises";
|
|
6667
|
+
import { join as join16, resolve as resolve6 } from "path";
|
|
6535
6668
|
import { Command as Command4 } from "commander";
|
|
6536
6669
|
function initCommand() {
|
|
6537
6670
|
return new Command4("init").description("create a new brain, laid out and ready to write into").argument("<path>", "where the brain should live").option("--name <name>", "what to call it in the hub", "My brain").action(async (path, opts) => {
|
|
@@ -6566,7 +6699,7 @@ function initCommand() {
|
|
|
6566
6699
|
}
|
|
6567
6700
|
async function createBrain(path, name) {
|
|
6568
6701
|
const root = resolve6(path);
|
|
6569
|
-
const existing = await
|
|
6702
|
+
const existing = await readdir6(root).catch(() => null);
|
|
6570
6703
|
if (existing && existing.filter((e) => !e.startsWith(".")).length > 0) {
|
|
6571
6704
|
throw memgError(
|
|
6572
6705
|
"already_initialized",
|
|
@@ -6576,9 +6709,9 @@ async function createBrain(path, name) {
|
|
|
6576
6709
|
);
|
|
6577
6710
|
}
|
|
6578
6711
|
await mkdir9(root, { recursive: true });
|
|
6579
|
-
for (const dir of FOLDERS) await mkdir9(
|
|
6712
|
+
for (const dir of FOLDERS) await mkdir9(join16(root, dir), { recursive: true });
|
|
6580
6713
|
for (const [file, content] of Object.entries(files(name))) {
|
|
6581
|
-
await writeFile7(
|
|
6714
|
+
await writeFile7(join16(root, file), content, "utf8");
|
|
6582
6715
|
}
|
|
6583
6716
|
const canonicalRoot2 = await canonicalize(root);
|
|
6584
6717
|
const adapter = await LocalFileReadAdapter.openVault(canonicalRoot2);
|
|
@@ -6732,8 +6865,8 @@ var init_cloud_pointer = __esm({
|
|
|
6732
6865
|
});
|
|
6733
6866
|
|
|
6734
6867
|
// src/commands/link.ts
|
|
6735
|
-
import { readFile as
|
|
6736
|
-
import { join as
|
|
6868
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
6869
|
+
import { join as join19 } from "path";
|
|
6737
6870
|
import { Command as Command13 } from "commander";
|
|
6738
6871
|
import prompts2 from "prompts";
|
|
6739
6872
|
function sameBasename(all, entry) {
|
|
@@ -6937,7 +7070,7 @@ async function recordLink(adapter, observationsDir) {
|
|
|
6937
7070
|
}
|
|
6938
7071
|
async function refuseSilentRules(path, denied, excluded, unsupported) {
|
|
6939
7072
|
const denyEntries = (await LocalFileReadAdapter.readDenyFile(path, ".memgdeny")).entries;
|
|
6940
|
-
const rawDenyLines = await
|
|
7073
|
+
const rawDenyLines = await readFile19(join19(path, ".memgdeny"), "utf8").then(
|
|
6941
7074
|
(t) => t.split("\n").map((l) => l.trim()).filter((l) => l !== "" && !l.startsWith("#"))
|
|
6942
7075
|
).catch(() => []);
|
|
6943
7076
|
const asWritten = (folded) => rawDenyLines.find(
|
|
@@ -7565,6 +7698,155 @@ var init_push = __esm({
|
|
|
7565
7698
|
}
|
|
7566
7699
|
});
|
|
7567
7700
|
|
|
7701
|
+
// src/lib/guard-shell.ts
|
|
7702
|
+
function guardHookCommand() {
|
|
7703
|
+
return `/bin/sh -c '${GUARD_SHELL_SCRIPT}'`;
|
|
7704
|
+
}
|
|
7705
|
+
var GUARD_SHELL_SCRIPT;
|
|
7706
|
+
var init_guard_shell = __esm({
|
|
7707
|
+
"src/lib/guard-shell.ts"() {
|
|
7708
|
+
"use strict";
|
|
7709
|
+
GUARD_SHELL_SCRIPT = [
|
|
7710
|
+
'h="${MEMGINEERING_HOME-$HOME/.memgineering}"',
|
|
7711
|
+
'f="$h/guard/fast"',
|
|
7712
|
+
'c="$h/config.json"',
|
|
7713
|
+
'k="$h/credentials.json"',
|
|
7714
|
+
"g() { memgineering guard; exit 0; }",
|
|
7715
|
+
'[ -f "$c" ] && [ -f "$k" ] && [ -d "$f" ] || g',
|
|
7716
|
+
"p=$(cat)",
|
|
7717
|
+
'go() { printf "%s\\n" "$p" | g; exit 0; }',
|
|
7718
|
+
'[ -n "$p" ] || exit 0',
|
|
7719
|
+
'q=\\"',
|
|
7720
|
+
'case "$p" in */memory/* | *"$q"memory/* | *"\\\\/memory\\\\/"* | *"$q"memory\\\\/*) go ;; esac',
|
|
7721
|
+
'w=${p%%"$q"*}',
|
|
7722
|
+
'r=${p#"$w"}',
|
|
7723
|
+
'case "$r" in "$q"session_id"$q"*) ;; *) go ;; esac',
|
|
7724
|
+
'r=${r#"$q"session_id"$q"}',
|
|
7725
|
+
"w=${r%%[![:space:]]*}",
|
|
7726
|
+
'r=${r#"$w"}',
|
|
7727
|
+
'case "$r" in :*) ;; *) go ;; esac',
|
|
7728
|
+
"r=${r#:}",
|
|
7729
|
+
"w=${r%%[![:space:]]*}",
|
|
7730
|
+
'r=${r#"$w"}',
|
|
7731
|
+
'case "$r" in "$q"*) ;; *) go ;; esac',
|
|
7732
|
+
'r=${r#"$q"}',
|
|
7733
|
+
's=${r%%"$q"*}',
|
|
7734
|
+
'case "$s" in "" | *[!A-Za-z0-9-]*) go ;; esac',
|
|
7735
|
+
'm=""',
|
|
7736
|
+
'if [ -f "$f/$s" ]; then m="$f/$s"; else',
|
|
7737
|
+
' n=""',
|
|
7738
|
+
' for e in "$f/$s".empty.*; do',
|
|
7739
|
+
' [ -f "$e" ] || continue',
|
|
7740
|
+
" t=${e##*.}",
|
|
7741
|
+
' case "$t" in "" | *[!0-9]*) continue ;; esac',
|
|
7742
|
+
' [ -n "$n" ] || n=$(date +%s)',
|
|
7743
|
+
' if [ "$t" -gt "$n" ]; then m="$e"; break; fi',
|
|
7744
|
+
" done",
|
|
7745
|
+
' [ -n "$m" ] || go',
|
|
7746
|
+
' [ "$f/.rules-changed" -nt "$m" ] && go',
|
|
7747
|
+
"fi",
|
|
7748
|
+
'[ "$c" -nt "$m" ] && go',
|
|
7749
|
+
'[ "$k" -nt "$m" ] && go',
|
|
7750
|
+
"exit 0"
|
|
7751
|
+
].join("\n");
|
|
7752
|
+
}
|
|
7753
|
+
});
|
|
7754
|
+
|
|
7755
|
+
// src/lib/hook-refresh.ts
|
|
7756
|
+
import { chmod, lstat as lstat5, readFile as readFile25 } from "fs/promises";
|
|
7757
|
+
import { join as join25 } from "path";
|
|
7758
|
+
function moveShippedCommands(parsed, moves) {
|
|
7759
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
7760
|
+
const settings = parsed;
|
|
7761
|
+
const hooks = settings["hooks"];
|
|
7762
|
+
if (hooks === void 0) return { next: settings, changed: 0 };
|
|
7763
|
+
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return null;
|
|
7764
|
+
let changed = 0;
|
|
7765
|
+
const nextHooks = { ...hooks };
|
|
7766
|
+
for (const move of moves) {
|
|
7767
|
+
const groups = nextHooks[move.event];
|
|
7768
|
+
if (!Array.isArray(groups)) continue;
|
|
7769
|
+
nextHooks[move.event] = groups.map((group) => {
|
|
7770
|
+
const entries = group?.hooks;
|
|
7771
|
+
if (!Array.isArray(entries)) return group;
|
|
7772
|
+
let touched = false;
|
|
7773
|
+
const nextEntries = entries.map((entry) => {
|
|
7774
|
+
const command = entry?.command;
|
|
7775
|
+
if (typeof command !== "string" || command === move.to || !move.from.includes(command)) {
|
|
7776
|
+
return entry;
|
|
7777
|
+
}
|
|
7778
|
+
touched = true;
|
|
7779
|
+
changed += 1;
|
|
7780
|
+
return { ...entry, command: move.to };
|
|
7781
|
+
});
|
|
7782
|
+
return touched ? { ...group, hooks: nextEntries } : group;
|
|
7783
|
+
});
|
|
7784
|
+
}
|
|
7785
|
+
return { next: changed === 0 ? settings : { ...settings, hooks: nextHooks }, changed };
|
|
7786
|
+
}
|
|
7787
|
+
function withHookFileLock(fn, waitMs = 0) {
|
|
7788
|
+
return withPathLock(join25(brandHome(), "hook-files.lock"), fn, waitMs);
|
|
7789
|
+
}
|
|
7790
|
+
async function readSettings(path) {
|
|
7791
|
+
const raw = await readFile25(path, "utf8").catch(() => null);
|
|
7792
|
+
if (raw === null) return void 0;
|
|
7793
|
+
try {
|
|
7794
|
+
return JSON.parse(raw);
|
|
7795
|
+
} catch {
|
|
7796
|
+
return null;
|
|
7797
|
+
}
|
|
7798
|
+
}
|
|
7799
|
+
async function refreshHookFile(path, moves) {
|
|
7800
|
+
const info = await lstat5(path).catch(() => null);
|
|
7801
|
+
if (info === null) return "missing";
|
|
7802
|
+
if (info.isSymbolicLink()) return "symlink";
|
|
7803
|
+
await clearStaleTemps(path);
|
|
7804
|
+
const first = moveShippedCommands(await readSettings(path), moves);
|
|
7805
|
+
if (first === null) return "refused";
|
|
7806
|
+
if (first.changed === 0) return "unchanged";
|
|
7807
|
+
const outcome = await withHookFileLock(async () => {
|
|
7808
|
+
const again = moveShippedCommands(await readSettings(path), moves);
|
|
7809
|
+
if (again === null) return "refused";
|
|
7810
|
+
if (again.changed === 0) return "unchanged";
|
|
7811
|
+
await writeThenRename(path, `${JSON.stringify(again.next, null, 2)}
|
|
7812
|
+
`);
|
|
7813
|
+
await chmod(path, info.mode & 4095).catch(() => void 0);
|
|
7814
|
+
return "changed";
|
|
7815
|
+
});
|
|
7816
|
+
return outcome ?? "busy";
|
|
7817
|
+
}
|
|
7818
|
+
function refreshNotice(report3, told) {
|
|
7819
|
+
const lines2 = [];
|
|
7820
|
+
if (report3.changed.length > 0) {
|
|
7821
|
+
lines2.push(
|
|
7822
|
+
`memgineering brought its own hooks up to this version in ${report3.changed.join(", ")} \u2014 the before-edit check now answers an already-served session without starting node.`
|
|
7823
|
+
);
|
|
7824
|
+
if (report3.changed.some((p) => /[/\\]\.codex[/\\]hooks\.json$/.test(p))) {
|
|
7825
|
+
lines2.push(
|
|
7826
|
+
"Codex will ask to review the changed hook the next time it starts: trust it (press `t`) \u2014 until then neither memgineering hook runs in Codex."
|
|
7827
|
+
);
|
|
7828
|
+
}
|
|
7829
|
+
}
|
|
7830
|
+
const nowTold = [...told];
|
|
7831
|
+
for (const skip of report3.skipped) {
|
|
7832
|
+
const key2 = `${skip.path}:${skip.why}`;
|
|
7833
|
+
if (nowTold.includes(key2)) continue;
|
|
7834
|
+
nowTold.push(key2);
|
|
7835
|
+
lines2.push(
|
|
7836
|
+
skip.why === "symlink" ? `memgineering left its hooks in ${skip.path} as they are: the file is a link, and rewriting it would replace the link. \`memgineering setup\` updates them when you want it to.` : `memgineering left its hooks in ${skip.path} as they are: the file does not parse as JSON. Fix it, then \`memgineering setup\` updates them.`
|
|
7837
|
+
);
|
|
7838
|
+
}
|
|
7839
|
+
return { text: lines2.length === 0 ? null : lines2.join("\n"), told: nowTold };
|
|
7840
|
+
}
|
|
7841
|
+
var init_hook_refresh = __esm({
|
|
7842
|
+
"src/lib/hook-refresh.ts"() {
|
|
7843
|
+
"use strict";
|
|
7844
|
+
init_atomic();
|
|
7845
|
+
init_brand();
|
|
7846
|
+
init_path_lock();
|
|
7847
|
+
}
|
|
7848
|
+
});
|
|
7849
|
+
|
|
7568
7850
|
// src/commands/setup-brain.ts
|
|
7569
7851
|
function migration(localBrains) {
|
|
7570
7852
|
return ` They also keep ${localBrains} brain(s) in folders on this machine. Offer moving those notes to the account instead of starting empty: \`memgineering push --brain <name>\` uploads a folder into a hosted brain and points here (nothing local is deleted), then \`memgineering reindex\` makes the moved notes searchable by meaning. The memgineering-setup skill has the migration steps.`;
|
|
@@ -7899,38 +8181,38 @@ var init_setup_signin = __esm({
|
|
|
7899
8181
|
});
|
|
7900
8182
|
|
|
7901
8183
|
// src/lib/next-step.ts
|
|
7902
|
-
import { mkdir as
|
|
7903
|
-
import { join as
|
|
8184
|
+
import { mkdir as mkdir16, readFile as readFile26, writeFile as writeFile14 } from "fs/promises";
|
|
8185
|
+
import { join as join26 } from "path";
|
|
7904
8186
|
function isSetUp(cfg) {
|
|
7905
8187
|
return Object.keys(cfg.system.installed).length > 0;
|
|
7906
8188
|
}
|
|
7907
8189
|
function connectedMarker(at) {
|
|
7908
|
-
return
|
|
8190
|
+
return join26(brandHome(), `setup-connected-shown-${at.replace(/[^0-9A-Za-z]/g, "")}`);
|
|
7909
8191
|
}
|
|
7910
8192
|
async function connectedNoticeShown(at) {
|
|
7911
|
-
return
|
|
8193
|
+
return readFile26(connectedMarker(at), "utf8").then(
|
|
7912
8194
|
() => true,
|
|
7913
8195
|
() => false
|
|
7914
8196
|
);
|
|
7915
8197
|
}
|
|
7916
8198
|
async function markConnectedNoticeShown(at) {
|
|
7917
8199
|
try {
|
|
7918
|
-
await
|
|
7919
|
-
await
|
|
8200
|
+
await mkdir16(brandHome(), { recursive: true });
|
|
8201
|
+
await writeFile14(connectedMarker(at), `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
7920
8202
|
`);
|
|
7921
8203
|
} catch {
|
|
7922
8204
|
}
|
|
7923
8205
|
}
|
|
7924
8206
|
async function claimFullSetupNotice() {
|
|
7925
|
-
const marker =
|
|
8207
|
+
const marker = join26(brandHome(), "setup-notice-shown");
|
|
7926
8208
|
try {
|
|
7927
|
-
await
|
|
8209
|
+
await readFile26(marker, "utf8");
|
|
7928
8210
|
return false;
|
|
7929
8211
|
} catch {
|
|
7930
8212
|
}
|
|
7931
8213
|
try {
|
|
7932
|
-
await
|
|
7933
|
-
await
|
|
8214
|
+
await mkdir16(brandHome(), { recursive: true });
|
|
8215
|
+
await writeFile14(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
7934
8216
|
`);
|
|
7935
8217
|
} catch {
|
|
7936
8218
|
}
|
|
@@ -8096,12 +8378,12 @@ Synced instead? \`memgineering login\`, then \`memgineering create <name>\`.`;
|
|
|
8096
8378
|
// src/lib/provenance.ts
|
|
8097
8379
|
import { execFile, spawn as spawn2 } from "child_process";
|
|
8098
8380
|
import { realpath as realpath3 } from "fs/promises";
|
|
8099
|
-
import { dirname as
|
|
8381
|
+
import { dirname as dirname9, join as join27, resolve as resolve14 } from "path";
|
|
8100
8382
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8101
8383
|
import { promisify } from "util";
|
|
8102
8384
|
async function detectProvenance(selfPath, opts = {}) {
|
|
8103
8385
|
const entry = selfPath ?? fileURLToPath2(import.meta.url);
|
|
8104
|
-
const selfDir = await realpath3(
|
|
8386
|
+
const selfDir = await realpath3(dirname9(entry)).catch(() => dirname9(entry));
|
|
8105
8387
|
if (/[\\/]_npx[\\/]|[\\/]\.npm[\\/]_cacache[\\/]/.test(selfDir)) {
|
|
8106
8388
|
return {
|
|
8107
8389
|
kind: "npx",
|
|
@@ -8219,7 +8501,7 @@ function isInside2(child, parent) {
|
|
|
8219
8501
|
}
|
|
8220
8502
|
async function unreachableByName(selfPath) {
|
|
8221
8503
|
const entry = selfPath ?? fileURLToPath2(import.meta.url);
|
|
8222
|
-
const selfDir = await realpath3(
|
|
8504
|
+
const selfDir = await realpath3(dirname9(entry)).catch(() => dirname9(entry));
|
|
8223
8505
|
if (!/[\\/](node_modules|_npx|_cacache)[\\/]/.test(`${selfDir}/`)) return null;
|
|
8224
8506
|
const p = await detectProvenance(entry);
|
|
8225
8507
|
return p.kind === "local" || p.kind === "npx" ? p.reason : null;
|
|
@@ -8234,7 +8516,7 @@ var init_provenance = __esm({
|
|
|
8234
8516
|
kind: "npm",
|
|
8235
8517
|
bin: "npm",
|
|
8236
8518
|
prefixArgs: ["prefix", "-g"],
|
|
8237
|
-
moduleRoot: (prefix) => process.platform === "win32" ?
|
|
8519
|
+
moduleRoot: (prefix) => process.platform === "win32" ? join27(prefix, "node_modules") : join27(prefix, "lib", "node_modules"),
|
|
8238
8520
|
install: "npm i -g memgineering@latest"
|
|
8239
8521
|
},
|
|
8240
8522
|
{
|
|
@@ -8271,9 +8553,9 @@ __export(setup_cloud_exports, {
|
|
|
8271
8553
|
});
|
|
8272
8554
|
import { spawn as spawn3 } from "child_process";
|
|
8273
8555
|
import { realpathSync as realpathSync2 } from "fs";
|
|
8274
|
-
import { readdir as
|
|
8556
|
+
import { readdir as readdir9, stat as stat8 } from "fs/promises";
|
|
8275
8557
|
import { homedir as homedir5 } from "os";
|
|
8276
|
-
import { basename as
|
|
8558
|
+
import { basename as basename4, dirname as dirname10, join as join28, resolve as resolve15, sep as sep4 } from "path";
|
|
8277
8559
|
async function report2(setupToken, state, detail, code) {
|
|
8278
8560
|
try {
|
|
8279
8561
|
await apiRequest({
|
|
@@ -8291,16 +8573,16 @@ async function report2(setupToken, state, detail, code) {
|
|
|
8291
8573
|
}
|
|
8292
8574
|
async function folderCandidates() {
|
|
8293
8575
|
const home = homedir5();
|
|
8294
|
-
const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) =>
|
|
8576
|
+
const roots = ["Documents", "notes", "Notes", "Obsidian", "vault"].map((d) => join28(home, d));
|
|
8295
8577
|
const found = [];
|
|
8296
8578
|
const countNotes = async (dir) => {
|
|
8297
8579
|
let count = 0;
|
|
8298
|
-
const entries = await
|
|
8580
|
+
const entries = await readdir9(dir, { withFileTypes: true }).catch(() => []);
|
|
8299
8581
|
for (const entry of entries) {
|
|
8300
8582
|
if (entry.name.startsWith(".")) continue;
|
|
8301
8583
|
if (entry.isFile() && /\.(md|markdown)$/i.test(entry.name)) count += 1;
|
|
8302
8584
|
else if (entry.isDirectory()) {
|
|
8303
|
-
const inner = await
|
|
8585
|
+
const inner = await readdir9(join28(dir, entry.name)).catch(() => []);
|
|
8304
8586
|
count += inner.filter((n) => /\.(md|markdown)$/i.test(n)).length;
|
|
8305
8587
|
}
|
|
8306
8588
|
if (count > 999) break;
|
|
@@ -8308,7 +8590,7 @@ async function folderCandidates() {
|
|
|
8308
8590
|
return count;
|
|
8309
8591
|
};
|
|
8310
8592
|
for (const root of roots) {
|
|
8311
|
-
const info = await
|
|
8593
|
+
const info = await stat8(root).catch(() => null);
|
|
8312
8594
|
if (!info?.isDirectory()) continue;
|
|
8313
8595
|
const notes = await countNotes(root);
|
|
8314
8596
|
if (notes > 0) found.push({ path: root, note_count: notes });
|
|
@@ -8643,9 +8925,9 @@ function insideFolder(target, folder) {
|
|
|
8643
8925
|
realpathSync2(existing);
|
|
8644
8926
|
break;
|
|
8645
8927
|
} catch {
|
|
8646
|
-
const parent =
|
|
8928
|
+
const parent = dirname10(existing);
|
|
8647
8929
|
if (parent === existing) break;
|
|
8648
|
-
rest.unshift(
|
|
8930
|
+
rest.unshift(basename4(existing));
|
|
8649
8931
|
existing = parent;
|
|
8650
8932
|
}
|
|
8651
8933
|
}
|
|
@@ -8692,14 +8974,14 @@ var init_setup_cloud = __esm({
|
|
|
8692
8974
|
|
|
8693
8975
|
// src/commands/setup.ts
|
|
8694
8976
|
import { createHash as createHash9 } from "crypto";
|
|
8695
|
-
import { access as access2, mkdir as
|
|
8696
|
-
import { dirname as
|
|
8977
|
+
import { access as access2, mkdir as mkdir17, readFile as readFile27 } from "fs/promises";
|
|
8978
|
+
import { dirname as dirname11, join as join29 } from "path";
|
|
8697
8979
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8698
|
-
import { Command as
|
|
8980
|
+
import { Command as Command38 } from "commander";
|
|
8699
8981
|
import prompts5 from "prompts";
|
|
8700
8982
|
import { homedir as homedir6 } from "os";
|
|
8701
8983
|
function setupCommand() {
|
|
8702
|
-
return new
|
|
8984
|
+
return new Command38("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(
|
|
8703
8985
|
"--wait",
|
|
8704
8986
|
"with --web: block until the screen is finished, even with nobody watching the terminal"
|
|
8705
8987
|
).option(
|
|
@@ -9060,7 +9342,7 @@ async function applySetup(choice, dryRun) {
|
|
|
9060
9342
|
}
|
|
9061
9343
|
async function write2() {
|
|
9062
9344
|
for (const tool of choice.tools) {
|
|
9063
|
-
const hubPath =
|
|
9345
|
+
const hubPath = join29(agentHome(), tool.hubFile);
|
|
9064
9346
|
const written = await injectBlock(
|
|
9065
9347
|
hubPath,
|
|
9066
9348
|
hub + tailFor(tool, choice.hook),
|
|
@@ -9075,8 +9357,8 @@ async function applySetup(choice, dryRun) {
|
|
|
9075
9357
|
});
|
|
9076
9358
|
if (!dryRun && written.after !== null) mark(hubPath, written.after);
|
|
9077
9359
|
for (const skill of BRAND.subSkills) {
|
|
9078
|
-
const content = await loadAsset(
|
|
9079
|
-
const path =
|
|
9360
|
+
const content = await loadAsset(join29(skill, "SKILL.md"));
|
|
9361
|
+
const path = join29(agentHome(), tool.skillsDir, skill, "SKILL.md");
|
|
9080
9362
|
const out = await writeIfDifferent(path, content, dryRun);
|
|
9081
9363
|
results.push({
|
|
9082
9364
|
tool: tool.label,
|
|
@@ -9092,7 +9374,14 @@ async function applySetup(choice, dryRun) {
|
|
|
9092
9374
|
results.push({
|
|
9093
9375
|
tool: tool.label,
|
|
9094
9376
|
what: hook.what,
|
|
9095
|
-
|
|
9377
|
+
// Under the lock every writer of these files takes, so a session
|
|
9378
|
+
// starting meanwhile cannot refresh the same file under this write.
|
|
9379
|
+
// A lock nobody gives up within five seconds is a crashed holder or
|
|
9380
|
+
// a stuck one; the write goes ahead rather than leaving setup hung.
|
|
9381
|
+
result: await (async () => {
|
|
9382
|
+
const write3 = () => choice.hook ? installHook(hook, tool, dryRun) : removeHook(hook, tool, dryRun);
|
|
9383
|
+
return await withHookFileLock(write3, 5e3) ?? write3();
|
|
9384
|
+
})()
|
|
9096
9385
|
});
|
|
9097
9386
|
}
|
|
9098
9387
|
}
|
|
@@ -9159,7 +9448,7 @@ async function hooksArmed(tools) {
|
|
|
9159
9448
|
const capable = tools.filter((t) => t.hooks && t.hooksFile);
|
|
9160
9449
|
if (capable.length === 0) return void 0;
|
|
9161
9450
|
for (const tool of capable) {
|
|
9162
|
-
const raw = await
|
|
9451
|
+
const raw = await readFile27(join29(agentHome(), ...(tool.hooksFile ?? "").split("/")), "utf8").catch(
|
|
9163
9452
|
() => ""
|
|
9164
9453
|
);
|
|
9165
9454
|
if (hooksFor([tool]).some((spec) => raw.includes(spec.signature))) return "on";
|
|
@@ -9199,7 +9488,7 @@ async function detectTools() {
|
|
|
9199
9488
|
const home = agentHome();
|
|
9200
9489
|
const found = [];
|
|
9201
9490
|
for (const tool of TOOLS) {
|
|
9202
|
-
if (await exists2(
|
|
9491
|
+
if (await exists2(join29(home, tool.dir))) found.push(tool);
|
|
9203
9492
|
}
|
|
9204
9493
|
return found;
|
|
9205
9494
|
}
|
|
@@ -9299,10 +9588,10 @@ async function injectBlock(filePath, content, dryRun, weWroteThis) {
|
|
|
9299
9588
|
const block = `${BRAND.hubMarkerStart}
|
|
9300
9589
|
${content.trimEnd()}
|
|
9301
9590
|
${BRAND.hubMarkerEnd}`;
|
|
9302
|
-
const existing = await
|
|
9591
|
+
const existing = await readFile27(filePath, "utf8").catch(() => null);
|
|
9303
9592
|
if (existing === null) {
|
|
9304
9593
|
if (!dryRun) {
|
|
9305
|
-
await
|
|
9594
|
+
await mkdir17(dirname11(filePath), { recursive: true });
|
|
9306
9595
|
await writeThenRename(filePath, `${block}
|
|
9307
9596
|
`);
|
|
9308
9597
|
}
|
|
@@ -9342,23 +9631,56 @@ function looksLikeOurGuide(text) {
|
|
|
9342
9631
|
return text.includes("memgineering recall") && text.includes("memgineering remember");
|
|
9343
9632
|
}
|
|
9344
9633
|
async function writeIfDifferent(filePath, content, dryRun) {
|
|
9345
|
-
const existing = await
|
|
9634
|
+
const existing = await readFile27(filePath, "utf8").catch(() => null);
|
|
9346
9635
|
const after = fingerprint(content);
|
|
9347
9636
|
if (existing === content) return { outcome: "unchanged", before: existing, after };
|
|
9348
9637
|
if (!dryRun) {
|
|
9349
|
-
await
|
|
9638
|
+
await mkdir17(dirname11(filePath), { recursive: true });
|
|
9350
9639
|
await writeThenRename(filePath, content);
|
|
9351
9640
|
}
|
|
9352
9641
|
return { outcome: existing === null ? "injected" : "updated", before: existing, after };
|
|
9353
9642
|
}
|
|
9643
|
+
function guardCommandFor(platform2) {
|
|
9644
|
+
return platform2 === "win32" ? "memgineering guard || true" : guardHookCommand();
|
|
9645
|
+
}
|
|
9646
|
+
async function refreshOwnHookCommands() {
|
|
9647
|
+
const moves = HOOKS.filter((h) => h.event !== "Stop").map((h) => ({
|
|
9648
|
+
event: h.event,
|
|
9649
|
+
from: SHIPPED_HOOK_COMMANDS[h.event],
|
|
9650
|
+
to: h.command
|
|
9651
|
+
}));
|
|
9652
|
+
const changed = [];
|
|
9653
|
+
const skipped = [];
|
|
9654
|
+
for (const tool of TOOLS.filter((t) => t.hooks && t.hooksFile && DEFAULT_HOOK_TOOLS.includes(t.id))) {
|
|
9655
|
+
const path = join29(agentHome(), ...(tool.hooksFile ?? "").split("/"));
|
|
9656
|
+
const outcome = await refreshHookFile(path, moves);
|
|
9657
|
+
if (outcome === "changed") changed.push(path);
|
|
9658
|
+
else if (outcome === "symlink" || outcome === "refused") skipped.push({ path, why: outcome });
|
|
9659
|
+
}
|
|
9660
|
+
return { changed, skipped };
|
|
9661
|
+
}
|
|
9662
|
+
async function refreshOwnHooksAndTell(say) {
|
|
9663
|
+
try {
|
|
9664
|
+
const report3 = await refreshOwnHookCommands();
|
|
9665
|
+
if (report3.changed.length === 0 && report3.skipped.length === 0) return;
|
|
9666
|
+
const cfg = await loadConfig();
|
|
9667
|
+
const told = cfg.system.hookNoticesTold ?? [];
|
|
9668
|
+
const { text, told: nowTold } = refreshNotice(report3, told);
|
|
9669
|
+
if (nowTold.length !== told.length) {
|
|
9670
|
+
await saveConfig({ ...cfg, system: { ...cfg.system, hookNoticesTold: nowTold } });
|
|
9671
|
+
}
|
|
9672
|
+
if (text !== null) say(text);
|
|
9673
|
+
} catch {
|
|
9674
|
+
}
|
|
9675
|
+
}
|
|
9354
9676
|
function hooksFor(tools) {
|
|
9355
9677
|
return HOOKS.filter(
|
|
9356
9678
|
(h) => tools.some((t) => t.hooks && (h.tools ?? DEFAULT_HOOK_TOOLS).includes(t.id))
|
|
9357
9679
|
);
|
|
9358
9680
|
}
|
|
9359
9681
|
async function installHook(spec, tool, dryRun) {
|
|
9360
|
-
const settingsPath =
|
|
9361
|
-
const raw = await
|
|
9682
|
+
const settingsPath = join29(agentHome(), ...(tool.hooksFile ?? "").split("/"));
|
|
9683
|
+
const raw = await readFile27(settingsPath, "utf8").catch(() => null);
|
|
9362
9684
|
let settings = {};
|
|
9363
9685
|
if (raw !== null) {
|
|
9364
9686
|
try {
|
|
@@ -9400,15 +9722,15 @@ async function installHook(spec, tool, dryRun) {
|
|
|
9400
9722
|
}
|
|
9401
9723
|
const next = { ...settings, hooks: { ...hooks, [spec.event]: existingGroups } };
|
|
9402
9724
|
if (!dryRun) {
|
|
9403
|
-
await
|
|
9725
|
+
await mkdir17(dirname11(settingsPath), { recursive: true });
|
|
9404
9726
|
await writeThenRename(settingsPath, `${JSON.stringify(next, null, 2)}
|
|
9405
9727
|
`);
|
|
9406
9728
|
}
|
|
9407
9729
|
return raw === null ? "injected" : "updated";
|
|
9408
9730
|
}
|
|
9409
9731
|
async function removeHook(spec, tool, dryRun) {
|
|
9410
|
-
const settingsPath =
|
|
9411
|
-
const raw = await
|
|
9732
|
+
const settingsPath = join29(agentHome(), ...(tool.hooksFile ?? "").split("/"));
|
|
9733
|
+
const raw = await readFile27(settingsPath, "utf8").catch(() => null);
|
|
9412
9734
|
if (raw === null) return "unchanged";
|
|
9413
9735
|
let parsed;
|
|
9414
9736
|
try {
|
|
@@ -9432,7 +9754,7 @@ async function removeHook(spec, tool, dryRun) {
|
|
|
9432
9754
|
return "removed";
|
|
9433
9755
|
}
|
|
9434
9756
|
async function loadAsset(relPath) {
|
|
9435
|
-
const raw = await
|
|
9757
|
+
const raw = await readFile27(join29(await assetRoot(), relPath), "utf8");
|
|
9436
9758
|
if (relPath === BRAND.hubFileName && raw.startsWith("---")) {
|
|
9437
9759
|
const end = raw.indexOf("\n---", 4);
|
|
9438
9760
|
if (end !== -1) return raw.slice(end + 4).trimStart();
|
|
@@ -9440,14 +9762,14 @@ async function loadAsset(relPath) {
|
|
|
9440
9762
|
return raw;
|
|
9441
9763
|
}
|
|
9442
9764
|
async function assetRoot() {
|
|
9443
|
-
const here =
|
|
9444
|
-
const bundled =
|
|
9445
|
-
if (await exists2(
|
|
9765
|
+
const here = dirname11(fileURLToPath3(import.meta.url));
|
|
9766
|
+
const bundled = join29(here, "..", "assets");
|
|
9767
|
+
if (await exists2(join29(bundled, BRAND.hubFileName))) return bundled;
|
|
9446
9768
|
let dir = here;
|
|
9447
9769
|
for (; ; ) {
|
|
9448
|
-
const candidate =
|
|
9449
|
-
if (await exists2(
|
|
9450
|
-
const parent =
|
|
9770
|
+
const candidate = join29(dir, "packages", "skills");
|
|
9771
|
+
if (await exists2(join29(candidate, BRAND.hubFileName))) return candidate;
|
|
9772
|
+
const parent = dirname11(dir);
|
|
9451
9773
|
if (parent === dir) break;
|
|
9452
9774
|
dir = parent;
|
|
9453
9775
|
}
|
|
@@ -9465,13 +9787,15 @@ async function exists2(path) {
|
|
|
9465
9787
|
return false;
|
|
9466
9788
|
}
|
|
9467
9789
|
}
|
|
9468
|
-
var TOOLS, HOSTED_CONNECTOR_URL, SUPPORTED_LIST, REFUSAL_HINT, DUPLICATE_HINT, HOOK_TIMEOUT_S, DEFAULT_HOOK_TOOLS, HOOK_WHAT, HOOKS;
|
|
9790
|
+
var TOOLS, HOSTED_CONNECTOR_URL, SUPPORTED_LIST, REFUSAL_HINT, DUPLICATE_HINT, HOOK_TIMEOUT_S, DEFAULT_HOOK_TOOLS, HOOK_WHAT, HOOKS, SHIPPED_HOOK_COMMANDS;
|
|
9469
9791
|
var init_setup = __esm({
|
|
9470
9792
|
"src/commands/setup.ts"() {
|
|
9471
9793
|
"use strict";
|
|
9472
9794
|
init_atomic();
|
|
9473
9795
|
init_brand();
|
|
9474
9796
|
init_deadline();
|
|
9797
|
+
init_guard_shell();
|
|
9798
|
+
init_hook_refresh();
|
|
9475
9799
|
init_errors();
|
|
9476
9800
|
init_setup_brain();
|
|
9477
9801
|
init_setup_signin();
|
|
@@ -9599,7 +9923,7 @@ For a guide: put ${BRAND.hubMarkerStart} and ${BRAND.hubMarkerEnd} back around t
|
|
|
9599
9923
|
// here would mean a session that reads and answers never hears a rule.
|
|
9600
9924
|
codex: "apply_patch|Bash"
|
|
9601
9925
|
},
|
|
9602
|
-
command:
|
|
9926
|
+
command: guardCommandFor(process.platform),
|
|
9603
9927
|
signature: "memgineering guard"
|
|
9604
9928
|
},
|
|
9605
9929
|
{
|
|
@@ -9625,6 +9949,13 @@ For a guide: put ${BRAND.hubMarkerStart} and ${BRAND.hubMarkerEnd} back around t
|
|
|
9625
9949
|
tools: ["grok"]
|
|
9626
9950
|
}
|
|
9627
9951
|
];
|
|
9952
|
+
SHIPPED_HOOK_COMMANDS = {
|
|
9953
|
+
SessionStart: [
|
|
9954
|
+
"memgineering resurface --limit 5 || true",
|
|
9955
|
+
`memgineering resurface --limit 5 --deadline ${HOOK_DEADLINE_MS} || true`
|
|
9956
|
+
],
|
|
9957
|
+
PreToolUse: ["memgineering guard || true"]
|
|
9958
|
+
};
|
|
9628
9959
|
}
|
|
9629
9960
|
});
|
|
9630
9961
|
|
|
@@ -9956,6 +10287,21 @@ var ReportReasonSchema = z10.enum(["spam", "harmful", "incorrect", "copyright",
|
|
|
9956
10287
|
|
|
9957
10288
|
// ../../packages/contracts/src/errors.ts
|
|
9958
10289
|
import { z as z11 } from "zod";
|
|
10290
|
+
|
|
10291
|
+
// ../../packages/contracts/src/refresh-race.ts
|
|
10292
|
+
var REFRESH_RACE_FLAG = "refresh_rotated_by_another_request";
|
|
10293
|
+
function isRefreshRace(body) {
|
|
10294
|
+
if (typeof body !== "object" || body === null) return false;
|
|
10295
|
+
const error = body.error;
|
|
10296
|
+
if (typeof error !== "object" || error === null) return false;
|
|
10297
|
+
const { code, flags } = error;
|
|
10298
|
+
if (code !== "not_authenticated" || !Array.isArray(flags)) return false;
|
|
10299
|
+
return flags.some(
|
|
10300
|
+
(flag2) => typeof flag2 === "object" && flag2 !== null && flag2.code === REFRESH_RACE_FLAG
|
|
10301
|
+
);
|
|
10302
|
+
}
|
|
10303
|
+
|
|
10304
|
+
// ../../packages/contracts/src/errors.ts
|
|
9959
10305
|
var ErrorCodeSchema = z11.enum([
|
|
9960
10306
|
"generic_failure",
|
|
9961
10307
|
// 1
|
|
@@ -10905,16 +11251,33 @@ var PublishPackageRequestSchema = z19.object({
|
|
|
10905
11251
|
*/
|
|
10906
11252
|
content: z19.string().min(50).max(1e5),
|
|
10907
11253
|
/**
|
|
10908
|
-
*
|
|
10909
|
-
*
|
|
11254
|
+
* Optional, and refused when it disagrees. core-api stores the title,
|
|
11255
|
+
* description and type it reads from the frontmatter of `content`, which must
|
|
11256
|
+
* carry all three (the title may come from `name:`). A value here that
|
|
11257
|
+
* differs from the frontmatter's, compared trimmed, is refused as
|
|
11258
|
+
* `usage_error` (HTTP 400) with a hint naming the frontmatter key; absent
|
|
11259
|
+
* means no claim. A `type` the registry cannot publish at all (`scoped-rule`,
|
|
11260
|
+
* `mdc-rule`, `plugin`) names no key to write: its hint says the type cannot
|
|
11261
|
+
* be published yet and to drop `--type`. `kordis push` ≤0.15.2 sends
|
|
11262
|
+
* `--type` here, and writes `type: skill` into the frontmatter of a SKILL.md
|
|
11263
|
+
* or *.skill.md file, a folder or a .skill bundle that names no type — so
|
|
11264
|
+
* `--type hook` on one is refused, where until 2026-10 it published a skill.
|
|
11265
|
+
* Later versions send no `type`: they write `--type` into the frontmatter.
|
|
10910
11266
|
*/
|
|
10911
11267
|
title: z19.string().min(1).max(200).optional(),
|
|
10912
11268
|
description: z19.string().min(1).max(2048).optional(),
|
|
10913
11269
|
type: HarnessTypeSchema.optional(),
|
|
11270
|
+
/**
|
|
11271
|
+
* A new package is published under this slug instead of the one derived from
|
|
11272
|
+
* the frontmatter title. An update keeps the slug of the package it updates:
|
|
11273
|
+
* a slug here that differs from the target's is refused (`usage_error`).
|
|
11274
|
+
*/
|
|
10914
11275
|
slug: SlugSchema.optional(),
|
|
10915
11276
|
/**
|
|
10916
|
-
* When updating, the target full_slug
|
|
10917
|
-
*
|
|
11277
|
+
* When updating, the target full_slug; its presence is what makes a publish
|
|
11278
|
+
* an update (`PUT /api/packages/:u/:s` sets it from the path). Omitted for a
|
|
11279
|
+
* new package, whose slug is `slug` or else derived from the frontmatter
|
|
11280
|
+
* title — never derived on an update.
|
|
10918
11281
|
*/
|
|
10919
11282
|
target_full_slug: FullSlugSchema.optional(),
|
|
10920
11283
|
/**
|
|
@@ -10922,6 +11285,12 @@ var PublishPackageRequestSchema = z19.object({
|
|
|
10922
11285
|
*/
|
|
10923
11286
|
bump: z19.enum(["patch", "minor", "major"]).optional(),
|
|
10924
11287
|
changelog: z19.string().max(2e3).optional(),
|
|
11288
|
+
/**
|
|
11289
|
+
* Refused when non-empty (`usage_error`): a package's dependencies are read
|
|
11290
|
+
* from its frontmatter's `depends:` and `recommends:` lists only. Kept in the
|
|
11291
|
+
* schema so the server sees them to refuse them — this object is non-strict,
|
|
11292
|
+
* and an unlisted key would be stripped without a word.
|
|
11293
|
+
*/
|
|
10925
11294
|
depends: z19.array(z19.string()).optional(),
|
|
10926
11295
|
recommends: z19.array(z19.string()).optional(),
|
|
10927
11296
|
/**
|
|
@@ -11395,6 +11764,120 @@ var AdminTelemetrySummaryResponseSchema = z22.object({
|
|
|
11395
11764
|
daily: z22.array(TelemetryDailyBucketSchema)
|
|
11396
11765
|
});
|
|
11397
11766
|
|
|
11767
|
+
// ../../packages/contracts/src/review-patterns.ts
|
|
11768
|
+
var STAGE_1_REJECT_PATTERNS = [
|
|
11769
|
+
{
|
|
11770
|
+
code: "curl_pipe_shell",
|
|
11771
|
+
pattern: /\bcurl\s+[^|;]+\s*\|\s*(?:(?:sudo|doas)\s+)?(?:-E\s+|[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:sh|bash|zsh|ksh|dash|csh|tcsh|fish)\b/i,
|
|
11772
|
+
hint: "Replace `curl \u2026 | bash` with a verifiable install path: download the script to a file, show the user `cat` of it for inspection, then `bash <file>` only after confirmation."
|
|
11773
|
+
},
|
|
11774
|
+
{
|
|
11775
|
+
code: "wget_pipe_shell",
|
|
11776
|
+
pattern: /\bwget\s+[^|;]+\s*\|\s*(?:(?:sudo|doas)\s+)?(?:-E\s+|[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:sh|bash|zsh|ksh|dash|csh|tcsh|fish)\b/i,
|
|
11777
|
+
hint: "Replace `wget \u2026 | bash` with a verifiable install path: download the script to a file, show the user `cat` of it for inspection, then `bash <file>` only after confirmation."
|
|
11778
|
+
},
|
|
11779
|
+
{
|
|
11780
|
+
code: "shell_process_substitution",
|
|
11781
|
+
pattern: /(?:bash|sh|zsh)\s*<\(\s*(?:curl|wget)\b/i,
|
|
11782
|
+
hint: "Replace `bash <(curl \u2026)` with a verifiable install path: download the script to a file, show the user `cat` of it for inspection, then `bash <file>` only after confirmation."
|
|
11783
|
+
},
|
|
11784
|
+
{
|
|
11785
|
+
code: "eval_atob",
|
|
11786
|
+
pattern: /\beval\s*\(\s*atob\b/i,
|
|
11787
|
+
hint: "Encoded eval (`eval(atob(...))`) is a hallmark of obfuscated payloads. Move the logic into plain code the reader can audit."
|
|
11788
|
+
},
|
|
11789
|
+
{
|
|
11790
|
+
code: "eval_buffer_from",
|
|
11791
|
+
pattern: /\beval\s*\(\s*Buffer\.from\b/i,
|
|
11792
|
+
hint: "Encoded eval (`eval(Buffer.from(...))`) is a hallmark of obfuscated payloads. Move the logic into plain code the reader can audit."
|
|
11793
|
+
},
|
|
11794
|
+
{
|
|
11795
|
+
code: "aws_access_key",
|
|
11796
|
+
pattern: /\b(?:AKIA|ASIA|AGPA)[0-9A-Z]{16}\b/,
|
|
11797
|
+
hint: "Remove the AWS access key from the body. Reference credentials via env var (`AWS_ACCESS_KEY_ID`); rotate the leaked key immediately if it was real."
|
|
11798
|
+
},
|
|
11799
|
+
{
|
|
11800
|
+
code: "private_key_block",
|
|
11801
|
+
pattern: /-----BEGIN\s+(?:[A-Z0-9]+\s+)?PRIVATE\s+KEY(?:\s+BLOCK)?-----/,
|
|
11802
|
+
hint: "Remove the private key block from the body. Reference keys via env var or a secret manager; rotate the leaked key immediately if it was real."
|
|
11803
|
+
},
|
|
11804
|
+
{
|
|
11805
|
+
code: "google_api_key",
|
|
11806
|
+
pattern: /\bAIza[0-9A-Za-z_-]{35}\b/,
|
|
11807
|
+
hint: "Remove the Google API key from the body. Reference credentials via env var; rotate the leaked key immediately if it was real."
|
|
11808
|
+
},
|
|
11809
|
+
{
|
|
11810
|
+
code: "slack_token",
|
|
11811
|
+
pattern: /\b(?:xox[abpors]|xapp|xoxe)-[0-9a-zA-Z-]{10,}\b/,
|
|
11812
|
+
hint: "Remove the Slack token from the body. Reference tokens via env var; revoke the leaked token immediately if it was real."
|
|
11813
|
+
},
|
|
11814
|
+
{
|
|
11815
|
+
code: "github_token",
|
|
11816
|
+
pattern: /\b(?:gh[pousr]_[A-Za-z0-9_]{36,}|github_pat_[A-Za-z0-9_]{60,})\b/,
|
|
11817
|
+
hint: "Remove the GitHub token from the body. Reference tokens via env var; revoke the leaked token immediately if it was real."
|
|
11818
|
+
}
|
|
11819
|
+
];
|
|
11820
|
+
var STAGE_1_5_AUP_PATTERNS = [
|
|
11821
|
+
{
|
|
11822
|
+
// Stolen-card / bank-credential trafficking storefronts. Anchored to the
|
|
11823
|
+
// trafficking jargon that only appears in carding markets, not in the
|
|
11824
|
+
// defensive "how to detect carding" literature. `dumps` is also an
|
|
11825
|
+
// ordinary developer verb — "this hook dumps vendor files", "dumps store
|
|
11826
|
+
// state", "dumps market data to CSV" all matched the shared noun list —
|
|
11827
|
+
// so after `dumps` only sale framing (`shop`, `for sale`) counts, while
|
|
11828
|
+
// `cvv` and `fullz` keep the full list. Narrowed when comments started
|
|
11829
|
+
// running this table (2026-10-14).
|
|
11830
|
+
code: "aup_stolen_financial_data_market",
|
|
11831
|
+
pattern: /\b(?:cvv|fullz)\s+(?:shop|store|for\s+sale|market|vendor)\b|\bdumps\s+(?:shop|for\s+sale)\b|\bdumps?\s+with\s+pin\b|\bsell(?:ing)?\s+(?:cvv|fullz|dumps)\b/i,
|
|
11832
|
+
hint: "This reads as a storefront for trafficking stolen financial data, which violates the Kordis AUP. If the package is about DETECTING or DEFENDING against card fraud, reword it so it does not read as an offer to buy/sell stolen data."
|
|
11833
|
+
},
|
|
11834
|
+
{
|
|
11835
|
+
// DDoS-for-hire (booter/stresser) services sold on a subscription.
|
|
11836
|
+
code: "aup_ddos_for_hire",
|
|
11837
|
+
pattern: /\b(?:booter|stresser)\s+(?:service|panel|subscription)\b|\b(?:ddos|dstat)\s+for\s+hire\b|\bhire\s+(?:a\s+)?(?:booter|stresser|ddos)\b/i,
|
|
11838
|
+
hint: "This reads as a DDoS-for-hire (booter/stresser) service, which violates the Kordis AUP. Load-testing tooling is fine \u2014 describe it as testing infrastructure you own/are authorized to test, not a for-hire attack service."
|
|
11839
|
+
},
|
|
11840
|
+
{
|
|
11841
|
+
// Malware/stealer/ransomware sold or distributed as a product ("builder
|
|
11842
|
+
// for sale", "buy our stealer"). A skill that DEFENDS against these, or a
|
|
11843
|
+
// malware-analysis skill, does not use buy/sell framing.
|
|
11844
|
+
code: "aup_malware_sale",
|
|
11845
|
+
pattern: /\b(?:ransomware|stealer|infostealer|rat|keylogger|botnet)\s+(?:builder|kit|panel|source)\s+(?:for\s+sale|for\s+rent)\b|\b(?:buy|sell(?:ing)?|rent)\s+(?:our\s+)?(?:ransomware|stealer|infostealer|botnet)\b/i,
|
|
11846
|
+
hint: "This reads as an offer to sell/rent malware, which violates the Kordis AUP. Malware ANALYSIS and DEFENSE content is welcome \u2014 reword so it does not read as distributing a weaponized build."
|
|
11847
|
+
},
|
|
11848
|
+
{
|
|
11849
|
+
// Child sexual abuse material. Unlike the rest of this table this entry is
|
|
11850
|
+
// NOT anchored to solicitation: it matches the spelled-out phrases ("child
|
|
11851
|
+
// porn", "child pornography", "child sexual abuse material") wherever they
|
|
11852
|
+
// appear, child-safety and moderation education included. Only the
|
|
11853
|
+
// acronym "CSAM" on its own does not match ("how platforms detect CSAM
|
|
11854
|
+
// for NCMEC reporting" passes). Kept this broad on purpose (2026-10-14):
|
|
11855
|
+
// comments have no Stage-2 review after this table, so a miss costs more
|
|
11856
|
+
// than a false positive.
|
|
11857
|
+
code: "aup_csam",
|
|
11858
|
+
pattern: /\bchild\s+(?:porn(?:ography)?|sexual\s+abuse\s+material)\b/i,
|
|
11859
|
+
hint: "This reads as solicitation or distribution of child sexual abuse material, which is illegal and violates the Kordis AUP. This is a terminal, non-appealable rejection."
|
|
11860
|
+
},
|
|
11861
|
+
{
|
|
11862
|
+
// Drug / untraceable-weapon brokering with explicit no-id / untraceable /
|
|
11863
|
+
// dark-web fulfillment language — not a bare mention of a substance or
|
|
11864
|
+
// firearm topic (e.g. harm-reduction or policy content does not use this
|
|
11865
|
+
// buy/sell + untraceable-fulfillment framing together).
|
|
11866
|
+
code: "aup_drug_weapon_brokering",
|
|
11867
|
+
pattern: /\b(?:buy|sell(?:ing)?)\b[\s\S]{0,40}\b(?:cocaine|heroin|fentanyl|methamphetamine|untraceable firearms?|untraceable guns?)\b[\s\S]{0,40}\b(?:no id|untraceable|worldwide shipping|dark ?web)\b/i,
|
|
11868
|
+
hint: "This reads as brokering illegal drugs or untraceable weapons for sale, which violates the Kordis AUP. If the package is about DETECTING or REPORTING such activity, reword it so it does not read as an offer to buy/sell."
|
|
11869
|
+
},
|
|
11870
|
+
{
|
|
11871
|
+
// A directive to dox or swat a specific, named/tagged target — not a
|
|
11872
|
+
// bare mention of doxxing/swatting as a topic (e.g. "how to protect
|
|
11873
|
+
// yourself from doxxing" does not match).
|
|
11874
|
+
code: "aup_targeted_harassment_directive",
|
|
11875
|
+
pattern: /\b(?:dox|swat)\s+(?:this (?:guy|girl|person|user)|him|her|@[\w.]+)\b/i,
|
|
11876
|
+
hint: "This reads as a directive to dox or swat a specific target, which violates the Kordis AUP. Remove the targeted-harassment instruction."
|
|
11877
|
+
}
|
|
11878
|
+
];
|
|
11879
|
+
var DETERMINISTIC_REJECT_PATTERNS = [...STAGE_1_REJECT_PATTERNS, ...STAGE_1_5_AUP_PATTERNS];
|
|
11880
|
+
|
|
11398
11881
|
// ../../packages/api-client/src/errors.ts
|
|
11399
11882
|
function httpStatusToCode(status) {
|
|
11400
11883
|
if (status === 401) return "not_authenticated";
|
|
@@ -11931,7 +12414,7 @@ Wait ${err.retryAfter}s before trying again.` : "";
|
|
|
11931
12414
|
|
|
11932
12415
|
// src/commands/base-write.ts
|
|
11933
12416
|
init_errors();
|
|
11934
|
-
import { readFile as
|
|
12417
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
11935
12418
|
import { resolve as resolve4 } from "path";
|
|
11936
12419
|
import { Command as Command2 } from "commander";
|
|
11937
12420
|
|
|
@@ -11941,8 +12424,8 @@ init_brand();
|
|
|
11941
12424
|
init_errors();
|
|
11942
12425
|
init_ui();
|
|
11943
12426
|
import { createHash as createHash3 } from "crypto";
|
|
11944
|
-
import { appendFile, mkdir as mkdir6, readFile as
|
|
11945
|
-
import { join as
|
|
12427
|
+
import { appendFile, mkdir as mkdir6, readFile as readFile7, readdir as readdir3, rm as rm4, writeFile as writeFile5 } from "fs/promises";
|
|
12428
|
+
import { join as join9 } from "path";
|
|
11946
12429
|
import { z as z23 } from "zod";
|
|
11947
12430
|
|
|
11948
12431
|
// src/lib/ids.ts
|
|
@@ -12165,20 +12648,20 @@ function sanitizeRationale(raw) {
|
|
|
12165
12648
|
return capped2;
|
|
12166
12649
|
}
|
|
12167
12650
|
function ledgerDir(vaultRoot) {
|
|
12168
|
-
return
|
|
12651
|
+
return join9(vaultRoot, BRAND.vaultLedgerDir, "ledger");
|
|
12169
12652
|
}
|
|
12170
12653
|
function ledgerFile(vaultRoot, ts) {
|
|
12171
|
-
return
|
|
12654
|
+
return join9(ledgerDir(vaultRoot), `ops-${ts.slice(0, 7)}.jsonl`);
|
|
12172
12655
|
}
|
|
12173
12656
|
function hashContent(content) {
|
|
12174
12657
|
return createHash3("sha256").update(content, "utf8").digest("hex");
|
|
12175
12658
|
}
|
|
12176
12659
|
function stateDirFor(vaultRoot) {
|
|
12177
12660
|
const hash = createHash3("sha256").update(vaultRoot).digest("hex").slice(0, 16);
|
|
12178
|
-
return
|
|
12661
|
+
return join9(brandHome(), "state", hash);
|
|
12179
12662
|
}
|
|
12180
12663
|
function snapshotDir(vaultRoot) {
|
|
12181
|
-
return
|
|
12664
|
+
return join9(stateDirFor(vaultRoot), "undo");
|
|
12182
12665
|
}
|
|
12183
12666
|
async function appendOp(vaultRoot, op) {
|
|
12184
12667
|
const ts = (op.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
@@ -12200,7 +12683,7 @@ async function appendOp(vaultRoot, op) {
|
|
|
12200
12683
|
if (op.snapshotBefore != null) {
|
|
12201
12684
|
const dir = snapshotDir(vaultRoot);
|
|
12202
12685
|
await mkdir6(dir, { recursive: true });
|
|
12203
|
-
await writeFile5(
|
|
12686
|
+
await writeFile5(join9(dir, `${record.op_id}.txt`), op.snapshotBefore, {
|
|
12204
12687
|
encoding: "utf8",
|
|
12205
12688
|
mode: 384
|
|
12206
12689
|
});
|
|
@@ -12212,7 +12695,7 @@ async function appendOp(vaultRoot, op) {
|
|
|
12212
12695
|
}
|
|
12213
12696
|
async function readSnapshot(vaultRoot, opId) {
|
|
12214
12697
|
try {
|
|
12215
|
-
return await
|
|
12698
|
+
return await readFile7(join9(snapshotDir(vaultRoot), `${opId}.txt`), "utf8");
|
|
12216
12699
|
} catch {
|
|
12217
12700
|
return null;
|
|
12218
12701
|
}
|
|
@@ -12220,14 +12703,14 @@ async function readSnapshot(vaultRoot, opId) {
|
|
|
12220
12703
|
async function readOps(vaultRoot) {
|
|
12221
12704
|
let files2;
|
|
12222
12705
|
try {
|
|
12223
|
-
files2 = (await
|
|
12706
|
+
files2 = (await readdir3(ledgerDir(vaultRoot))).filter((f) => /^ops-\d{4}-\d{2}\.jsonl$/.test(f)).sort();
|
|
12224
12707
|
} catch {
|
|
12225
12708
|
return { ops: [], skipped: 0 };
|
|
12226
12709
|
}
|
|
12227
12710
|
const ops = [];
|
|
12228
12711
|
let skipped = 0;
|
|
12229
12712
|
for (const file of files2) {
|
|
12230
|
-
const raw = await
|
|
12713
|
+
const raw = await readFile7(join9(ledgerDir(vaultRoot), file), "utf8");
|
|
12231
12714
|
for (const line of raw.split("\n")) {
|
|
12232
12715
|
if (line.trim() === "") continue;
|
|
12233
12716
|
try {
|
|
@@ -12523,7 +13006,7 @@ async function readBody(source) {
|
|
|
12523
13006
|
process.stdin.on("error", reject);
|
|
12524
13007
|
});
|
|
12525
13008
|
}
|
|
12526
|
-
return
|
|
13009
|
+
return readFile11(source, "utf8").catch(() => source);
|
|
12527
13010
|
}
|
|
12528
13011
|
|
|
12529
13012
|
// src/commands/brain-admin.ts
|
|
@@ -12536,8 +13019,8 @@ init_errors();
|
|
|
12536
13019
|
init_cloud_brain();
|
|
12537
13020
|
init_config();
|
|
12538
13021
|
init_held_back();
|
|
12539
|
-
import { readFile as
|
|
12540
|
-
import { join as
|
|
13022
|
+
import { readFile as readFile12, rm as rm5 } from "fs/promises";
|
|
13023
|
+
import { join as join12 } from "path";
|
|
12541
13024
|
import { Command as Command3 } from "commander";
|
|
12542
13025
|
init_vault();
|
|
12543
13026
|
init_ui();
|
|
@@ -12755,7 +13238,7 @@ function syncRulesCommand() {
|
|
|
12755
13238
|
return new Command3("sync-rules").description("copy this brain\u2019s read/index exclusions into a file that follows you").option("--vault <path>", "which brain").action(async (opts) => {
|
|
12756
13239
|
const { brain } = await resolveBrain(opts.vault);
|
|
12757
13240
|
const root = brain.root;
|
|
12758
|
-
const existingRaw = await
|
|
13241
|
+
const existingRaw = await readFile12(join12(root, RULES_FILE), "utf8").catch(() => void 0);
|
|
12759
13242
|
const current = await readRulesFile(root);
|
|
12760
13243
|
const [dotDeny, dotIgnore] = await Promise.all([
|
|
12761
13244
|
readLines(root, ".memgdeny"),
|
|
@@ -12783,7 +13266,7 @@ function syncRulesCommand() {
|
|
|
12783
13266
|
});
|
|
12784
13267
|
return;
|
|
12785
13268
|
}
|
|
12786
|
-
await writeThenRename(
|
|
13269
|
+
await writeThenRename(join12(root, RULES_FILE), next);
|
|
12787
13270
|
const after = await readRulesFile(root);
|
|
12788
13271
|
const lost = [
|
|
12789
13272
|
...neverRead.filter((e) => !after.raw.never_read.includes(e)),
|
|
@@ -12816,7 +13299,7 @@ function syncRulesCommand() {
|
|
|
12816
13299
|
});
|
|
12817
13300
|
}
|
|
12818
13301
|
async function readLines(root, name) {
|
|
12819
|
-
const raw = await
|
|
13302
|
+
const raw = await readFile12(join12(root, name), "utf8").catch(() => "");
|
|
12820
13303
|
return raw.split("\n").map((l) => l.replace(/\r$/, "").trim()).filter((l) => l !== "" && !l.startsWith("#"));
|
|
12821
13304
|
}
|
|
12822
13305
|
async function stopReadingHosted(path) {
|
|
@@ -13026,11 +13509,11 @@ init_index_build();
|
|
|
13026
13509
|
|
|
13027
13510
|
// src/lib/lock.ts
|
|
13028
13511
|
init_errors();
|
|
13029
|
-
import { mkdir as mkdir7, readFile as
|
|
13030
|
-
import { join as
|
|
13512
|
+
import { mkdir as mkdir7, readFile as readFile13, rm as rm6, writeFile as writeFile6 } from "fs/promises";
|
|
13513
|
+
import { join as join13 } from "path";
|
|
13031
13514
|
var STALE_MS = 6e4;
|
|
13032
13515
|
function lockPath(vaultRoot) {
|
|
13033
|
-
return
|
|
13516
|
+
return join13(stateDirFor(vaultRoot), "write.lock");
|
|
13034
13517
|
}
|
|
13035
13518
|
function holderIsGone(pid) {
|
|
13036
13519
|
try {
|
|
@@ -13073,7 +13556,7 @@ async function withFileLock(path, dir, fn) {
|
|
|
13073
13556
|
}
|
|
13074
13557
|
async function readLock(path) {
|
|
13075
13558
|
try {
|
|
13076
|
-
const parsed = JSON.parse(await
|
|
13559
|
+
const parsed = JSON.parse(await readFile13(path, "utf8"));
|
|
13077
13560
|
if (typeof parsed.pid === "number" && typeof parsed.at === "string") {
|
|
13078
13561
|
return { pid: parsed.pid, at: parsed.at };
|
|
13079
13562
|
}
|
|
@@ -13092,15 +13575,15 @@ init_errors();
|
|
|
13092
13575
|
import { Command as Command5 } from "commander";
|
|
13093
13576
|
|
|
13094
13577
|
// src/lib/events.ts
|
|
13095
|
-
import { appendFile as appendFile2, mkdir as mkdir8, readFile as
|
|
13096
|
-
import { join as
|
|
13578
|
+
import { appendFile as appendFile2, mkdir as mkdir8, readFile as readFile15, readdir as readdir5 } from "fs/promises";
|
|
13579
|
+
import { join as join15 } from "path";
|
|
13097
13580
|
import { z as z24 } from "zod";
|
|
13098
13581
|
|
|
13099
13582
|
// src/lib/project.ts
|
|
13100
13583
|
init_config();
|
|
13101
13584
|
init_vault();
|
|
13102
|
-
import { readFile as
|
|
13103
|
-
import { basename, dirname as
|
|
13585
|
+
import { readFile as readFile14, stat as stat3 } from "fs/promises";
|
|
13586
|
+
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join14, resolve as resolve5 } from "path";
|
|
13104
13587
|
async function projectOf(cwd = process.cwd()) {
|
|
13105
13588
|
const here = await canonicalize(resolve5(cwd));
|
|
13106
13589
|
const repo = await findRepository(here);
|
|
@@ -13131,37 +13614,37 @@ async function boundName(key2) {
|
|
|
13131
13614
|
return cfg.projects.bindings.find((b) => resolve5(b.dir) === key2)?.name ?? null;
|
|
13132
13615
|
}
|
|
13133
13616
|
async function derivedName(repo) {
|
|
13134
|
-
const url = await originUrl(
|
|
13617
|
+
const url = await originUrl(join14(repo.commonDir, "config"));
|
|
13135
13618
|
const fromRemote = url === null ? null : ownerRepo(url);
|
|
13136
13619
|
if (fromRemote !== null) return { name: fromRemote, via: "remote" };
|
|
13137
|
-
const folder =
|
|
13620
|
+
const folder = basename2(repo.root);
|
|
13138
13621
|
return folder === "" ? { name: null, via: "none" } : { name: folder, via: "dirname" };
|
|
13139
13622
|
}
|
|
13140
13623
|
async function findRepository(from) {
|
|
13141
13624
|
let dir = from;
|
|
13142
13625
|
for (; ; ) {
|
|
13143
|
-
const found = await readGitLink(
|
|
13626
|
+
const found = await readGitLink(join14(dir, ".git"), dir);
|
|
13144
13627
|
if (found !== null) return found;
|
|
13145
|
-
const parent =
|
|
13628
|
+
const parent = dirname4(dir);
|
|
13146
13629
|
if (parent === dir) return null;
|
|
13147
13630
|
dir = parent;
|
|
13148
13631
|
}
|
|
13149
13632
|
}
|
|
13150
13633
|
async function readGitLink(dot, dir) {
|
|
13151
|
-
const info = await
|
|
13634
|
+
const info = await stat3(dot).catch(() => null);
|
|
13152
13635
|
if (info === null) return null;
|
|
13153
13636
|
if (info.isDirectory()) return { root: dir, commonDir: dot };
|
|
13154
|
-
const raw = await
|
|
13637
|
+
const raw = await readFile14(dot, "utf8").catch(() => null);
|
|
13155
13638
|
const pointed = raw === null ? null : /^gitdir:\s*(.+)$/m.exec(raw)?.[1]?.trim();
|
|
13156
13639
|
if (pointed === null || pointed === void 0 || pointed === "") return null;
|
|
13157
13640
|
const gitDir = isAbsolute2(pointed) ? resolve5(pointed) : resolve5(dir, pointed);
|
|
13158
|
-
const common = (await
|
|
13641
|
+
const common = (await readFile14(join14(gitDir, "commondir"), "utf8").catch(() => null))?.trim();
|
|
13159
13642
|
const commonDir = common === void 0 || common === "" ? gitDir : isAbsolute2(common) ? resolve5(common) : resolve5(gitDir, common);
|
|
13160
|
-
const root =
|
|
13643
|
+
const root = basename2(commonDir) === ".git" ? await canonicalize(dirname4(commonDir)) : dir;
|
|
13161
13644
|
return { root, commonDir };
|
|
13162
13645
|
}
|
|
13163
13646
|
async function originUrl(configPath2) {
|
|
13164
|
-
const text = await
|
|
13647
|
+
const text = await readFile14(configPath2, "utf8").catch(() => null);
|
|
13165
13648
|
if (text === null) return null;
|
|
13166
13649
|
let inOrigin = false;
|
|
13167
13650
|
let url = null;
|
|
@@ -13263,7 +13746,7 @@ var EventSchema = z24.object({
|
|
|
13263
13746
|
opened_id: z24.string().nullable().default(null)
|
|
13264
13747
|
});
|
|
13265
13748
|
function eventsFile(stateDir, ts) {
|
|
13266
|
-
return
|
|
13749
|
+
return join15(stateDir, `events-${ts.slice(0, 7)}.jsonl`);
|
|
13267
13750
|
}
|
|
13268
13751
|
function ledgerStateDir(target) {
|
|
13269
13752
|
return target.kind === "cloud" ? stateDirFor(`cloud:${target.brainId}`) : stateDirFor(target.brain.root);
|
|
@@ -13307,13 +13790,13 @@ async function readEvents(vaultRoot) {
|
|
|
13307
13790
|
async function readEventsAt(stateDir) {
|
|
13308
13791
|
let files2;
|
|
13309
13792
|
try {
|
|
13310
|
-
files2 = (await
|
|
13793
|
+
files2 = (await readdir5(stateDir)).filter((f) => /^events-\d{4}-\d{2}\.jsonl$/.test(f)).sort();
|
|
13311
13794
|
} catch {
|
|
13312
13795
|
return [];
|
|
13313
13796
|
}
|
|
13314
13797
|
const events = [];
|
|
13315
13798
|
for (const file of files2) {
|
|
13316
|
-
const raw = await
|
|
13799
|
+
const raw = await readFile15(join15(stateDir, file), "utf8");
|
|
13317
13800
|
for (const line of raw.split("\n")) {
|
|
13318
13801
|
if (line.trim() === "") continue;
|
|
13319
13802
|
try {
|
|
@@ -13761,8 +14244,8 @@ init_exclude();
|
|
|
13761
14244
|
init_adapter();
|
|
13762
14245
|
init_walk();
|
|
13763
14246
|
import { createHash as createHash5 } from "crypto";
|
|
13764
|
-
import { link, lstat as lstat3, mkdir as mkdir10, readFile as
|
|
13765
|
-
import { dirname as
|
|
14247
|
+
import { link, lstat as lstat3, mkdir as mkdir10, readFile as readFile16, rm as rm7, writeFile as writeFile8 } from "fs/promises";
|
|
14248
|
+
import { dirname as dirname5, join as join17, resolve as resolve7 } from "path";
|
|
13766
14249
|
var LocalFileCasAdapter = class _LocalFileCasAdapter {
|
|
13767
14250
|
tier = 1;
|
|
13768
14251
|
#read;
|
|
@@ -13853,7 +14336,7 @@ var LocalFileCasAdapter = class _LocalFileCasAdapter {
|
|
|
13853
14336
|
}
|
|
13854
14337
|
const gate = await this.#gateExisting(path, content);
|
|
13855
14338
|
if (!gate.ok) throw gate.error;
|
|
13856
|
-
const current = await
|
|
14339
|
+
const current = await readFile16(gate.abs);
|
|
13857
14340
|
const currentHash = createHash5("sha256").update(current).digest("hex");
|
|
13858
14341
|
if (currentHash !== opts.baseRevision.contentHash) {
|
|
13859
14342
|
const st = await lstat3(gate.abs);
|
|
@@ -13877,7 +14360,7 @@ var LocalFileCasAdapter = class _LocalFileCasAdapter {
|
|
|
13877
14360
|
async create(path, content) {
|
|
13878
14361
|
const gate = await this.#gateNew(path, content);
|
|
13879
14362
|
if (!gate.ok) throw gate.error;
|
|
13880
|
-
await mkdir10(
|
|
14363
|
+
await mkdir10(dirname5(gate.abs), { recursive: true });
|
|
13881
14364
|
const tmp = `${gate.abs}.${process.pid}.tmp`;
|
|
13882
14365
|
try {
|
|
13883
14366
|
await writeFile8(tmp, content, { encoding: "utf8", mode: 384 });
|
|
@@ -13888,7 +14371,7 @@ var LocalFileCasAdapter = class _LocalFileCasAdapter {
|
|
|
13888
14371
|
if (st.isSymbolicLink()) {
|
|
13889
14372
|
throw new Error(`refusing to follow a symlink inside the vault: ${path}`);
|
|
13890
14373
|
}
|
|
13891
|
-
const existing = await
|
|
14374
|
+
const existing = await readFile16(gate.abs);
|
|
13892
14375
|
return {
|
|
13893
14376
|
ok: false,
|
|
13894
14377
|
reason: "revision_conflict",
|
|
@@ -13946,8 +14429,8 @@ var LocalFileCasAdapter = class _LocalFileCasAdapter {
|
|
|
13946
14429
|
return { ok: false, error: err };
|
|
13947
14430
|
}
|
|
13948
14431
|
const rel = relativeTo(this.#root, abs);
|
|
13949
|
-
const
|
|
13950
|
-
return
|
|
14432
|
+
const refusal2 = this.#refuse(rel, path, content);
|
|
14433
|
+
return refusal2 ? { ok: false, error: refusal2 } : { ok: true, abs, rel };
|
|
13951
14434
|
}
|
|
13952
14435
|
/**
|
|
13953
14436
|
* Gates for a path that must NOT already exist.
|
|
@@ -13972,7 +14455,7 @@ var LocalFileCasAdapter = class _LocalFileCasAdapter {
|
|
|
13972
14455
|
}
|
|
13973
14456
|
let cursor = resolve7(this.#root);
|
|
13974
14457
|
for (const segment of rel.split("/").slice(0, -1)) {
|
|
13975
|
-
cursor =
|
|
14458
|
+
cursor = join17(cursor, segment);
|
|
13976
14459
|
try {
|
|
13977
14460
|
const st = await lstat3(cursor);
|
|
13978
14461
|
if (st.isSymbolicLink()) {
|
|
@@ -13986,8 +14469,8 @@ var LocalFileCasAdapter = class _LocalFileCasAdapter {
|
|
|
13986
14469
|
return { ok: false, error: err };
|
|
13987
14470
|
}
|
|
13988
14471
|
}
|
|
13989
|
-
const
|
|
13990
|
-
return
|
|
14472
|
+
const refusal2 = this.#refuse(rel, path, content);
|
|
14473
|
+
return refusal2 ? { ok: false, error: refusal2 } : { ok: true, abs, rel };
|
|
13991
14474
|
}
|
|
13992
14475
|
/**
|
|
13993
14476
|
* The rules, applied to a write. Returns the refusal or null.
|
|
@@ -14057,7 +14540,7 @@ init_ui();
|
|
|
14057
14540
|
|
|
14058
14541
|
// src/commands/revise.ts
|
|
14059
14542
|
init_src();
|
|
14060
|
-
import { readFile as
|
|
14543
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
14061
14544
|
import { Command as Command7 } from "commander";
|
|
14062
14545
|
init_errors();
|
|
14063
14546
|
init_index_build();
|
|
@@ -14072,6 +14555,69 @@ init_errors();
|
|
|
14072
14555
|
init_index_build();
|
|
14073
14556
|
init_cloud_brain();
|
|
14074
14557
|
init_vault();
|
|
14558
|
+
|
|
14559
|
+
// src/lib/guard-fast.ts
|
|
14560
|
+
init_brand();
|
|
14561
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
14562
|
+
import { mkdir as mkdir11, readdir as readdir7, rename as rename4, stat as stat4, unlink as unlink4, utimes, writeFile as writeFile9 } from "fs/promises";
|
|
14563
|
+
import { join as join18 } from "path";
|
|
14564
|
+
var fastDir = () => join18(brandHome(), "guard", "fast");
|
|
14565
|
+
var EMPTY_TTL_S = 120;
|
|
14566
|
+
var SESSION_ID = /^[A-Za-z0-9-]{1,128}$/;
|
|
14567
|
+
var EMPTY_MARK = /^(.+)\.empty\.(\d+)$/;
|
|
14568
|
+
var RULES_CHANGED = ".rules-changed";
|
|
14569
|
+
var TMP_PREFIX = ".tmp-";
|
|
14570
|
+
async function place(name, asOfMs) {
|
|
14571
|
+
await mkdir11(fastDir(), { recursive: true, mode: 448 });
|
|
14572
|
+
const tmp = join18(fastDir(), `${TMP_PREFIX}${process.pid}-${randomUUID3()}`);
|
|
14573
|
+
try {
|
|
14574
|
+
await writeFile9(tmp, "", { mode: 384 });
|
|
14575
|
+
const dated = (asOfMs - 1e3) / 1e3;
|
|
14576
|
+
await utimes(tmp, dated, dated);
|
|
14577
|
+
await rename4(tmp, join18(fastDir(), name));
|
|
14578
|
+
} catch (err) {
|
|
14579
|
+
await unlink4(tmp).catch(() => void 0);
|
|
14580
|
+
throw err;
|
|
14581
|
+
}
|
|
14582
|
+
}
|
|
14583
|
+
async function markServed(sessionId, asOfMs) {
|
|
14584
|
+
if (!SESSION_ID.test(sessionId)) return;
|
|
14585
|
+
await place(sessionId, asOfMs).catch(() => void 0);
|
|
14586
|
+
}
|
|
14587
|
+
async function markEmpty(sessionId, asOfMs) {
|
|
14588
|
+
if (!SESSION_ID.test(sessionId)) return;
|
|
14589
|
+
const until = Math.floor(asOfMs / 1e3) + EMPTY_TTL_S;
|
|
14590
|
+
await place(`${sessionId}.empty.${until}`, asOfMs).catch(() => void 0);
|
|
14591
|
+
await unlink4(join18(fastDir(), sessionId)).catch(() => void 0);
|
|
14592
|
+
}
|
|
14593
|
+
async function forgetEmptyChecks() {
|
|
14594
|
+
await mkdir11(fastDir(), { recursive: true, mode: 448 }).then(() => writeFile9(join18(fastDir(), RULES_CHANGED), "", { mode: 384 })).catch(() => void 0);
|
|
14595
|
+
const names = await readdir7(fastDir()).catch(() => []);
|
|
14596
|
+
await Promise.all(
|
|
14597
|
+
names.filter((name) => EMPTY_MARK.test(name)).map((name) => unlink4(join18(fastDir(), name)).catch(() => void 0))
|
|
14598
|
+
);
|
|
14599
|
+
}
|
|
14600
|
+
var SERVED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
14601
|
+
var TMP_TTL_MS = 60 * 1e3;
|
|
14602
|
+
async function pruneFast(nowMs = Date.now()) {
|
|
14603
|
+
const names = await readdir7(fastDir()).catch(() => []);
|
|
14604
|
+
const nowS = Math.floor(nowMs / 1e3);
|
|
14605
|
+
await Promise.all(
|
|
14606
|
+
names.map(async (name) => {
|
|
14607
|
+
const path = join18(fastDir(), name);
|
|
14608
|
+
const empty = EMPTY_MARK.exec(name);
|
|
14609
|
+
if (empty) {
|
|
14610
|
+
if (Number(empty[2]) <= nowS) await unlink4(path).catch(() => void 0);
|
|
14611
|
+
return;
|
|
14612
|
+
}
|
|
14613
|
+
const info = await stat4(path).catch(() => null);
|
|
14614
|
+
const ttl = name.startsWith(TMP_PREFIX) ? TMP_TTL_MS : SERVED_TTL_MS;
|
|
14615
|
+
if (info && nowMs - info.mtimeMs > ttl) await unlink4(path).catch(() => void 0);
|
|
14616
|
+
})
|
|
14617
|
+
);
|
|
14618
|
+
}
|
|
14619
|
+
|
|
14620
|
+
// src/commands/remember.ts
|
|
14075
14621
|
init_ui();
|
|
14076
14622
|
function rememberCommand() {
|
|
14077
14623
|
const said = { scope: false, noScope: false };
|
|
@@ -14145,6 +14691,7 @@ function rememberCommand() {
|
|
|
14145
14691
|
const kindLanded = kind === void 0 || written.kind === kind;
|
|
14146
14692
|
const boundaryLanded = neverStore.length === 0 || Array.isArray(written.never_store) && neverStore.every((term) => written.never_store?.includes(term) === true);
|
|
14147
14693
|
const overrideLanded = opts.overrideBoundary === void 0 || "boundary_override" in written;
|
|
14694
|
+
if (binding) await forgetEmptyChecks();
|
|
14148
14695
|
printDual({
|
|
14149
14696
|
json: {
|
|
14150
14697
|
id: written.id,
|
|
@@ -14779,6 +15326,7 @@ async function reviseHosted(where, ref, opts, added) {
|
|
|
14779
15326
|
}
|
|
14780
15327
|
}
|
|
14781
15328
|
}
|
|
15329
|
+
if (opts.rule === true) await forgetEmptyChecks();
|
|
14782
15330
|
printDual({
|
|
14783
15331
|
json: {
|
|
14784
15332
|
brain: where.name,
|
|
@@ -15022,7 +15570,7 @@ async function readInput(source) {
|
|
|
15022
15570
|
process.stdin.on("data", (chunk) => buf += chunk);
|
|
15023
15571
|
process.stdin.on("end", () => resolve17(buf));
|
|
15024
15572
|
process.stdin.on("error", reject);
|
|
15025
|
-
}) : await
|
|
15573
|
+
}) : await readFile17(source, "utf8").catch(() => source);
|
|
15026
15574
|
try {
|
|
15027
15575
|
return JSON.parse(raw);
|
|
15028
15576
|
} catch {
|
|
@@ -16522,7 +17070,7 @@ async function deleteCloudBrain(args) {
|
|
|
16522
17070
|
|
|
16523
17071
|
// src/commands/curation.ts
|
|
16524
17072
|
init_src();
|
|
16525
|
-
import { stat as
|
|
17073
|
+
import { stat as stat5 } from "fs/promises";
|
|
16526
17074
|
import { resolve as resolve8 } from "path";
|
|
16527
17075
|
import { Command as Command14 } from "commander";
|
|
16528
17076
|
|
|
@@ -16531,12 +17079,12 @@ init_atomic();
|
|
|
16531
17079
|
init_exclude();
|
|
16532
17080
|
init_walk();
|
|
16533
17081
|
import { createHash as createHash6 } from "crypto";
|
|
16534
|
-
import { readFile as
|
|
17082
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
16535
17083
|
var FILE = ".memgignore";
|
|
16536
17084
|
var hashOf = (content) => createHash6("sha256").update(Buffer.from(content, "utf8")).digest("hex");
|
|
16537
17085
|
async function readIgnoreFileState(root) {
|
|
16538
17086
|
try {
|
|
16539
|
-
const content = await
|
|
17087
|
+
const content = await readFile18(resolveInside(root, FILE), "utf8");
|
|
16540
17088
|
return { content, exists: true, hash: hashOf(content) };
|
|
16541
17089
|
} catch (err) {
|
|
16542
17090
|
if (err?.code === "ENOENT") {
|
|
@@ -16838,7 +17386,7 @@ async function mustMatchAFile(root, adapter, path) {
|
|
|
16838
17386
|
const wanted = path.normalize("NFC").replace(/^\/+/, "");
|
|
16839
17387
|
const absolute = resolve8(root, wanted);
|
|
16840
17388
|
if (isInside(absolute, root)) {
|
|
16841
|
-
const found = await
|
|
17389
|
+
const found = await stat5(absolute).catch(() => null);
|
|
16842
17390
|
if (found?.isFile()) return wanted;
|
|
16843
17391
|
}
|
|
16844
17392
|
const hits = sameBasename(await adapter.list(""), path);
|
|
@@ -17412,8 +17960,8 @@ function ago(iso, now) {
|
|
|
17412
17960
|
init_brand();
|
|
17413
17961
|
init_config();
|
|
17414
17962
|
import { createHash as createHash7 } from "crypto";
|
|
17415
|
-
import { mkdir as
|
|
17416
|
-
import { dirname as
|
|
17963
|
+
import { mkdir as mkdir12, readdir as readdir8, stat as stat6, unlink as unlink5, writeFile as writeFile10 } from "fs/promises";
|
|
17964
|
+
import { dirname as dirname6, isAbsolute as isAbsolute3, join as join20, resolve as resolve9 } from "path";
|
|
17417
17965
|
import { Command as Command17 } from "commander";
|
|
17418
17966
|
|
|
17419
17967
|
// src/lib/rules.ts
|
|
@@ -17484,6 +18032,7 @@ function renderRuleGroups(rules, project) {
|
|
|
17484
18032
|
init_src();
|
|
17485
18033
|
init_deadline();
|
|
17486
18034
|
init_cloud_brain();
|
|
18035
|
+
init_vault();
|
|
17487
18036
|
function guardCommand() {
|
|
17488
18037
|
return new Command17("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) => {
|
|
17489
18038
|
try {
|
|
@@ -17515,7 +18064,7 @@ async function runGuard(payload, contextDirFlag) {
|
|
|
17515
18064
|
if (!FILE_TOOLS.has(toolName)) return null;
|
|
17516
18065
|
const filePath = fileFrom(payload);
|
|
17517
18066
|
const cwd = typeof payload.cwd === "string" ? payload.cwd : process.cwd();
|
|
17518
|
-
const contextDir = contextDirFlag ?? (filePath === null ? cwd :
|
|
18067
|
+
const contextDir = contextDirFlag ?? (filePath === null ? cwd : dirname6(resolve9(cwd, filePath)));
|
|
17519
18068
|
const sessionId = typeof payload.session_id === "string" ? payload.session_id : null;
|
|
17520
18069
|
if (sessionId === null) return null;
|
|
17521
18070
|
const misdelivered = await misdeliveryNotice(toolName, filePath, sessionId);
|
|
@@ -17548,22 +18097,35 @@ async function runStopGuard(payload, contextDirFlag) {
|
|
|
17548
18097
|
});
|
|
17549
18098
|
}
|
|
17550
18099
|
async function claimRules(sessionId, contextDir) {
|
|
18100
|
+
const asOf = Date.now();
|
|
17551
18101
|
const where = await resolveTarget({}).catch(() => null);
|
|
17552
18102
|
if (where?.kind === "cloud") {
|
|
17553
|
-
if (!await claimSession(sessionId, where.brainId))
|
|
18103
|
+
if (!await claimSession(sessionId, where.brainId)) {
|
|
18104
|
+
if (await claimExists(sessionId, where.brainId)) await markServed(sessionId, asOf);
|
|
18105
|
+
return null;
|
|
18106
|
+
}
|
|
17554
18107
|
const hosted = await hostedRuleLines(where, contextDir).catch(() => null);
|
|
17555
18108
|
if (hosted !== null) {
|
|
17556
|
-
if (hosted.lines.length > 0)
|
|
18109
|
+
if (hosted.lines.length > 0) {
|
|
18110
|
+
await markServed(sessionId, asOf);
|
|
18111
|
+
return { lines: hosted.lines, total: hosted.total };
|
|
18112
|
+
}
|
|
17557
18113
|
await releaseSession(sessionId, where.brainId);
|
|
18114
|
+
await markEmpty(sessionId, asOf);
|
|
17558
18115
|
return null;
|
|
17559
18116
|
}
|
|
17560
18117
|
await releaseSession(sessionId, where.brainId);
|
|
17561
18118
|
}
|
|
18119
|
+
const named = await resolveBrain(void 0, contextDir).catch(() => null);
|
|
18120
|
+
if (named === null) return null;
|
|
18121
|
+
const root = named.brain.root;
|
|
18122
|
+
if (!await claimSession(sessionId, root)) return null;
|
|
17562
18123
|
const collected = await collectRules(void 0, contextDir).catch(() => null);
|
|
17563
|
-
if (collected === null)
|
|
18124
|
+
if (collected === null || collected.rules.length === 0) {
|
|
18125
|
+
await releaseSession(sessionId, root);
|
|
18126
|
+
return null;
|
|
18127
|
+
}
|
|
17564
18128
|
const { brainRoot, rules, total } = collected;
|
|
17565
|
-
if (rules.length === 0) return null;
|
|
17566
|
-
if (!await claimSession(sessionId, brainRoot)) return null;
|
|
17567
18129
|
await recordEvent(brainRoot, {
|
|
17568
18130
|
verb: "guard",
|
|
17569
18131
|
contextDir,
|
|
@@ -17619,7 +18181,7 @@ async function readPayload() {
|
|
|
17619
18181
|
return null;
|
|
17620
18182
|
}
|
|
17621
18183
|
}
|
|
17622
|
-
var guardDir = () =>
|
|
18184
|
+
var guardDir = () => join20(brandHome(), "guard");
|
|
17623
18185
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
17624
18186
|
"Write",
|
|
17625
18187
|
"Edit",
|
|
@@ -17640,7 +18202,7 @@ async function misdeliveryNotice(toolName, filePath, sessionId) {
|
|
|
17640
18202
|
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.';
|
|
17641
18203
|
}
|
|
17642
18204
|
function markerFor(sessionId, brain) {
|
|
17643
|
-
return
|
|
18205
|
+
return join20(
|
|
17644
18206
|
guardDir(),
|
|
17645
18207
|
`${createHash7("sha256").update(`${sessionId}\0${brain}`).digest("hex").slice(0, 32)}`
|
|
17646
18208
|
);
|
|
@@ -17648,8 +18210,8 @@ function markerFor(sessionId, brain) {
|
|
|
17648
18210
|
async function claimSession(sessionId, brain) {
|
|
17649
18211
|
const marker = markerFor(sessionId, brain);
|
|
17650
18212
|
try {
|
|
17651
|
-
await
|
|
17652
|
-
await
|
|
18213
|
+
await mkdir12(guardDir(), { recursive: true, mode: 448 });
|
|
18214
|
+
await writeFile10(marker, "", { flag: "wx", mode: 384 });
|
|
17653
18215
|
} catch {
|
|
17654
18216
|
return false;
|
|
17655
18217
|
}
|
|
@@ -17657,19 +18219,23 @@ async function claimSession(sessionId, brain) {
|
|
|
17657
18219
|
return true;
|
|
17658
18220
|
}
|
|
17659
18221
|
async function releaseSession(sessionId, brain) {
|
|
17660
|
-
await
|
|
18222
|
+
await unlink5(markerFor(sessionId, brain)).catch(() => void 0);
|
|
18223
|
+
}
|
|
18224
|
+
async function claimExists(sessionId, brain) {
|
|
18225
|
+
return stat6(markerFor(sessionId, brain)).then(() => true).catch(() => false);
|
|
17661
18226
|
}
|
|
17662
18227
|
var MARKER_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
17663
18228
|
async function pruneOldMarkers() {
|
|
17664
18229
|
try {
|
|
17665
18230
|
const dir = guardDir();
|
|
17666
18231
|
const now = Date.now();
|
|
17667
|
-
for (const name of await
|
|
17668
|
-
const path =
|
|
18232
|
+
for (const name of await readdir8(dir)) {
|
|
18233
|
+
const path = join20(dir, name);
|
|
17669
18234
|
if (!isAbsolute3(path)) continue;
|
|
17670
|
-
const info = await
|
|
17671
|
-
if (info && now - info.mtimeMs > MARKER_TTL_MS) await
|
|
18235
|
+
const info = await stat6(path).catch(() => null);
|
|
18236
|
+
if (info && now - info.mtimeMs > MARKER_TTL_MS) await unlink5(path).catch(() => void 0);
|
|
17672
18237
|
}
|
|
18238
|
+
await pruneFast(now);
|
|
17673
18239
|
} catch {
|
|
17674
18240
|
}
|
|
17675
18241
|
}
|
|
@@ -17711,7 +18277,7 @@ init_link();
|
|
|
17711
18277
|
// src/commands/log.ts
|
|
17712
18278
|
init_src();
|
|
17713
18279
|
init_walk();
|
|
17714
|
-
import { lstat as lstat4, readFile as
|
|
18280
|
+
import { lstat as lstat4, readFile as readFile20 } from "fs/promises";
|
|
17715
18281
|
import { Command as Command19 } from "commander";
|
|
17716
18282
|
init_errors();
|
|
17717
18283
|
init_cloud_brain();
|
|
@@ -18277,7 +18843,7 @@ async function readOrNull(root, path) {
|
|
|
18277
18843
|
if (absolute === null) return null;
|
|
18278
18844
|
const info = await lstat4(absolute).catch(() => null);
|
|
18279
18845
|
if (!info || !info.isFile()) return null;
|
|
18280
|
-
return
|
|
18846
|
+
return readFile20(absolute, "utf8").catch(() => null);
|
|
18281
18847
|
}
|
|
18282
18848
|
|
|
18283
18849
|
// src/commands/project.ts
|
|
@@ -18413,37 +18979,142 @@ init_api_client();
|
|
|
18413
18979
|
init_credentials();
|
|
18414
18980
|
init_config();
|
|
18415
18981
|
init_pending_login();
|
|
18416
|
-
init_ui();
|
|
18417
18982
|
import { Command as Command22 } from "commander";
|
|
18418
|
-
|
|
18419
|
-
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18423
|
-
|
|
18424
|
-
|
|
18425
|
-
|
|
18426
|
-
|
|
18427
|
-
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18432
|
-
|
|
18433
|
-
|
|
18434
|
-
|
|
18435
|
-
|
|
18436
|
-
|
|
18437
|
-
|
|
18438
|
-
|
|
18439
|
-
|
|
18440
|
-
|
|
18441
|
-
|
|
18442
|
-
|
|
18443
|
-
|
|
18444
|
-
|
|
18445
|
-
|
|
18446
|
-
|
|
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
|
|
19088
|
+
init_ui();
|
|
19089
|
+
function logoutCommand() {
|
|
19090
|
+
return new Command22("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
|
+
"--local",
|
|
19092
|
+
"forget the token here without telling the server \u2014 only for a server you cannot reach"
|
|
19093
|
+
).action(async (opts) => {
|
|
19094
|
+
const base2 = apiUrl();
|
|
19095
|
+
const credentials = await readCredentials();
|
|
19096
|
+
await clearPendingLogin();
|
|
19097
|
+
await clearSetupFollowUps();
|
|
19098
|
+
if (!credentials) {
|
|
19099
|
+
printDual({
|
|
19100
|
+
json: { signed_out: true, was_signed_in: false, api_url: base2 },
|
|
19101
|
+
human: () => printHuman("Not signed in \u2014 nothing to do.")
|
|
19102
|
+
});
|
|
19103
|
+
return;
|
|
19104
|
+
}
|
|
19105
|
+
if (opts.local) {
|
|
19106
|
+
await clearCredentials();
|
|
19107
|
+
printDual({
|
|
19108
|
+
json: { signed_out: true, revoked_on_server: false, api_url: credentials.api_url },
|
|
19109
|
+
human: () => {
|
|
19110
|
+
printHuman("Forgotten here.");
|
|
19111
|
+
printHuman(
|
|
19112
|
+
c.yellow(
|
|
19113
|
+
" The token was NOT revoked \u2014 it still works for anyone holding a copy.\n Run `memgineering logout --all` once the server is reachable."
|
|
19114
|
+
)
|
|
19115
|
+
);
|
|
19116
|
+
}
|
|
19117
|
+
});
|
|
18447
19118
|
return;
|
|
18448
19119
|
}
|
|
18449
19120
|
let revoked = null;
|
|
@@ -18484,12 +19155,13 @@ function logoutCommand() {
|
|
|
18484
19155
|
}) : err;
|
|
18485
19156
|
}
|
|
18486
19157
|
}
|
|
18487
|
-
const
|
|
19158
|
+
const registry = (await readCredentials())?.registry ?? credentials.registry;
|
|
19159
|
+
const registryRevoked = await revokeRegistrySession(registry);
|
|
18488
19160
|
await clearCredentials();
|
|
18489
19161
|
printDual({
|
|
18490
19162
|
json: {
|
|
18491
19163
|
signed_out: true,
|
|
18492
|
-
registry:
|
|
19164
|
+
registry: registry === void 0 ? "not-linked" : registryRevoked ? "revoked" : "revoke-failed-expires-on-its-own",
|
|
18493
19165
|
revoked_on_server: true,
|
|
18494
19166
|
revoked,
|
|
18495
19167
|
all: opts.all === true,
|
|
@@ -18514,18 +19186,16 @@ function logoutCommand() {
|
|
|
18514
19186
|
async function revokeRegistrySession(reg) {
|
|
18515
19187
|
if (!reg) return false;
|
|
18516
19188
|
try {
|
|
18517
|
-
const
|
|
18518
|
-
|
|
18519
|
-
|
|
18520
|
-
body: JSON.stringify({ refresh_token: reg.refresh_token }),
|
|
18521
|
-
signal: AbortSignal.timeout(3e3)
|
|
19189
|
+
const { registry, tokens } = await refreshRegistrySession(reg, {
|
|
19190
|
+
apiUrlOf: (r) => r.api_url,
|
|
19191
|
+
requestTimeoutMs: 3e3
|
|
18522
19192
|
});
|
|
18523
|
-
if (
|
|
18524
|
-
|
|
18525
|
-
|
|
18526
|
-
const del = await fetch(`${
|
|
19193
|
+
if (typeof tokens.access_token !== "string" || typeof tokens.session_id !== "string") {
|
|
19194
|
+
return false;
|
|
19195
|
+
}
|
|
19196
|
+
const del = await fetch(`${registry.api_url}/auth/sessions/${tokens.session_id}`, {
|
|
18527
19197
|
method: "DELETE",
|
|
18528
|
-
headers: { authorization: `Bearer ${
|
|
19198
|
+
headers: { authorization: `Bearer ${tokens.access_token}` },
|
|
18529
19199
|
signal: AbortSignal.timeout(3e3)
|
|
18530
19200
|
});
|
|
18531
19201
|
return del.ok;
|
|
@@ -18687,8 +19357,8 @@ init_api_client();
|
|
|
18687
19357
|
init_config();
|
|
18688
19358
|
init_cloud_brain();
|
|
18689
19359
|
init_ui();
|
|
18690
|
-
import { mkdir as
|
|
18691
|
-
import { dirname as
|
|
19360
|
+
import { mkdir as mkdir13, readFile as readFile21, writeFile as writeFile11 } from "fs/promises";
|
|
19361
|
+
import { dirname as dirname7, isAbsolute as isAbsolute4, join as join21, resolve as resolve11, sep as sep2 } from "path";
|
|
18692
19362
|
import { Command as Command24 } from "commander";
|
|
18693
19363
|
function pullCommand() {
|
|
18694
19364
|
return new Command24("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) => {
|
|
@@ -18756,8 +19426,8 @@ function pullCommand() {
|
|
|
18756
19426
|
already.push(note.path);
|
|
18757
19427
|
continue;
|
|
18758
19428
|
}
|
|
18759
|
-
await
|
|
18760
|
-
await
|
|
19429
|
+
await mkdir13(dirname7(file), { recursive: true });
|
|
19430
|
+
await writeFile11(file, note.content ?? "", "utf8");
|
|
18761
19431
|
written.push(note.path);
|
|
18762
19432
|
if (note.excluded === true) excluded.push(note.path);
|
|
18763
19433
|
}
|
|
@@ -18871,7 +19541,7 @@ function safeJoin(root, relative3) {
|
|
|
18871
19541
|
"Nothing was written for it. This should not happen \u2014 please report it."
|
|
18872
19542
|
);
|
|
18873
19543
|
}
|
|
18874
|
-
const full = resolve11(
|
|
19544
|
+
const full = resolve11(join21(root, relative3));
|
|
18875
19545
|
if (full !== root && !full.startsWith(root + sep2)) {
|
|
18876
19546
|
throw memgError(
|
|
18877
19547
|
"server_refused",
|
|
@@ -18883,7 +19553,7 @@ function safeJoin(root, relative3) {
|
|
|
18883
19553
|
}
|
|
18884
19554
|
async function exists(file) {
|
|
18885
19555
|
try {
|
|
18886
|
-
await
|
|
19556
|
+
await readFile21(file);
|
|
18887
19557
|
return true;
|
|
18888
19558
|
} catch {
|
|
18889
19559
|
return false;
|
|
@@ -19032,72 +19702,6 @@ ${body}`;
|
|
|
19032
19702
|
init_cloud_brain();
|
|
19033
19703
|
init_index_build();
|
|
19034
19704
|
|
|
19035
|
-
// src/lib/registry-session.ts
|
|
19036
|
-
init_errors();
|
|
19037
|
-
init_credentials();
|
|
19038
|
-
init_version();
|
|
19039
|
-
function registryApiUrl(stored) {
|
|
19040
|
-
const override = process.env["KORDIS_API_URL"];
|
|
19041
|
-
return override !== void 0 && override !== "" ? override : stored;
|
|
19042
|
-
}
|
|
19043
|
-
async function requireRegistryCredentials() {
|
|
19044
|
-
const credentials = await readCredentials();
|
|
19045
|
-
const registry = credentials?.registry;
|
|
19046
|
-
if (!registry) {
|
|
19047
|
-
throw memgError(
|
|
19048
|
-
"not_linked",
|
|
19049
|
-
"this machine holds no registry identity",
|
|
19050
|
-
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."
|
|
19051
|
-
);
|
|
19052
|
-
}
|
|
19053
|
-
return registry;
|
|
19054
|
-
}
|
|
19055
|
-
async function openRegistrySession(fetchImpl) {
|
|
19056
|
-
const registry = await requireRegistryCredentials();
|
|
19057
|
-
const baseUrl = registryApiUrl(registry.api_url);
|
|
19058
|
-
const anonymous = new KordisClient({
|
|
19059
|
-
baseUrl,
|
|
19060
|
-
userAgent: `memgineering/${VERSION}`,
|
|
19061
|
-
...fetchImpl ? { fetch: fetchImpl } : {}
|
|
19062
|
-
});
|
|
19063
|
-
let tokens;
|
|
19064
|
-
try {
|
|
19065
|
-
tokens = await anonymous.refreshSession(registry.refresh_token);
|
|
19066
|
-
} catch (err) {
|
|
19067
|
-
throw memgError(
|
|
19068
|
-
"not_linked",
|
|
19069
|
-
`the registry refused this machine's stored sign-in: ${err instanceof Error ? err.message : String(err)}`,
|
|
19070
|
-
"Nothing was sent. The link between this brain and your registry account has expired or been revoked. `memgineering login` re-links both."
|
|
19071
|
-
);
|
|
19072
|
-
}
|
|
19073
|
-
const credentialsRotated = await persistRotatedRefresh(
|
|
19074
|
-
registry,
|
|
19075
|
-
tokens.refresh_token,
|
|
19076
|
-
tokens.session_id
|
|
19077
|
-
);
|
|
19078
|
-
const client = new KordisClient({
|
|
19079
|
-
baseUrl,
|
|
19080
|
-
userAgent: `memgineering/${VERSION}`,
|
|
19081
|
-
getAccessToken: () => tokens.access_token,
|
|
19082
|
-
...fetchImpl ? { fetch: fetchImpl } : {}
|
|
19083
|
-
});
|
|
19084
|
-
return { client, username: registry.username, apiUrl: baseUrl, credentialsRotated };
|
|
19085
|
-
}
|
|
19086
|
-
async function persistRotatedRefresh(registry, refreshToken, sessionId) {
|
|
19087
|
-
if (refreshToken === registry.refresh_token && sessionId === registry.session_id) return false;
|
|
19088
|
-
try {
|
|
19089
|
-
const credentials = await readCredentials();
|
|
19090
|
-
if (!credentials?.registry) return false;
|
|
19091
|
-
await saveCredentials({
|
|
19092
|
-
...credentials,
|
|
19093
|
-
registry: { ...credentials.registry, refresh_token: refreshToken, session_id: sessionId }
|
|
19094
|
-
});
|
|
19095
|
-
return true;
|
|
19096
|
-
} catch {
|
|
19097
|
-
return false;
|
|
19098
|
-
}
|
|
19099
|
-
}
|
|
19100
|
-
|
|
19101
19705
|
// src/lib/publish-version.ts
|
|
19102
19706
|
import semver from "semver";
|
|
19103
19707
|
|
|
@@ -19793,9 +20397,9 @@ init_errors();
|
|
|
19793
20397
|
init_credentials();
|
|
19794
20398
|
|
|
19795
20399
|
// src/lib/untrusted-fence.ts
|
|
19796
|
-
import { randomUUID as
|
|
20400
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
19797
20401
|
function makeUntrustedSentinel() {
|
|
19798
|
-
return `KORDIS-UNTRUSTED-${
|
|
20402
|
+
return `KORDIS-UNTRUSTED-${randomUUID4()}`;
|
|
19799
20403
|
}
|
|
19800
20404
|
function fenceUntrusted(sentinel, text) {
|
|
19801
20405
|
const neutralized = text.replace(/KORDIS-UNTRUSTED-/g, "KORDIS-UNTRUSTED\u200B-");
|
|
@@ -20290,8 +20894,8 @@ init_errors();
|
|
|
20290
20894
|
|
|
20291
20895
|
// src/lib/harness-context.ts
|
|
20292
20896
|
init_brand();
|
|
20293
|
-
import { access, readFile as
|
|
20294
|
-
import { join as
|
|
20897
|
+
import { access, readFile as readFile22 } from "fs/promises";
|
|
20898
|
+
import { join as join22 } from "path";
|
|
20295
20899
|
var PER_FILE_CAP = 16 * 1024;
|
|
20296
20900
|
var TOTAL_CAP = 48 * 1024;
|
|
20297
20901
|
var TRUNCATION_RESERVE = 64;
|
|
@@ -20336,13 +20940,13 @@ function extractHeadings(content) {
|
|
|
20336
20940
|
}
|
|
20337
20941
|
async function readHarnessContext(cwd = process.cwd(), home = agentHome()) {
|
|
20338
20942
|
const candidates = [
|
|
20339
|
-
{ path:
|
|
20340
|
-
{ path:
|
|
20341
|
-
{ path:
|
|
20342
|
-
{ path:
|
|
20343
|
-
{ path:
|
|
20344
|
-
{ path:
|
|
20345
|
-
{ path:
|
|
20943
|
+
{ path: join22(cwd, "CLAUDE.md"), display: "CLAUDE.md", scope: "project" },
|
|
20944
|
+
{ path: join22(cwd, "AGENTS.md"), display: "AGENTS.md", scope: "project" },
|
|
20945
|
+
{ path: join22(cwd, "GEMINI.md"), display: "GEMINI.md", scope: "project" },
|
|
20946
|
+
{ path: join22(cwd, ".claude", "CLAUDE.md"), display: ".claude/CLAUDE.md", scope: "project" },
|
|
20947
|
+
{ path: join22(home, ".claude", "CLAUDE.md"), display: "~/.claude/CLAUDE.md", scope: "global" },
|
|
20948
|
+
{ path: join22(home, ".codex", "AGENTS.md"), display: "~/.codex/AGENTS.md", scope: "global" },
|
|
20949
|
+
{ path: join22(home, ".gemini", "GEMINI.md"), display: "~/.gemini/GEMINI.md", scope: "global" }
|
|
20346
20950
|
];
|
|
20347
20951
|
const files2 = [];
|
|
20348
20952
|
let totalBytes = 0;
|
|
@@ -20350,7 +20954,7 @@ async function readHarnessContext(cwd = process.cwd(), home = agentHome()) {
|
|
|
20350
20954
|
if (!await pathExists(cand.path)) continue;
|
|
20351
20955
|
let raw;
|
|
20352
20956
|
try {
|
|
20353
|
-
raw = await
|
|
20957
|
+
raw = await readFile22(cand.path, "utf8");
|
|
20354
20958
|
} catch {
|
|
20355
20959
|
continue;
|
|
20356
20960
|
}
|
|
@@ -20747,8 +21351,8 @@ function parseLimit2(raw) {
|
|
|
20747
21351
|
import { Command as Command30 } from "commander";
|
|
20748
21352
|
import prompts3 from "prompts";
|
|
20749
21353
|
import { existsSync, lstatSync, realpathSync } from "fs";
|
|
20750
|
-
import { mkdir as
|
|
20751
|
-
import { dirname as
|
|
21354
|
+
import { mkdir as mkdir15, readFile as readFile24, stat as stat7, writeFile as writeFile13 } from "fs/promises";
|
|
21355
|
+
import { dirname as dirname8, join as join24, resolve as resolve12, sep as sep3 } from "path";
|
|
20752
21356
|
|
|
20753
21357
|
// ../../packages/translators/src/skill/identity.ts
|
|
20754
21358
|
function buildSkillFile(input, outputPath) {
|
|
@@ -21323,80 +21927,31 @@ function getTranslator(type, format) {
|
|
|
21323
21927
|
return fn;
|
|
21324
21928
|
}
|
|
21325
21929
|
|
|
21326
|
-
//
|
|
21327
|
-
|
|
21328
|
-
|
|
21329
|
-
|
|
21330
|
-
|
|
21331
|
-
|
|
21332
|
-
|
|
21333
|
-
|
|
21334
|
-
|
|
21335
|
-
|
|
21336
|
-
|
|
21337
|
-
}
|
|
21338
|
-
function lockfilePath() {
|
|
21339
|
-
return join21(brandHome(), "installed.json");
|
|
21340
|
-
}
|
|
21341
|
-
function kordisLockfilePath() {
|
|
21342
|
-
return join21(process.env["KORDIS_HOME"] ?? join21(homedir4(), ".kordis"), "installed.json");
|
|
21343
|
-
}
|
|
21344
|
-
function lockfileDisplayPath() {
|
|
21345
|
-
const path = lockfilePath();
|
|
21346
|
-
const home = process.env["HOME"];
|
|
21347
|
-
return home !== void 0 && home !== "" && path.startsWith(home) ? "~" + path.slice(home.length) : path;
|
|
21348
|
-
}
|
|
21349
|
-
async function migrateFromKordis() {
|
|
21350
|
-
const source = kordisLockfilePath();
|
|
21351
|
-
const destination = lockfilePath();
|
|
21352
|
-
try {
|
|
21353
|
-
await mkdir13(brandHome(), { recursive: true });
|
|
21354
|
-
await copyFile(source, destination, fsConstants.COPYFILE_EXCL);
|
|
21355
|
-
} catch {
|
|
21356
|
-
return;
|
|
21357
|
-
}
|
|
21358
|
-
try {
|
|
21359
|
-
const raw = await readFile22(destination, "utf8");
|
|
21360
|
-
const parsed = JSON.parse(raw);
|
|
21361
|
-
if (parsed.migratedFrom === void 0) {
|
|
21362
|
-
parsed.migratedFrom = source;
|
|
21363
|
-
await writeLockfile(parsed);
|
|
21364
|
-
}
|
|
21365
|
-
} catch {
|
|
21366
|
-
}
|
|
21367
|
-
}
|
|
21368
|
-
async function readLockfile() {
|
|
21369
|
-
try {
|
|
21370
|
-
return JSON.parse(await readFile22(lockfilePath(), "utf8"));
|
|
21371
|
-
} catch {
|
|
21372
|
-
await migrateFromKordis();
|
|
21930
|
+
// ../../packages/translators/src/merge-emitted-file.ts
|
|
21931
|
+
import tomlLib2 from "@iarna/toml";
|
|
21932
|
+
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
21933
|
+
var MergeRefusal = class extends Error {
|
|
21934
|
+
code;
|
|
21935
|
+
reason;
|
|
21936
|
+
constructor(reason) {
|
|
21937
|
+
super(refusalMessage(reason));
|
|
21938
|
+
this.name = "MergeRefusal";
|
|
21939
|
+
this.code = reason.kind === "prototype_key" ? "unsafe_package_config" : "merge_refused";
|
|
21940
|
+
this.reason = reason;
|
|
21373
21941
|
}
|
|
21374
|
-
|
|
21375
|
-
|
|
21376
|
-
|
|
21377
|
-
|
|
21942
|
+
};
|
|
21943
|
+
function refusalMessage(reason) {
|
|
21944
|
+
switch (reason.kind) {
|
|
21945
|
+
case "prototype_key":
|
|
21946
|
+
return `refusing to merge config fragment \u2014 prototype-polluting key "${reason.key}" at ${reason.path}`;
|
|
21947
|
+
case "malformed_file":
|
|
21948
|
+
return `refusing to merge into "${reason.targetPath}" \u2014 existing file is not valid ${reason.format.toUpperCase()}: ${reason.parseError}`;
|
|
21949
|
+
case "non_object_root":
|
|
21950
|
+
return `refusing to merge into "${reason.targetPath}" \u2014 existing ${reason.format.toUpperCase()} root is not an object`;
|
|
21951
|
+
case "dangling_marker":
|
|
21952
|
+
return `refusing to merge \u2014 found a "kordis:${reason.fullSlug}" start marker with no matching end marker`;
|
|
21378
21953
|
}
|
|
21379
21954
|
}
|
|
21380
|
-
async function writeLockfile(lf) {
|
|
21381
|
-
await mkdir13(brandHome(), { recursive: true });
|
|
21382
|
-
await writeFile11(lockfilePath(), JSON.stringify(lf, null, 2) + "\n", {
|
|
21383
|
-
encoding: "utf8",
|
|
21384
|
-
mode: 384
|
|
21385
|
-
});
|
|
21386
|
-
}
|
|
21387
|
-
async function recordInstalled(fullSlug, entry) {
|
|
21388
|
-
const home = brandHome();
|
|
21389
|
-
await withFileLock(join21(home, "installed.lock"), home, async () => {
|
|
21390
|
-
const lf = await readLockfile();
|
|
21391
|
-
lf.packages[fullSlug] = entry;
|
|
21392
|
-
await writeLockfile(lf);
|
|
21393
|
-
});
|
|
21394
|
-
}
|
|
21395
|
-
|
|
21396
|
-
// src/lib/merge-emitted-file.ts
|
|
21397
|
-
init_errors();
|
|
21398
|
-
import tomlLib2 from "@iarna/toml";
|
|
21399
|
-
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
|
|
21400
21955
|
function isPlainObject(v) {
|
|
21401
21956
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
21402
21957
|
}
|
|
@@ -21421,11 +21976,7 @@ function assertNoProtoKeys(value, path) {
|
|
|
21421
21976
|
if (!isPlainObject(value)) return;
|
|
21422
21977
|
for (const [k, v] of Object.entries(value)) {
|
|
21423
21978
|
if (PROTO_KEYS.has(k)) {
|
|
21424
|
-
throw
|
|
21425
|
-
"unsafe_package_config",
|
|
21426
|
-
`refusing to merge config fragment \u2014 prototype-polluting key "${k}" at ${[...path, k].join(".")}`,
|
|
21427
|
-
"Nothing was written. The package emits a config key named __proto__, constructor or prototype, which no legitimate tool config needs. Do not install it, and tell the author if you did not publish it."
|
|
21428
|
-
);
|
|
21979
|
+
throw new MergeRefusal({ kind: "prototype_key", key: k, path: [...path, k].join(".") });
|
|
21429
21980
|
}
|
|
21430
21981
|
assertNoProtoKeys(v, [...path, k]);
|
|
21431
21982
|
}
|
|
@@ -21434,12 +21985,15 @@ function isOwnedServerLeaf(path) {
|
|
|
21434
21985
|
return path.length === 2 && (path[0] === "mcpServers" || path[0] === "mcp_servers");
|
|
21435
21986
|
}
|
|
21436
21987
|
function deepMergeFragment(existing, fragment, path = []) {
|
|
21437
|
-
|
|
21988
|
+
assertNoProtoKeys(fragment, path);
|
|
21989
|
+
return mergeAt(existing, fragment, path);
|
|
21990
|
+
}
|
|
21991
|
+
function mergeAt(existing, fragment, path) {
|
|
21438
21992
|
if (isOwnedServerLeaf(path)) return fragment;
|
|
21439
21993
|
if (isPlainObject(existing) && isPlainObject(fragment)) {
|
|
21440
21994
|
const out = { ...existing };
|
|
21441
21995
|
for (const [k, v] of Object.entries(fragment)) {
|
|
21442
|
-
out[k] =
|
|
21996
|
+
out[k] = mergeAt(existing[k], v, [...path, k]);
|
|
21443
21997
|
}
|
|
21444
21998
|
return out;
|
|
21445
21999
|
}
|
|
@@ -21482,18 +22036,15 @@ function mergeStructuredFile(existingRaw, fragmentRaw, format, targetPath) {
|
|
|
21482
22036
|
try {
|
|
21483
22037
|
existing = parseStructured(existingRaw, format);
|
|
21484
22038
|
} catch (err) {
|
|
21485
|
-
throw
|
|
21486
|
-
"
|
|
21487
|
-
|
|
21488
|
-
|
|
21489
|
-
|
|
22039
|
+
throw new MergeRefusal({
|
|
22040
|
+
kind: "malformed_file",
|
|
22041
|
+
targetPath,
|
|
22042
|
+
format,
|
|
22043
|
+
parseError: err.message
|
|
22044
|
+
});
|
|
21490
22045
|
}
|
|
21491
22046
|
if (!isPlainObject(existing) && isPlainObject(fragment)) {
|
|
21492
|
-
throw
|
|
21493
|
-
"merge_refused",
|
|
21494
|
-
`refusing to merge into "${targetPath}" \u2014 existing ${format.toUpperCase()} root is not an object`,
|
|
21495
|
-
`Nothing was written. ${targetPath} parses, but its top level is not an object, so a config fragment cannot be merged into it. Fix its shape or move it aside, then run the install again.`
|
|
21496
|
-
);
|
|
22047
|
+
throw new MergeRefusal({ kind: "non_object_root", targetPath, format });
|
|
21497
22048
|
}
|
|
21498
22049
|
const merged = deepMergeFragment(existing, fragment);
|
|
21499
22050
|
return stringifyStructured(merged, format);
|
|
@@ -21515,15 +22066,100 @@ function mergeFencedFile(existingRaw, fullSlug, block) {
|
|
|
21515
22066
|
return existingRaw.slice(0, startMatch.index) + trimmedBlock + existingRaw.slice(spanEnd);
|
|
21516
22067
|
}
|
|
21517
22068
|
if (startMatch && endIdx === -1) {
|
|
21518
|
-
throw
|
|
21519
|
-
"merge_refused",
|
|
21520
|
-
`refusing to merge \u2014 found a "kordis:${fullSlug}" start marker with no matching end marker`,
|
|
21521
|
-
`Nothing was written. The file has a dangling <!-- kordis:${fullSlug}:\u2026:start --> marker. Delete that line, or restore its <!-- kordis:${fullSlug}:end --> line, then run the install again.`
|
|
21522
|
-
);
|
|
22069
|
+
throw new MergeRefusal({ kind: "dangling_marker", fullSlug });
|
|
21523
22070
|
}
|
|
21524
22071
|
return existingRaw.trimEnd() + "\n\n" + trimmedBlock + "\n";
|
|
21525
22072
|
}
|
|
21526
22073
|
|
|
22074
|
+
// src/commands/registry/pull.ts
|
|
22075
|
+
init_errors();
|
|
22076
|
+
init_ui();
|
|
22077
|
+
|
|
22078
|
+
// src/lib/lockfile.ts
|
|
22079
|
+
init_brand();
|
|
22080
|
+
import { copyFile, mkdir as mkdir14, readFile as readFile23, writeFile as writeFile12, constants as fsConstants } from "fs/promises";
|
|
22081
|
+
import { homedir as homedir4 } from "os";
|
|
22082
|
+
import { join as join23 } from "path";
|
|
22083
|
+
function effectiveRange(entry) {
|
|
22084
|
+
return entry.requestedRange ?? `^${entry.version}`;
|
|
22085
|
+
}
|
|
22086
|
+
function lockfilePath() {
|
|
22087
|
+
return join23(brandHome(), "installed.json");
|
|
22088
|
+
}
|
|
22089
|
+
function kordisLockfilePath() {
|
|
22090
|
+
return join23(process.env["KORDIS_HOME"] ?? join23(homedir4(), ".kordis"), "installed.json");
|
|
22091
|
+
}
|
|
22092
|
+
function lockfileDisplayPath() {
|
|
22093
|
+
const path = lockfilePath();
|
|
22094
|
+
const home = process.env["HOME"];
|
|
22095
|
+
return home !== void 0 && home !== "" && path.startsWith(home) ? "~" + path.slice(home.length) : path;
|
|
22096
|
+
}
|
|
22097
|
+
async function migrateFromKordis() {
|
|
22098
|
+
const source = kordisLockfilePath();
|
|
22099
|
+
const destination = lockfilePath();
|
|
22100
|
+
try {
|
|
22101
|
+
await mkdir14(brandHome(), { recursive: true });
|
|
22102
|
+
await copyFile(source, destination, fsConstants.COPYFILE_EXCL);
|
|
22103
|
+
} catch {
|
|
22104
|
+
return;
|
|
22105
|
+
}
|
|
22106
|
+
try {
|
|
22107
|
+
const raw = await readFile23(destination, "utf8");
|
|
22108
|
+
const parsed = JSON.parse(raw);
|
|
22109
|
+
if (parsed.migratedFrom === void 0) {
|
|
22110
|
+
parsed.migratedFrom = source;
|
|
22111
|
+
await writeLockfile(parsed);
|
|
22112
|
+
}
|
|
22113
|
+
} catch {
|
|
22114
|
+
}
|
|
22115
|
+
}
|
|
22116
|
+
async function readLockfile() {
|
|
22117
|
+
try {
|
|
22118
|
+
return JSON.parse(await readFile23(lockfilePath(), "utf8"));
|
|
22119
|
+
} catch {
|
|
22120
|
+
await migrateFromKordis();
|
|
22121
|
+
}
|
|
22122
|
+
try {
|
|
22123
|
+
return JSON.parse(await readFile23(lockfilePath(), "utf8"));
|
|
22124
|
+
} catch {
|
|
22125
|
+
return { packages: {} };
|
|
22126
|
+
}
|
|
22127
|
+
}
|
|
22128
|
+
async function writeLockfile(lf) {
|
|
22129
|
+
await mkdir14(brandHome(), { recursive: true });
|
|
22130
|
+
await writeFile12(lockfilePath(), JSON.stringify(lf, null, 2) + "\n", {
|
|
22131
|
+
encoding: "utf8",
|
|
22132
|
+
mode: 384
|
|
22133
|
+
});
|
|
22134
|
+
}
|
|
22135
|
+
async function recordInstalled(fullSlug, entry) {
|
|
22136
|
+
const home = brandHome();
|
|
22137
|
+
await withFileLock(join23(home, "installed.lock"), home, async () => {
|
|
22138
|
+
const lf = await readLockfile();
|
|
22139
|
+
lf.packages[fullSlug] = entry;
|
|
22140
|
+
await writeLockfile(lf);
|
|
22141
|
+
});
|
|
22142
|
+
}
|
|
22143
|
+
|
|
22144
|
+
// src/lib/merge-refusal.ts
|
|
22145
|
+
init_errors();
|
|
22146
|
+
function adaptMergeRefusal(err) {
|
|
22147
|
+
if (!(err instanceof MergeRefusal)) return null;
|
|
22148
|
+
return memgError(err.code, err.message, mergeRefusalHint(err.reason));
|
|
22149
|
+
}
|
|
22150
|
+
function mergeRefusalHint(reason) {
|
|
22151
|
+
switch (reason.kind) {
|
|
22152
|
+
case "prototype_key":
|
|
22153
|
+
return "Nothing was written. The package emits a config key named __proto__, constructor or prototype, which no legitimate tool config needs. Do not install it, and tell the author if you did not publish it.";
|
|
22154
|
+
case "malformed_file":
|
|
22155
|
+
return `Nothing was written. Fix or move aside the malformed ${reason.targetPath}, then run the install again. It is not overwritten blind because it may hold configuration from other tools and other packages.`;
|
|
22156
|
+
case "non_object_root":
|
|
22157
|
+
return `Nothing was written. ${reason.targetPath} parses, but its top level is not an object, so a config fragment cannot be merged into it. Fix its shape or move it aside, then run the install again.`;
|
|
22158
|
+
case "dangling_marker":
|
|
22159
|
+
return `Nothing was written. The file has a dangling <!-- kordis:${reason.fullSlug}:\u2026:start --> marker. Delete that line, or restore its <!-- kordis:${reason.fullSlug}:end --> line, then run the install again.`;
|
|
22160
|
+
}
|
|
22161
|
+
}
|
|
22162
|
+
|
|
21527
22163
|
// src/commands/registry/pull.ts
|
|
21528
22164
|
async function requireExecutableApproval(pkg, fullSlug, files2, approve2) {
|
|
21529
22165
|
if (!pkg.capabilities.executable) return;
|
|
@@ -21654,7 +22290,7 @@ async function doPullInstall(fullSlug, tool, installRoot, opts = {}) {
|
|
|
21654
22290
|
);
|
|
21655
22291
|
for (const f of translated.files) {
|
|
21656
22292
|
const abs = safeInstallPath(installRoot, f.path);
|
|
21657
|
-
await
|
|
22293
|
+
await mkdir15(dirname8(abs), { recursive: true });
|
|
21658
22294
|
await applyEmittedFile(abs, f, fullSlug, notes);
|
|
21659
22295
|
written.push(abs);
|
|
21660
22296
|
}
|
|
@@ -21810,7 +22446,7 @@ var TOOL_MARKERS = [
|
|
|
21810
22446
|
async function detectTool(root) {
|
|
21811
22447
|
for (const [path, tool] of TOOL_MARKERS) {
|
|
21812
22448
|
try {
|
|
21813
|
-
await
|
|
22449
|
+
await stat7(join24(root, path));
|
|
21814
22450
|
return tool;
|
|
21815
22451
|
} catch {
|
|
21816
22452
|
}
|
|
@@ -21826,7 +22462,7 @@ function safeInstallPath(installRoot, relPath) {
|
|
|
21826
22462
|
"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."
|
|
21827
22463
|
);
|
|
21828
22464
|
}
|
|
21829
|
-
const abs =
|
|
22465
|
+
const abs = join24(installRoot, relPath);
|
|
21830
22466
|
const resolvedRoot = resolve12(installRoot);
|
|
21831
22467
|
const resolvedAbs = resolve12(abs);
|
|
21832
22468
|
if (resolvedAbs !== resolvedRoot && !resolvedAbs.startsWith(resolvedRoot + sep3)) {
|
|
@@ -21842,9 +22478,9 @@ function safeInstallPath(installRoot, relPath) {
|
|
|
21842
22478
|
} catch {
|
|
21843
22479
|
return abs;
|
|
21844
22480
|
}
|
|
21845
|
-
let ancestor =
|
|
22481
|
+
let ancestor = dirname8(resolvedAbs);
|
|
21846
22482
|
while (!existsSync(ancestor)) {
|
|
21847
|
-
const parent =
|
|
22483
|
+
const parent = dirname8(ancestor);
|
|
21848
22484
|
if (parent === ancestor) break;
|
|
21849
22485
|
ancestor = parent;
|
|
21850
22486
|
}
|
|
@@ -21880,9 +22516,9 @@ function safeInstallPath(installRoot, relPath) {
|
|
|
21880
22516
|
}
|
|
21881
22517
|
async function writeEmittedFile(abs, content, encoding) {
|
|
21882
22518
|
if (encoding === "base64") {
|
|
21883
|
-
await
|
|
22519
|
+
await writeFile13(abs, Buffer.from(content, "base64"));
|
|
21884
22520
|
} else {
|
|
21885
|
-
await
|
|
22521
|
+
await writeFile13(abs, content, "utf8");
|
|
21886
22522
|
}
|
|
21887
22523
|
}
|
|
21888
22524
|
async function applyEmittedFile(abs, f, fullSlug, notes) {
|
|
@@ -21892,21 +22528,32 @@ async function applyEmittedFile(abs, f, fullSlug, notes) {
|
|
|
21892
22528
|
}
|
|
21893
22529
|
let existingRaw = null;
|
|
21894
22530
|
try {
|
|
21895
|
-
existingRaw = await
|
|
22531
|
+
existingRaw = await readFile24(abs, "utf8");
|
|
21896
22532
|
} catch (err) {
|
|
21897
22533
|
if (err.code !== "ENOENT") throw err;
|
|
21898
22534
|
}
|
|
21899
22535
|
if (f.format === "json" || f.format === "yaml" || f.format === "toml") {
|
|
21900
|
-
|
|
22536
|
+
let merged;
|
|
22537
|
+
try {
|
|
22538
|
+
merged = mergeStructuredFile(existingRaw, f.content, f.format, abs);
|
|
22539
|
+
} catch (err) {
|
|
22540
|
+
throw adaptMergeRefusal(err) ?? err;
|
|
22541
|
+
}
|
|
21901
22542
|
if (existingRaw !== null && existingRaw.trim() !== "") {
|
|
21902
22543
|
notes?.push(
|
|
21903
22544
|
`merged into ${f.path} \u2014 comments/formatting in the existing file are not preserved`
|
|
21904
22545
|
);
|
|
21905
22546
|
}
|
|
21906
|
-
await
|
|
22547
|
+
await writeFile13(abs, merged, "utf8");
|
|
21907
22548
|
return;
|
|
21908
22549
|
}
|
|
21909
|
-
|
|
22550
|
+
let fenced;
|
|
22551
|
+
try {
|
|
22552
|
+
fenced = mergeFencedFile(existingRaw, fullSlug, f.content);
|
|
22553
|
+
} catch (err) {
|
|
22554
|
+
throw adaptMergeRefusal(err) ?? err;
|
|
22555
|
+
}
|
|
22556
|
+
await writeFile13(abs, fenced, "utf8");
|
|
21910
22557
|
}
|
|
21911
22558
|
|
|
21912
22559
|
// src/commands/registry/update.ts
|
|
@@ -22409,12 +23056,14 @@ init_push();
|
|
|
22409
23056
|
|
|
22410
23057
|
// src/commands/recall.ts
|
|
22411
23058
|
init_src();
|
|
23059
|
+
init_exclude();
|
|
22412
23060
|
init_config();
|
|
22413
23061
|
import { Command as Command37 } from "commander";
|
|
22414
|
-
import { basename as
|
|
23062
|
+
import { basename as basename3, resolve as resolve13 } from "path";
|
|
22415
23063
|
init_index_build();
|
|
22416
23064
|
init_cloud_brain();
|
|
22417
23065
|
init_errors();
|
|
23066
|
+
init_held_back();
|
|
22418
23067
|
init_vault();
|
|
22419
23068
|
init_ui();
|
|
22420
23069
|
function recallCommand() {
|
|
@@ -22439,6 +23088,10 @@ function recallCommand() {
|
|
|
22439
23088
|
let unsearchedBrains = [];
|
|
22440
23089
|
let unsearchedLocal = [];
|
|
22441
23090
|
let searchedLocalName = null;
|
|
23091
|
+
let nothingToSearch = null;
|
|
23092
|
+
let alsoLeftOut = null;
|
|
23093
|
+
let namedIsPointer = false;
|
|
23094
|
+
let serverHint;
|
|
22442
23095
|
let isScaffolding;
|
|
22443
23096
|
let templatesWithheld = 0;
|
|
22444
23097
|
let spread = null;
|
|
@@ -22485,8 +23138,19 @@ function recallCommand() {
|
|
|
22485
23138
|
}));
|
|
22486
23139
|
spread = isScopeSpread(answered.scope_spread) ? answered.scope_spread : null;
|
|
22487
23140
|
templatesWithheld = Number.isInteger(answered.templates_withheld) && (answered.templates_withheld ?? 0) > 0 ? answered.templates_withheld : 0;
|
|
22488
|
-
if (rows.
|
|
22489
|
-
|
|
23141
|
+
if (rows.every(({ card }) => unfilledIds.has(card.id))) {
|
|
23142
|
+
const listed = await listCloudBrains(cloud.token, cloud.apiUrl, { retries: 0 }).catch(
|
|
23143
|
+
() => []
|
|
23144
|
+
);
|
|
23145
|
+
unsearchedBrains = listed.filter((b) => b.id !== cloud.brainId);
|
|
23146
|
+
if (listed.find((b) => b.id === cloud.brainId)?.note_count === 0) {
|
|
23147
|
+
nothingToSearch = { kind: "no_notes" };
|
|
23148
|
+
}
|
|
23149
|
+
if (opts.brain !== void 0) {
|
|
23150
|
+
const pointer = await hostedPointer().catch(() => null);
|
|
23151
|
+
namedIsPointer = pointer !== null && pointer.id === cloud.brainId && (pointer.signedIn || pointer.signInPending);
|
|
23152
|
+
}
|
|
23153
|
+
serverHint = typeof answered.hint === "string" && answered.hint.trim() !== "" ? answered.hint : void 0;
|
|
22490
23154
|
}
|
|
22491
23155
|
isScaffolding = (card) => unfilledIds.has(card.id);
|
|
22492
23156
|
reasons = /* @__PURE__ */ new Map();
|
|
@@ -22528,7 +23192,11 @@ function recallCommand() {
|
|
|
22528
23192
|
);
|
|
22529
23193
|
isScaffolding = (card) => scaffolding.has(byId.get(card.id)?.path ?? "");
|
|
22530
23194
|
const sides = new Set(local.conflicts.flatMap((conflict) => conflict.between));
|
|
22531
|
-
const capped2 = capTemplates(
|
|
23195
|
+
const capped2 = capTemplates(
|
|
23196
|
+
local.memory_cards,
|
|
23197
|
+
isScaffolding,
|
|
23198
|
+
(card) => sides.has(card.id)
|
|
23199
|
+
);
|
|
22532
23200
|
templatesWithheld = capped2.withheld.length;
|
|
22533
23201
|
result = { ...local, memory_cards: capped2.kept };
|
|
22534
23202
|
entries = built.entries;
|
|
@@ -22541,8 +23209,16 @@ function recallCommand() {
|
|
|
22541
23209
|
built.entries.map((e) => declaredScope(e.memory))
|
|
22542
23210
|
);
|
|
22543
23211
|
}
|
|
22544
|
-
if (result.memory_cards.
|
|
22545
|
-
|
|
23212
|
+
if (result.memory_cards.every((card) => isScaffolding(card))) {
|
|
23213
|
+
const leftOut = await adapter.scan().then((scan) => leftOutOf(heldBack(scan), built.denied)).catch(() => void 0);
|
|
23214
|
+
if (built.entries.length === 0) {
|
|
23215
|
+
if (leftOut !== void 0) {
|
|
23216
|
+
nothingToSearch = leftOut === null ? { kind: "no_notes" } : { kind: "all_left_out", leftOut };
|
|
23217
|
+
}
|
|
23218
|
+
} else {
|
|
23219
|
+
alsoLeftOut = leftOut ?? null;
|
|
23220
|
+
}
|
|
23221
|
+
searchedLocalName = basename3(resolve13(brain.root));
|
|
22546
23222
|
unsearchedLocal = await loadConfig().then(
|
|
22547
23223
|
(cfg) => cfg.brain.brains.filter((other) => resolve13(other.root) !== resolve13(brain.root)).map((other) => ({ root: other.root }))
|
|
22548
23224
|
).catch(() => []);
|
|
@@ -22570,6 +23246,32 @@ function recallCommand() {
|
|
|
22570
23246
|
);
|
|
22571
23247
|
reasons = result.conflicts.length > 0 ? await conflictReasons(brain.root, result.conflicts, byId) : /* @__PURE__ */ new Map();
|
|
22572
23248
|
}
|
|
23249
|
+
const reachedSomethingWritten = rows.some(({ card }) => !isScaffolding(card));
|
|
23250
|
+
const otherScript = reachedSomethingWritten ? null : entries ? unreachableScript(query, entries) : null;
|
|
23251
|
+
const advice = !reachedSomethingWritten ? emptyRecallAdvice({
|
|
23252
|
+
query,
|
|
23253
|
+
depth,
|
|
23254
|
+
scoped: Boolean(opts.scope),
|
|
23255
|
+
chosenWith: {
|
|
23256
|
+
...opts.brain === void 0 || namedIsPointer ? {} : { brain: answeredBy ?? opts.brain },
|
|
23257
|
+
...opts.local === true ? { local: true } : {},
|
|
23258
|
+
...opts.vault === void 0 ? {} : { vault: opts.vault }
|
|
23259
|
+
},
|
|
23260
|
+
nothingToSearch,
|
|
23261
|
+
alsoLeftOut,
|
|
23262
|
+
otherScript,
|
|
23263
|
+
unsearched: unsearchedBrains.length > 0 ? {
|
|
23264
|
+
searched: answeredBy ?? "",
|
|
23265
|
+
flag: "--brain",
|
|
23266
|
+
others: unsearchedBrains.map((other) => other.name)
|
|
23267
|
+
} : unsearchedLocal.length > 0 ? {
|
|
23268
|
+
searched: searchedLocalName ?? "",
|
|
23269
|
+
flag: "--vault",
|
|
23270
|
+
others: unsearchedLocal.map((other) => other.root)
|
|
23271
|
+
} : null,
|
|
23272
|
+
semanticHint: semantic.state === "skipped" ? semantic.hint : void 0,
|
|
23273
|
+
serverHint
|
|
23274
|
+
}) : null;
|
|
22573
23275
|
printDual({
|
|
22574
23276
|
json: {
|
|
22575
23277
|
query,
|
|
@@ -22608,20 +23310,29 @@ function recallCommand() {
|
|
|
22608
23310
|
// Counted, never silent: these matched and were left off because
|
|
22609
23311
|
// they are still exactly what `init` wrote.
|
|
22610
23312
|
...templatesWithheld > 0 ? { templates_withheld: templatesWithheld } : {},
|
|
22611
|
-
...spread === null ? {} : { scope_spread: spread }
|
|
23313
|
+
...spread === null ? {} : { scope_spread: spread },
|
|
23314
|
+
// Only when nothing the person wrote came back — no card, or only
|
|
23315
|
+
// untouched templates: the one next step the footer would have led
|
|
23316
|
+
// with. An agent reading `cards: []` alone cannot tell an empty
|
|
23317
|
+
// brain from a missed word from an unread brain, and one reading
|
|
23318
|
+
// template cards alone took them for the whole brain.
|
|
23319
|
+
...advice === null ? {} : { hint: advice.hint }
|
|
22612
23320
|
},
|
|
22613
23321
|
human: () => {
|
|
22614
23322
|
const brainHeading = answeredBy === null ? "" : ` \u2014 ${oneLine(answeredBy, WIDTH.title)}`;
|
|
22615
|
-
const
|
|
22616
|
-
const otherScript = reachedSomethingWritten ? null : entries ? unreachableScript(query, entries) : null;
|
|
22617
|
-
const sayWhy = () => {
|
|
23323
|
+
const sayWhy = (notice2) => {
|
|
22618
23324
|
printHuman(
|
|
22619
|
-
c.yellow(
|
|
22620
|
-
|
|
22621
|
-
|
|
23325
|
+
c.yellow(`\u26A0 ${notice2.headline}`) + c.gray(`
|
|
23326
|
+
${notice2.lines.map((line) => ` ${line}`).join("\n")}
|
|
23327
|
+
`)
|
|
23328
|
+
);
|
|
23329
|
+
};
|
|
23330
|
+
const sayOtherBrains = (otherBrains) => {
|
|
23331
|
+
printHuman(
|
|
23332
|
+
c.yellow(`\u26A0 ${otherBrains.headline}`) + c.gray(
|
|
22622
23333
|
`
|
|
22623
|
-
|
|
22624
|
-
|
|
23334
|
+
${otherBrains.commands.map((cmd) => ` ${cmd}`).join("\n")}
|
|
23335
|
+
${otherBrains.safe}
|
|
22625
23336
|
`
|
|
22626
23337
|
)
|
|
22627
23338
|
);
|
|
@@ -22634,60 +23345,18 @@ function recallCommand() {
|
|
|
22634
23345
|
`)
|
|
22635
23346
|
);
|
|
22636
23347
|
};
|
|
22637
|
-
if (rows.length === 0) {
|
|
23348
|
+
if (advice !== null && rows.length === 0) {
|
|
22638
23349
|
printHuman(`## recall: ${query}${brainHeading}
|
|
22639
23350
|
`);
|
|
22640
23351
|
printHuman("_Nothing matched._\n");
|
|
22641
23352
|
sayDegraded();
|
|
22642
|
-
if (
|
|
22643
|
-
|
|
23353
|
+
if (advice.leftOut !== null) sayWhy(advice.leftOut);
|
|
23354
|
+
if (advice.otherScript !== null) {
|
|
23355
|
+
sayWhy(advice.otherScript);
|
|
22644
23356
|
return;
|
|
22645
23357
|
}
|
|
22646
|
-
if (
|
|
22647
|
-
|
|
22648
|
-
c.yellow(
|
|
22649
|
-
`\u26A0 Only \`${oneLine(answeredBy ?? "", WIDTH.title)}\` was searched \u2014 this account has ${unsearchedBrains.length + 1} brains.`
|
|
22650
|
-
) + c.gray(
|
|
22651
|
-
`
|
|
22652
|
-
${unsearchedBrains.map(
|
|
22653
|
-
(brain) => ` memgineering recall ${JSON.stringify(query)} --brain ${JSON.stringify(brain.name)}`
|
|
22654
|
-
).join(
|
|
22655
|
-
"\n"
|
|
22656
|
-
)}
|
|
22657
|
-
Safe to run \u2014 reading another brain does not change which one this machine points at.
|
|
22658
|
-
`
|
|
22659
|
-
)
|
|
22660
|
-
);
|
|
22661
|
-
}
|
|
22662
|
-
if (unsearchedLocal.length > 0) {
|
|
22663
|
-
printHuman(
|
|
22664
|
-
c.yellow(
|
|
22665
|
-
`\u26A0 Only \`${oneLine(searchedLocalName ?? "", WIDTH.title)}\` was searched \u2014 ${unsearchedLocal.length + 1} brains are linked on this machine.`
|
|
22666
|
-
) + c.gray(
|
|
22667
|
-
`
|
|
22668
|
-
${unsearchedLocal.map(
|
|
22669
|
-
(other) => ` memgineering recall ${JSON.stringify(query)} --vault ${JSON.stringify(other.root)}`
|
|
22670
|
-
).join(
|
|
22671
|
-
"\n"
|
|
22672
|
-
)}
|
|
22673
|
-
Safe to run \u2014 reading another brain does not change which one this folder points at.
|
|
22674
|
-
`
|
|
22675
|
-
)
|
|
22676
|
-
);
|
|
22677
|
-
}
|
|
22678
|
-
printHuman(
|
|
22679
|
-
c.gray(
|
|
22680
|
-
// "Within this brain" is load-bearing when the warning above
|
|
22681
|
-
// has just said another brain went unsearched. The enumeration
|
|
22682
|
-
// reads as the whole surface — a recheck tester called this
|
|
22683
|
-
// the last line on screen and a false all-clear, sitting below
|
|
22684
|
-
// the one line that says otherwise.
|
|
22685
|
-
(depth === "exact" ? "Within this brain, `--depth exact` searches the card only \u2014 titles, aliases, entities, summaries and scope \u2014 try `--depth standard` to search note bodies too, or fewer words" : "Within this brain, titles, aliases, summaries and note bodies are all searched \u2014 try fewer or broader words") + // Only when the reader actually passed one. Telling somebody
|
|
22686
|
-
// to drop an argument they never supplied cost one tester a
|
|
22687
|
-
// whole command to `--help` to find out it was not their bug.
|
|
22688
|
-
(opts.scope ? ", or drop --scope." : ".")
|
|
22689
|
-
)
|
|
22690
|
-
);
|
|
23358
|
+
if (advice.otherBrains !== null) sayOtherBrains(advice.otherBrains);
|
|
23359
|
+
if (advice.withinThisBrain !== null) printHuman(c.gray(advice.withinThisBrain));
|
|
22691
23360
|
return;
|
|
22692
23361
|
}
|
|
22693
23362
|
printHuman(`## recall: ${query}${brainHeading} (${rows.length} cards)
|
|
@@ -22699,6 +23368,10 @@ ${unsearchedLocal.map(
|
|
|
22699
23368
|
"\n Every card below is still the template `init` wrote \u2014 nobody has answered\n these yet, so none of them is evidence of anything.\n `memgineering onboard` prints what to ask to fill them in.\n"
|
|
22700
23369
|
)
|
|
22701
23370
|
);
|
|
23371
|
+
if (advice !== null && advice.leftOut !== null) sayWhy(advice.leftOut);
|
|
23372
|
+
if (advice !== null && advice.otherScript === null && advice.otherBrains !== null) {
|
|
23373
|
+
sayOtherBrains(advice.otherBrains);
|
|
23374
|
+
}
|
|
22702
23375
|
}
|
|
22703
23376
|
const templateCount = rows.filter(({ card }) => isScaffolding(card)).length;
|
|
22704
23377
|
if (reachedSomethingWritten && templateCount > 0) {
|
|
@@ -22728,7 +23401,7 @@ ${unsearchedLocal.map(
|
|
|
22728
23401
|
)
|
|
22729
23402
|
);
|
|
22730
23403
|
}
|
|
22731
|
-
if (otherScript) sayWhy();
|
|
23404
|
+
if (otherScript) sayWhy(otherScriptNotice(otherScript));
|
|
22732
23405
|
const glossOn = rows.findIndex(
|
|
22733
23406
|
({ card }) => card.confidence === "stated" && !isScaffolding(card)
|
|
22734
23407
|
);
|
|
@@ -22896,6 +23569,114 @@ function ago2(days) {
|
|
|
22896
23569
|
if (days < 365) return `${Math.floor(days / 30)} months ago`;
|
|
22897
23570
|
return `${Math.floor(days / 365)} years ago`;
|
|
22898
23571
|
}
|
|
23572
|
+
function emptyRecallAdvice(facts) {
|
|
23573
|
+
const otherScript = facts.otherScript === null ? null : otherScriptNotice(facts.otherScript);
|
|
23574
|
+
const otherBrains = facts.unsearched === null || facts.unsearched.others.length === 0 ? null : unsearchedNotice(facts.query, facts.unsearched);
|
|
23575
|
+
const nothing = facts.nothingToSearch;
|
|
23576
|
+
const leftOut = nothing?.kind === "all_left_out" ? leftOutNotice(nothing.leftOut, facts.chosenWith, "all") : facts.alsoLeftOut === null || notesIn(facts.alsoLeftOut) === 0 ? null : leftOutNotice(facts.alsoLeftOut, facts.chosenWith, "some");
|
|
23577
|
+
const within = (
|
|
23578
|
+
// "Within this brain" is load-bearing when the warning above it has just
|
|
23579
|
+
// said another brain went unsearched. The enumeration reads as the whole
|
|
23580
|
+
// surface — a recheck tester called this the last line on screen and a
|
|
23581
|
+
// false all-clear, sitting below the one line that says otherwise. Below
|
|
23582
|
+
// a count of held-back notes it would be the same false all-clear.
|
|
23583
|
+
(leftOut === null ? "Within this brain" : "Within the rest of this brain") + (facts.depth === "exact" ? ", `--depth exact` searches the card only \u2014 titles, aliases, entities, summaries and scope \u2014 try `--depth standard` to search note bodies too, or fewer words" : ", titles, aliases, summaries and note bodies are all searched \u2014 try fewer or broader words") + // Only when the reader actually passed one. Telling somebody to drop an
|
|
23584
|
+
// argument they never supplied cost one tester a whole command to `--help`
|
|
23585
|
+
// to find out it was not their bug.
|
|
23586
|
+
(facts.scoped ? ", or drop --scope." : ".")
|
|
23587
|
+
);
|
|
23588
|
+
const hint = (() => {
|
|
23589
|
+
if (nothing !== null) return nothingToSearchHint(nothing, facts.chosenWith);
|
|
23590
|
+
const next = (() => {
|
|
23591
|
+
if (otherScript !== null) return [otherScript.headline, ...otherScript.lines].join(" ");
|
|
23592
|
+
if (otherBrains !== null) {
|
|
23593
|
+
const runnable = otherBrains.commands.map((cmd) => `\`${cmd}\``).join(", ");
|
|
23594
|
+
return `${otherBrains.headline} To search ${otherBrains.commands.length === 1 ? "the other one" : "the others"}: ${runnable}. ${otherBrains.safe}`;
|
|
23595
|
+
}
|
|
23596
|
+
if (facts.semanticHint !== void 0) {
|
|
23597
|
+
return `Meaning was not searched \u2014 words only: ${facts.semanticHint}`;
|
|
23598
|
+
}
|
|
23599
|
+
if (facts.serverHint !== void 0) return facts.serverHint;
|
|
23600
|
+
return within;
|
|
23601
|
+
})();
|
|
23602
|
+
return leftOut === null ? next : `${noticeText(leftOut)} ${next}`;
|
|
23603
|
+
})();
|
|
23604
|
+
return {
|
|
23605
|
+
hint,
|
|
23606
|
+
leftOut,
|
|
23607
|
+
otherScript,
|
|
23608
|
+
otherBrains,
|
|
23609
|
+
// Nothing was searched, so no wording reaches anything.
|
|
23610
|
+
withinThisBrain: nothing?.kind === "all_left_out" ? null : within
|
|
23611
|
+
};
|
|
23612
|
+
}
|
|
23613
|
+
function noticeText(notice2) {
|
|
23614
|
+
return [notice2.headline, ...notice2.lines].join(" ");
|
|
23615
|
+
}
|
|
23616
|
+
function otherScriptNotice(script) {
|
|
23617
|
+
return {
|
|
23618
|
+
headline: `This brain's notes are mostly written in ${script}, and this query is not.`,
|
|
23619
|
+
lines: [
|
|
23620
|
+
"Search matches text, not meaning, so no rewording in another language will reach them.",
|
|
23621
|
+
`Ask again in ${script}, or run \`memgineering resurface\` to see what is here.`
|
|
23622
|
+
]
|
|
23623
|
+
};
|
|
23624
|
+
}
|
|
23625
|
+
function unsearchedNotice(query, unsearched) {
|
|
23626
|
+
const total = unsearched.others.length + 1;
|
|
23627
|
+
const hosted = unsearched.flag === "--brain";
|
|
23628
|
+
return {
|
|
23629
|
+
headline: `Only \`${oneLine(unsearched.searched, WIDTH.title)}\` was searched \u2014 ${hosted ? `this account has ${total} brains` : `${total} brains are linked on this machine`}.`,
|
|
23630
|
+
commands: unsearched.others.map(
|
|
23631
|
+
(other) => `memgineering recall ${JSON.stringify(query)} ${unsearched.flag} ${JSON.stringify(other)}`
|
|
23632
|
+
),
|
|
23633
|
+
safe: `Safe to run \u2014 reading another brain does not change which one this ${hosted ? "machine" : "folder"} points at.`
|
|
23634
|
+
};
|
|
23635
|
+
}
|
|
23636
|
+
function nothingToSearchHint(nothing, chosenWith) {
|
|
23637
|
+
return nothing.kind === "no_notes" ? emptyBrainHint(chosenWith) : noticeText(leftOutNotice(nothing.leftOut, chosenWith, "all"));
|
|
23638
|
+
}
|
|
23639
|
+
function folderFlags(chosenWith) {
|
|
23640
|
+
return (chosenWith.local === true ? " --local" : "") + (chosenWith.vault === void 0 ? "" : ` --vault ${JSON.stringify(chosenWith.vault)}`);
|
|
23641
|
+
}
|
|
23642
|
+
function emptyBrainHint(chosenWith) {
|
|
23643
|
+
if (chosenWith.brain !== void 0) {
|
|
23644
|
+
return `This brain has no notes yet. \`memgineering remember\` writes to the brain this machine points at, not to this one \u2014 \`memgineering use --brain ${JSON.stringify(chosenWith.brain)}\` points it here first, if this is where the user wants the note.`;
|
|
23645
|
+
}
|
|
23646
|
+
return `This brain has no notes yet; \`memgineering remember "<what to keep>" --reason "<why>"${folderFlags(chosenWith)}\` writes the first one.`;
|
|
23647
|
+
}
|
|
23648
|
+
function leftOutNotice(leftOut, chosenWith, share) {
|
|
23649
|
+
const notes = (n) => `${n} note${n === 1 ? "" : "s"}`;
|
|
23650
|
+
const parts = [
|
|
23651
|
+
leftOut.byRule > 0 ? `${notes(leftOut.byRule)} excluded by your rules` : null,
|
|
23652
|
+
leftOut.listed > 0 ? `${notes(leftOut.listed)} listed in .memgdeny` : null,
|
|
23653
|
+
leftOut.byName > 0 ? `${notes(leftOut.byName)} held back because the name looks private` : null,
|
|
23654
|
+
leftOut.byContent > 0 ? `${notes(leftOut.byContent)} refused because the content looks like a credential` : null,
|
|
23655
|
+
leftOut.symlinked > 0 ? `${notes(leftOut.symlinked)} behind a symlink, which is never followed` : null,
|
|
23656
|
+
leftOut.otherSymlinks > 0 ? `${leftOut.otherSymlinks} other symlink${leftOut.otherSymlinks === 1 ? "" : "s"}, never followed, so a folder behind one was not looked inside` : null,
|
|
23657
|
+
leftOut.folders > 0 ? `${leftOut.folders} excluded folder${leftOut.folders === 1 ? "" : "s"}, not looked inside` : null
|
|
23658
|
+
].filter((part) => part !== null);
|
|
23659
|
+
return {
|
|
23660
|
+
headline: `${share === "all" ? "Nothing in this brain could be searched" : "Not every note in this brain was searched"}: ${parts.join(", ")}.`,
|
|
23661
|
+
lines: [`\`memgineering reindex${folderFlags(chosenWith)}\` names each one and why.`]
|
|
23662
|
+
};
|
|
23663
|
+
}
|
|
23664
|
+
function notesIn(leftOut) {
|
|
23665
|
+
return leftOut.byRule + leftOut.listed + leftOut.byName + leftOut.byContent + leftOut.symlinked;
|
|
23666
|
+
}
|
|
23667
|
+
function leftOutOf(held, denied) {
|
|
23668
|
+
const refused = (reason) => denied.filter((d) => d.reason === reason).length;
|
|
23669
|
+
const leftOut = {
|
|
23670
|
+
byRule: held.byRule.filter((path) => !isExcluded(path, false)).length + refused("excluded"),
|
|
23671
|
+
listed: held.explicitly.length + refused("explicit"),
|
|
23672
|
+
byName: held.byName.length + refused("name"),
|
|
23673
|
+
byContent: refused("content"),
|
|
23674
|
+
symlinked: held.symlinked.length,
|
|
23675
|
+
otherSymlinks: held.otherSymlinks.filter((path) => !isExcluded(path, false)).length,
|
|
23676
|
+
folders: held.folders.filter((dir) => !isExcluded(dir, true)).length
|
|
23677
|
+
};
|
|
23678
|
+
return Object.values(leftOut).some((n) => n > 0) ? leftOut : null;
|
|
23679
|
+
}
|
|
22899
23680
|
function unreachableScript(query, entries) {
|
|
22900
23681
|
const queryScripts = scriptsIn(query);
|
|
22901
23682
|
if (queryScripts.size === 0) return null;
|
|
@@ -22957,15 +23738,17 @@ function isScopeSpread(value) {
|
|
|
22957
23738
|
}
|
|
22958
23739
|
|
|
22959
23740
|
// src/commands/resurface.ts
|
|
23741
|
+
init_setup();
|
|
23742
|
+
init_ui();
|
|
22960
23743
|
init_deadline();
|
|
22961
23744
|
init_src();
|
|
22962
|
-
import { Command as
|
|
23745
|
+
import { Command as Command39 } from "commander";
|
|
22963
23746
|
init_index_build();
|
|
22964
23747
|
init_cloud_brain();
|
|
22965
23748
|
init_vault();
|
|
22966
23749
|
init_ui();
|
|
22967
23750
|
function resurfaceCommand() {
|
|
22968
|
-
return new
|
|
23751
|
+
return new Command39("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(
|
|
22969
23752
|
"--brain <name>",
|
|
22970
23753
|
"read one existing hosted brain by name or id, without changing the pointer"
|
|
22971
23754
|
).option(
|
|
@@ -22976,6 +23759,11 @@ function resurfaceCommand() {
|
|
|
22976
23759
|
async (opts) => {
|
|
22977
23760
|
const bounded = (work) => opts.deadline !== void 0 && Number.isFinite(opts.deadline) ? withDeadline(opts.deadline, work) : work();
|
|
22978
23761
|
await bounded(async () => {
|
|
23762
|
+
await refreshOwnHooksAndTell(
|
|
23763
|
+
(text) => (isJsonMode() ? process.stderr : process.stdout).write(`${text}
|
|
23764
|
+
|
|
23765
|
+
`)
|
|
23766
|
+
);
|
|
22979
23767
|
const contextDir = opts.contextDir ?? process.cwd();
|
|
22980
23768
|
const where = await resolveTarget({
|
|
22981
23769
|
...opts.brain === void 0 ? {} : { brain: opts.brain },
|
|
@@ -23187,11 +23975,11 @@ rank by project yet, so that is not one of the signals above.`
|
|
|
23187
23975
|
}
|
|
23188
23976
|
|
|
23189
23977
|
// src/commands/rules.ts
|
|
23190
|
-
import { Command as
|
|
23978
|
+
import { Command as Command40 } from "commander";
|
|
23191
23979
|
init_cloud_brain();
|
|
23192
23980
|
init_ui();
|
|
23193
23981
|
function rulesCommand() {
|
|
23194
|
-
return new
|
|
23982
|
+
return new Command40("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(
|
|
23195
23983
|
"--brain <name>",
|
|
23196
23984
|
"read one existing hosted brain by name or id, without changing the pointer"
|
|
23197
23985
|
).action(async (opts) => {
|
|
@@ -23358,16 +24146,16 @@ init_next_step();
|
|
|
23358
24146
|
init_provenance();
|
|
23359
24147
|
import { spawn as spawn4 } from "child_process";
|
|
23360
24148
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
23361
|
-
import { mkdir as
|
|
23362
|
-
import { join as
|
|
24149
|
+
import { mkdir as mkdir19, open, readFile as readFile29, rename as rename5, rm as rm8, stat as stat9, unlink as unlink6, writeFile as writeFile16 } from "fs/promises";
|
|
24150
|
+
import { join as join31 } from "path";
|
|
23363
24151
|
import { Command as Command41 } from "commander";
|
|
23364
24152
|
|
|
23365
24153
|
// src/lib/update-check.ts
|
|
23366
24154
|
init_atomic();
|
|
23367
24155
|
init_brand();
|
|
23368
24156
|
import { execFile as execFile2 } from "child_process";
|
|
23369
|
-
import { mkdir as
|
|
23370
|
-
import { join as
|
|
24157
|
+
import { mkdir as mkdir18, readFile as readFile28, writeFile as writeFile15 } from "fs/promises";
|
|
24158
|
+
import { join as join30 } from "path";
|
|
23371
24159
|
import { promisify as promisify2 } from "util";
|
|
23372
24160
|
import semver4 from "semver";
|
|
23373
24161
|
import { z as z33 } from "zod";
|
|
@@ -23448,11 +24236,11 @@ var UpdateStateSchema = z33.object({
|
|
|
23448
24236
|
message: z33.string().nullable()
|
|
23449
24237
|
});
|
|
23450
24238
|
function statePath() {
|
|
23451
|
-
return
|
|
24239
|
+
return join30(brandHome(), "update-state.json");
|
|
23452
24240
|
}
|
|
23453
24241
|
async function readUpdateState() {
|
|
23454
24242
|
try {
|
|
23455
|
-
const parsed = UpdateStateSchema.safeParse(JSON.parse(await
|
|
24243
|
+
const parsed = UpdateStateSchema.safeParse(JSON.parse(await readFile28(statePath(), "utf8")));
|
|
23456
24244
|
return parsed.success ? parsed.data : null;
|
|
23457
24245
|
} catch {
|
|
23458
24246
|
return null;
|
|
@@ -23460,7 +24248,7 @@ async function readUpdateState() {
|
|
|
23460
24248
|
}
|
|
23461
24249
|
async function writeUpdateState(state) {
|
|
23462
24250
|
try {
|
|
23463
|
-
await
|
|
24251
|
+
await mkdir18(brandHome(), { recursive: true });
|
|
23464
24252
|
await writeThenRename(statePath(), `${JSON.stringify(state, null, 2)}
|
|
23465
24253
|
`);
|
|
23466
24254
|
} catch {
|
|
@@ -23572,7 +24360,7 @@ async function runUpdate(opts, inherited) {
|
|
|
23572
24360
|
}
|
|
23573
24361
|
const held = await holdUpdateSlot(inherited);
|
|
23574
24362
|
if (held === null && await blockedBy() === "stuck") {
|
|
23575
|
-
const lock =
|
|
24363
|
+
const lock = join31(brandHome(), RECLAIM_LOCK);
|
|
23576
24364
|
const hint = `A previous update did not finish, and clearing what it left is blocked by ${lock}. If no memgineering update is running, delete that folder and run \`memgineering update\` again.`;
|
|
23577
24365
|
printDual({
|
|
23578
24366
|
json: { ...status, provenance: provenance.kind, updated: false, reason: "update_claim_stuck", hint },
|
|
@@ -23687,14 +24475,14 @@ function newClaimToken() {
|
|
|
23687
24475
|
}
|
|
23688
24476
|
async function claimOwner() {
|
|
23689
24477
|
try {
|
|
23690
|
-
return (await
|
|
24478
|
+
return (await readFile29(join31(brandHome(), "update-running"), "utf8")).trim() || null;
|
|
23691
24479
|
} catch {
|
|
23692
24480
|
return null;
|
|
23693
24481
|
}
|
|
23694
24482
|
}
|
|
23695
24483
|
async function releaseUpdateSlot(token) {
|
|
23696
24484
|
if (await claimOwner() !== token) return;
|
|
23697
|
-
await
|
|
24485
|
+
await unlink6(join31(brandHome(), "update-running")).catch(() => void 0);
|
|
23698
24486
|
}
|
|
23699
24487
|
async function holdUpdateSlot(inherited) {
|
|
23700
24488
|
if (inherited !== void 0 && await claimOwner() === inherited) return inherited;
|
|
@@ -23706,9 +24494,9 @@ async function holdUpdateSlot(inherited) {
|
|
|
23706
24494
|
var HOOK_VERBS = /* @__PURE__ */ new Set(["guard", "resurface"]);
|
|
23707
24495
|
var UPDATE_CLAIM_MS = 15 * 6e4;
|
|
23708
24496
|
async function claimUpdateSlot(token = newClaimToken(), hooks = {}) {
|
|
23709
|
-
const path =
|
|
24497
|
+
const path = join31(brandHome(), "update-running");
|
|
23710
24498
|
try {
|
|
23711
|
-
await
|
|
24499
|
+
await mkdir19(brandHome(), { recursive: true });
|
|
23712
24500
|
const handle = await open(path, "wx");
|
|
23713
24501
|
await handle.writeFile(`${token}
|
|
23714
24502
|
`);
|
|
@@ -23718,12 +24506,12 @@ async function claimUpdateSlot(token = newClaimToken(), hooks = {}) {
|
|
|
23718
24506
|
if (err?.code !== "EEXIST") return false;
|
|
23719
24507
|
}
|
|
23720
24508
|
try {
|
|
23721
|
-
if (Date.now() - (await
|
|
24509
|
+
if (Date.now() - (await stat9(path)).mtimeMs >= UPDATE_CLAIM_MS) {
|
|
23722
24510
|
await hooks.afterObserve?.();
|
|
23723
24511
|
await withReclaimLock(async () => {
|
|
23724
|
-
if (Date.now() - (await
|
|
24512
|
+
if (Date.now() - (await stat9(path)).mtimeMs < UPDATE_CLAIM_MS) return;
|
|
23725
24513
|
await hooks.afterRecheck?.();
|
|
23726
|
-
await
|
|
24514
|
+
await unlink6(path);
|
|
23727
24515
|
});
|
|
23728
24516
|
}
|
|
23729
24517
|
} catch {
|
|
@@ -23734,13 +24522,13 @@ var RECLAIM_LOCK = "update-running.reclaim";
|
|
|
23734
24522
|
var RECLAIM_LOCK_MAX_MS = 10 * 6e4;
|
|
23735
24523
|
async function withReclaimLock(fn) {
|
|
23736
24524
|
const home = brandHome();
|
|
23737
|
-
const lock =
|
|
23738
|
-
const staging =
|
|
23739
|
-
await
|
|
23740
|
-
await
|
|
24525
|
+
const lock = join31(home, RECLAIM_LOCK);
|
|
24526
|
+
const staging = join31(home, `${RECLAIM_LOCK}.${process.pid}.${randomBytes2(4).toString("hex")}`);
|
|
24527
|
+
await mkdir19(staging, { recursive: true });
|
|
24528
|
+
await writeFile16(join31(staging, "owner"), `${process.pid} ${Date.now()}
|
|
23741
24529
|
`);
|
|
23742
24530
|
try {
|
|
23743
|
-
await
|
|
24531
|
+
await rename5(staging, lock);
|
|
23744
24532
|
} catch {
|
|
23745
24533
|
await rm8(staging, { recursive: true, force: true }).catch(() => void 0);
|
|
23746
24534
|
await breakAbandonedLock(lock);
|
|
@@ -23750,18 +24538,18 @@ async function withReclaimLock(fn) {
|
|
|
23750
24538
|
return await fn();
|
|
23751
24539
|
} finally {
|
|
23752
24540
|
const aside = `${lock}.done.${randomBytes2(4).toString("hex")}`;
|
|
23753
|
-
await
|
|
24541
|
+
await rename5(lock, aside).then(() => rm8(aside, { recursive: true, force: true })).catch(() => void 0);
|
|
23754
24542
|
}
|
|
23755
24543
|
}
|
|
23756
24544
|
async function breakAbandonedLock(lock) {
|
|
23757
24545
|
try {
|
|
23758
|
-
const seen = (await
|
|
24546
|
+
const seen = (await readFile29(join31(lock, "owner"), "utf8")).trim();
|
|
23759
24547
|
if (!lockAbandoned(seen)) return;
|
|
23760
24548
|
const aside = `${lock}.dead.${randomBytes2(4).toString("hex")}`;
|
|
23761
|
-
await
|
|
23762
|
-
const moved = (await
|
|
24549
|
+
await rename5(lock, aside);
|
|
24550
|
+
const moved = (await readFile29(join31(aside, "owner"), "utf8").catch(() => "")).trim();
|
|
23763
24551
|
if (moved !== seen) {
|
|
23764
|
-
await
|
|
24552
|
+
await rename5(aside, lock).catch(() => void 0);
|
|
23765
24553
|
return;
|
|
23766
24554
|
}
|
|
23767
24555
|
await rm8(aside, { recursive: true, force: true });
|
|
@@ -23783,9 +24571,9 @@ function lockAbandoned(owner, now = Date.now()) {
|
|
|
23783
24571
|
}
|
|
23784
24572
|
async function blockedBy() {
|
|
23785
24573
|
try {
|
|
23786
|
-
const old = Date.now() - (await
|
|
24574
|
+
const old = Date.now() - (await stat9(join31(brandHome(), "update-running"))).mtimeMs >= UPDATE_CLAIM_MS;
|
|
23787
24575
|
if (old) {
|
|
23788
|
-
await
|
|
24576
|
+
await stat9(join31(brandHome(), RECLAIM_LOCK));
|
|
23789
24577
|
return "stuck";
|
|
23790
24578
|
}
|
|
23791
24579
|
} catch {
|
|
@@ -23817,8 +24605,8 @@ function reportBelowMinimum(below) {
|
|
|
23817
24605
|
|
|
23818
24606
|
// src/commands/undo.ts
|
|
23819
24607
|
init_src();
|
|
23820
|
-
import { readFile as
|
|
23821
|
-
import { join as
|
|
24608
|
+
import { readFile as readFile30, rm as rm9 } from "fs/promises";
|
|
24609
|
+
import { join as join32 } from "path";
|
|
23822
24610
|
import { Command as Command42 } from "commander";
|
|
23823
24611
|
init_atomic();
|
|
23824
24612
|
init_errors();
|
|
@@ -23915,8 +24703,8 @@ ${matches.slice(0, 8).map((id) => ` ${id}`).join("\n")}${matches.length > 8 ? "
|
|
|
23915
24703
|
);
|
|
23916
24704
|
}
|
|
23917
24705
|
const record = await withWriteLock(brain.root, async () => {
|
|
23918
|
-
const absolute =
|
|
23919
|
-
const current = await
|
|
24706
|
+
const absolute = join32(brain.root, target.target);
|
|
24707
|
+
const current = await readFile30(absolute, "utf8").catch(() => null);
|
|
23920
24708
|
const actualHash = current === null ? null : hashContent(current);
|
|
23921
24709
|
if (actualHash !== target.hash_after) {
|
|
23922
24710
|
throw memgError(
|
|
@@ -23978,7 +24766,7 @@ function minutesSince(iso) {
|
|
|
23978
24766
|
return Math.max(0, Math.floor((Date.now() - at) / 6e4));
|
|
23979
24767
|
}
|
|
23980
24768
|
async function reverse(brain, op, current) {
|
|
23981
|
-
const absolute =
|
|
24769
|
+
const absolute = join32(brain.root, op.target);
|
|
23982
24770
|
if (op.verb === "remember") {
|
|
23983
24771
|
if (current === null) {
|
|
23984
24772
|
throw memgError(
|
|
@@ -24035,7 +24823,7 @@ async function conflictHint(vaultRoot, op) {
|
|
|
24035
24823
|
Read what it says now, then decide what should stay.`;
|
|
24036
24824
|
const snapshot = await readSnapshot(vaultRoot, op.op_id);
|
|
24037
24825
|
return snapshot === null ? base2 : `${base2}
|
|
24038
|
-
The text from before that change is still on this machine: ${
|
|
24826
|
+
The text from before that change is still on this machine: ${join32(snapshotDir(vaultRoot), `${op.op_id}.txt`)}`;
|
|
24039
24827
|
}
|
|
24040
24828
|
|
|
24041
24829
|
// src/commands/brains.ts
|
|
@@ -24720,6 +25508,10 @@ async function run3(argv) {
|
|
|
24720
25508
|
});
|
|
24721
25509
|
await program.parseAsync(cleaned);
|
|
24722
25510
|
if (ran !== "update") await backgroundUpdateNotice(ran === void 0 ? {} : { verb: ran });
|
|
25511
|
+
if (ran !== void 0 && !["guard", "resurface", "setup"].includes(ran)) {
|
|
25512
|
+
await refreshOwnHooksAndTell((text) => process.stderr.write(`${text}
|
|
25513
|
+
`));
|
|
25514
|
+
}
|
|
24723
25515
|
}
|
|
24724
25516
|
async function handleError(err) {
|
|
24725
25517
|
const failure = adaptRegistryError(err) ?? err;
|