@ametyst/cli 0.3.0 → 0.3.5
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/dist/index.js +1402 -1019
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -112016,12 +112016,17 @@ var init_core = __esm({
|
|
|
112016
112016
|
// src/commands/init-claim.ts
|
|
112017
112017
|
var init_claim_exports = {};
|
|
112018
112018
|
__export(init_claim_exports, {
|
|
112019
|
+
CLAIM_HOT_RELOAD_NOTICE: () => CLAIM_HOT_RELOAD_NOTICE,
|
|
112019
112020
|
WRAP_KEY_BYTES: () => WRAP_KEY_BYTES,
|
|
112021
|
+
backupVaultFile: () => backupVaultFile,
|
|
112020
112022
|
claimErrorMessage: () => claimErrorMessage,
|
|
112021
112023
|
decryptBootstrapBlob: () => decryptBootstrapBlob,
|
|
112022
112024
|
exchangeClaimToken: () => exchangeClaimToken,
|
|
112023
|
-
parseClaimArgument: () => parseClaimArgument
|
|
112025
|
+
parseClaimArgument: () => parseClaimArgument,
|
|
112026
|
+
vaultBackupPath: () => vaultBackupPath,
|
|
112027
|
+
vaultBackupSuffix: () => vaultBackupSuffix
|
|
112024
112028
|
});
|
|
112029
|
+
import { chmodSync as chmodSync3, constants as fsConstants, copyFileSync as copyFileSync3, existsSync as existsSync7 } from "fs";
|
|
112025
112030
|
import { createDecipheriv as createDecipheriv2 } from "crypto";
|
|
112026
112031
|
function claimErrorMessage(failure) {
|
|
112027
112032
|
switch (failure.kind) {
|
|
@@ -112157,13 +112162,32 @@ function decryptBootstrapBlob(wrapKey, blob) {
|
|
|
112157
112162
|
return null;
|
|
112158
112163
|
}
|
|
112159
112164
|
}
|
|
112160
|
-
|
|
112165
|
+
function vaultBackupSuffix(at) {
|
|
112166
|
+
return `${at.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z")}`;
|
|
112167
|
+
}
|
|
112168
|
+
function vaultBackupPath(vaultPath, at) {
|
|
112169
|
+
return `${vaultPath}.bak-${vaultBackupSuffix(at)}`;
|
|
112170
|
+
}
|
|
112171
|
+
function backupVaultFile(vaultPath, at = /* @__PURE__ */ new Date()) {
|
|
112172
|
+
if (!existsSync7(vaultPath)) return null;
|
|
112173
|
+
const target = vaultBackupPath(vaultPath, at);
|
|
112174
|
+
try {
|
|
112175
|
+
copyFileSync3(vaultPath, target, fsConstants.COPYFILE_EXCL);
|
|
112176
|
+
} catch (e) {
|
|
112177
|
+
if (e?.code === "EEXIST") return target;
|
|
112178
|
+
throw e;
|
|
112179
|
+
}
|
|
112180
|
+
chmodSync3(target, 384);
|
|
112181
|
+
return target;
|
|
112182
|
+
}
|
|
112183
|
+
var WRAP_KEY_BYTES, BASE64URL, CLAIM_HOT_RELOAD_NOTICE;
|
|
112161
112184
|
var init_init_claim = __esm({
|
|
112162
112185
|
"src/commands/init-claim.ts"() {
|
|
112163
112186
|
"use strict";
|
|
112164
112187
|
init_esm_shims();
|
|
112165
112188
|
WRAP_KEY_BYTES = 32;
|
|
112166
112189
|
BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
112190
|
+
CLAIM_HOT_RELOAD_NOTICE = "Running MCP servers pick up the new identity on their next call. If an agent host still shows the old workspace, restart it fully (Claude desktop: Cmd+Q).";
|
|
112167
112191
|
}
|
|
112168
112192
|
});
|
|
112169
112193
|
|
|
@@ -112173,12 +112197,12 @@ var init_version5 = __esm({
|
|
|
112173
112197
|
"src/version.ts"() {
|
|
112174
112198
|
"use strict";
|
|
112175
112199
|
init_esm_shims();
|
|
112176
|
-
CLI_VERSION = true ? "0.3.
|
|
112200
|
+
CLI_VERSION = true ? "0.3.5" : "0.0.0-dev";
|
|
112177
112201
|
}
|
|
112178
112202
|
});
|
|
112179
112203
|
|
|
112180
112204
|
// src/connections/catalog.ts
|
|
112181
|
-
import { existsSync as
|
|
112205
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
112182
112206
|
import { join as join6 } from "path";
|
|
112183
112207
|
function declaredVerbsHeader() {
|
|
112184
112208
|
return SUPPORTED_HTTP_METHODS.join(",");
|
|
@@ -112435,7 +112459,7 @@ function parseConnectorsResponseDetailed(body, onWarn = warnToStderr) {
|
|
|
112435
112459
|
function readCachedCatalogStatus() {
|
|
112436
112460
|
let raw;
|
|
112437
112461
|
try {
|
|
112438
|
-
if (!
|
|
112462
|
+
if (!existsSync8(CONNECTOR_CACHE_PATH)) return { state: "absent" };
|
|
112439
112463
|
raw = JSON.parse(readFileSync7(CONNECTOR_CACHE_PATH, "utf-8"));
|
|
112440
112464
|
} catch (e) {
|
|
112441
112465
|
return { state: "unreadable", reason: e instanceof Error ? e.message : String(e) };
|
|
@@ -112472,7 +112496,7 @@ function readMirrorRowsFromDisk() {
|
|
|
112472
112496
|
}
|
|
112473
112497
|
function readMirrorContainer() {
|
|
112474
112498
|
try {
|
|
112475
|
-
if (!
|
|
112499
|
+
if (!existsSync8(CONNECTOR_CACHE_PATH)) return { state: "absent" };
|
|
112476
112500
|
const raw = JSON.parse(readFileSync7(CONNECTOR_CACHE_PATH, "utf-8"));
|
|
112477
112501
|
if (!isRecord(raw)) {
|
|
112478
112502
|
return { state: "unreadable", reason: "the mirror's top level is not an object" };
|
|
@@ -112487,7 +112511,7 @@ function readMirrorContainer() {
|
|
|
112487
112511
|
}
|
|
112488
112512
|
function readMirrorFetchedAt() {
|
|
112489
112513
|
try {
|
|
112490
|
-
if (!
|
|
112514
|
+
if (!existsSync8(CONNECTOR_CACHE_PATH)) return "";
|
|
112491
112515
|
const raw = JSON.parse(readFileSync7(CONNECTOR_CACHE_PATH, "utf-8"));
|
|
112492
112516
|
if (!isRecord(raw)) return "";
|
|
112493
112517
|
return typeof raw.fetchedAt === "string" ? raw.fetchedAt : "";
|
|
@@ -113477,7 +113501,7 @@ var init_call2 = __esm({
|
|
|
113477
113501
|
});
|
|
113478
113502
|
|
|
113479
113503
|
// src/connections/engine-store.ts
|
|
113480
|
-
import { chmodSync as
|
|
113504
|
+
import { chmodSync as chmodSync4, closeSync as closeSync2, existsSync as existsSync11, mkdirSync as mkdirSync7, openSync as openSync2 } from "fs";
|
|
113481
113505
|
import { dirname as dirname7, join as join16 } from "path";
|
|
113482
113506
|
function connectionsDbPath() {
|
|
113483
113507
|
return join16(AMETYST_DIR, CONNECTIONS_DB_FILENAME);
|
|
@@ -113485,8 +113509,8 @@ function connectionsDbPath() {
|
|
|
113485
113509
|
function ensureOwnerOnlyFile(path2) {
|
|
113486
113510
|
if (path2 === IN_MEMORY_DB) return;
|
|
113487
113511
|
mkdirSync7(dirname7(path2), { recursive: true, mode: DIR_MODE });
|
|
113488
|
-
if (!
|
|
113489
|
-
|
|
113512
|
+
if (!existsSync11(path2)) closeSync2(openSync2(path2, "a", CONNECTIONS_DB_FILE_MODE));
|
|
113513
|
+
chmodSync4(path2, CONNECTIONS_DB_FILE_MODE);
|
|
113490
113514
|
}
|
|
113491
113515
|
function sqliteWasmDriver(db, raw) {
|
|
113492
113516
|
const connection = {
|
|
@@ -114884,6 +114908,7 @@ walletCommand.command("import").description("Import a pasted session private key
|
|
|
114884
114908
|
|
|
114885
114909
|
// src/commands/init.ts
|
|
114886
114910
|
init_config();
|
|
114911
|
+
init_paths();
|
|
114887
114912
|
init_core();
|
|
114888
114913
|
|
|
114889
114914
|
// src/mcp-server/policy-access.ts
|
|
@@ -115199,7 +115224,14 @@ confirm. No need to re-run init.`);
|
|
|
115199
115224
|
return false;
|
|
115200
115225
|
}
|
|
115201
115226
|
async function claimInit(claim, options) {
|
|
115202
|
-
const {
|
|
115227
|
+
const {
|
|
115228
|
+
parseClaimArgument: parseClaimArgument2,
|
|
115229
|
+
exchangeClaimToken: exchangeClaimToken2,
|
|
115230
|
+
decryptBootstrapBlob: decryptBootstrapBlob2,
|
|
115231
|
+
claimErrorMessage: claimErrorMessage2,
|
|
115232
|
+
backupVaultFile: backupVaultFile2,
|
|
115233
|
+
CLAIM_HOT_RELOAD_NOTICE: CLAIM_HOT_RELOAD_NOTICE2
|
|
115234
|
+
} = await Promise.resolve().then(() => (init_init_claim(), init_claim_exports));
|
|
115203
115235
|
const parsed = parseClaimArgument2(claim);
|
|
115204
115236
|
if (!parsed.ok) {
|
|
115205
115237
|
console.error(claimErrorMessage2(parsed.failure));
|
|
@@ -115226,11 +115258,21 @@ ${claimErrorMessage2({ kind: "decrypt" })}`);
|
|
|
115226
115258
|
}
|
|
115227
115259
|
await loginCommand({ apiKey: exchanged.result.apiKey, preseedAll: options.preseedAll });
|
|
115228
115260
|
const effective = applyWorkspacePassphraseStance(options);
|
|
115261
|
+
try {
|
|
115262
|
+
const backup = backupVaultFile2(VAULT_PATH);
|
|
115263
|
+
if (backup) console.log(`Previous wallet backed up to ${backup}`);
|
|
115264
|
+
} catch (e) {
|
|
115265
|
+
console.error(
|
|
115266
|
+
`Could not back up the existing wallet (${e instanceof Error ? e.message : String(e)}) \u2014 continuing with the claim.`
|
|
115267
|
+
);
|
|
115268
|
+
}
|
|
115229
115269
|
await walletImportCommand({
|
|
115230
115270
|
walletKey: privateKey,
|
|
115231
115271
|
force: effective.force,
|
|
115232
115272
|
noPassphrase: effective.noPassphrase
|
|
115233
115273
|
});
|
|
115274
|
+
console.log(`
|
|
115275
|
+
${CLAIM_HOT_RELOAD_NOTICE2}`);
|
|
115234
115276
|
return exchanged.result.apiKey;
|
|
115235
115277
|
}
|
|
115236
115278
|
async function initCommand(options = {}) {
|
|
@@ -115375,6 +115417,46 @@ var McpEventLogger = class {
|
|
|
115375
115417
|
}
|
|
115376
115418
|
};
|
|
115377
115419
|
|
|
115420
|
+
// src/mcp-server/newest-grant.ts
|
|
115421
|
+
init_esm_shims();
|
|
115422
|
+
function nonEmptyLowerCase(value2) {
|
|
115423
|
+
return typeof value2 === "string" && value2.trim() !== "" ? value2.toLowerCase() : null;
|
|
115424
|
+
}
|
|
115425
|
+
function isNewerGrant(candidate, incumbent) {
|
|
115426
|
+
const candidateId = Number(candidate?.id);
|
|
115427
|
+
const incumbentId = Number(incumbent?.id);
|
|
115428
|
+
const candidateIdUsable = Number.isFinite(candidateId);
|
|
115429
|
+
const incumbentIdUsable = Number.isFinite(incumbentId);
|
|
115430
|
+
if (candidateIdUsable && incumbentIdUsable && candidateId !== incumbentId) {
|
|
115431
|
+
return candidateId > incumbentId;
|
|
115432
|
+
}
|
|
115433
|
+
const candidateAt = Date.parse(typeof candidate?.createdAt === "string" ? candidate.createdAt : "");
|
|
115434
|
+
const incumbentAt = Date.parse(typeof incumbent?.createdAt === "string" ? incumbent.createdAt : "");
|
|
115435
|
+
if (Number.isFinite(candidateAt) && Number.isFinite(incumbentAt) && candidateAt !== incumbentAt) {
|
|
115436
|
+
return candidateAt > incumbentAt;
|
|
115437
|
+
}
|
|
115438
|
+
return candidateIdUsable && !incumbentIdUsable;
|
|
115439
|
+
}
|
|
115440
|
+
function selectNewestApprovedWallet(wallets, eoaAddress) {
|
|
115441
|
+
if (!Array.isArray(wallets)) return void 0;
|
|
115442
|
+
const eoa = nonEmptyLowerCase(eoaAddress);
|
|
115443
|
+
if (!eoa) return void 0;
|
|
115444
|
+
let newest;
|
|
115445
|
+
for (const wallet of wallets) {
|
|
115446
|
+
if (wallet?.status !== "approved") continue;
|
|
115447
|
+
if (nonEmptyLowerCase(wallet?.address) !== eoa) continue;
|
|
115448
|
+
if (newest === void 0 || isNewerGrant(wallet, newest)) newest = wallet;
|
|
115449
|
+
}
|
|
115450
|
+
return newest;
|
|
115451
|
+
}
|
|
115452
|
+
function findCurrentApprovedWallet(wallets, eoaAddress, pendingWalletId) {
|
|
115453
|
+
if (!Array.isArray(wallets)) return void 0;
|
|
115454
|
+
if (pendingWalletId) {
|
|
115455
|
+
return wallets.find((w) => String(w?.id) === String(pendingWalletId) && w?.status === "approved");
|
|
115456
|
+
}
|
|
115457
|
+
return selectNewestApprovedWallet(wallets, eoaAddress);
|
|
115458
|
+
}
|
|
115459
|
+
|
|
115378
115460
|
// src/mcp-server/start-session.ts
|
|
115379
115461
|
init_esm_shims();
|
|
115380
115462
|
init_dist();
|
|
@@ -115509,38 +115591,57 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
|
|
|
115509
115591
|
let virtualWalletId = ctx.virtualWalletId;
|
|
115510
115592
|
let paymentManagerAddress = ctx.paymentManagerAddress;
|
|
115511
115593
|
let policyId = ctx.policyId;
|
|
115512
|
-
|
|
115513
|
-
|
|
115514
|
-
|
|
115515
|
-
|
|
115516
|
-
|
|
115517
|
-
|
|
115518
|
-
|
|
115519
|
-
|
|
115520
|
-
|
|
115521
|
-
|
|
115522
|
-
|
|
115523
|
-
|
|
115524
|
-
|
|
115525
|
-
|
|
115526
|
-
|
|
115527
|
-
|
|
115528
|
-
|
|
115529
|
-
|
|
115530
|
-
|
|
115531
|
-
|
|
115594
|
+
let activeWallet;
|
|
115595
|
+
let grantChanged = false;
|
|
115596
|
+
try {
|
|
115597
|
+
activeWallet = selectNewestApprovedWallet(wallets, ctx.eoaAddress);
|
|
115598
|
+
if (activeWallet) {
|
|
115599
|
+
const newestId = String(activeWallet.id);
|
|
115600
|
+
const rowWalletAddress = activeWallet.walletAddress || activeWallet.kernelAccountAddress;
|
|
115601
|
+
const rowPaymentManagerAddress = activeWallet.paymentManagerAddress;
|
|
115602
|
+
const rowPolicyId = activeWallet.policyAssociated != null ? String(activeWallet.policyAssociated) : void 0;
|
|
115603
|
+
grantChanged = !!virtualWalletId && virtualWalletId !== newestId;
|
|
115604
|
+
if (grantChanged) {
|
|
115605
|
+
console.error(
|
|
115606
|
+
`[start_session] The active grant moved on \u2014 virtual wallet ${virtualWalletId} -> ${newestId}. Adopting the newest approved grant and dropping the session state derived from the old one.`
|
|
115607
|
+
);
|
|
115608
|
+
walletAddress = rowWalletAddress;
|
|
115609
|
+
paymentManagerAddress = rowPaymentManagerAddress;
|
|
115610
|
+
policyId = rowPolicyId;
|
|
115611
|
+
} else {
|
|
115612
|
+
walletAddress = rowWalletAddress || walletAddress;
|
|
115613
|
+
paymentManagerAddress = rowPaymentManagerAddress || paymentManagerAddress;
|
|
115614
|
+
policyId = rowPolicyId ?? policyId;
|
|
115615
|
+
}
|
|
115616
|
+
virtualWalletId = newestId;
|
|
115617
|
+
credentials.walletAddress = walletAddress;
|
|
115618
|
+
credentials.virtualWalletId = virtualWalletId;
|
|
115619
|
+
credentials.paymentManagerAddress = paymentManagerAddress;
|
|
115620
|
+
credentials.policyId = policyId;
|
|
115621
|
+
credentials.authorizationStatus = "approved";
|
|
115622
|
+
if (activeWallet.policyName != null) {
|
|
115623
|
+
credentials.policyName = activeWallet.policyName;
|
|
115624
|
+
}
|
|
115625
|
+
if (activeWallet.policyValidUntil != null) {
|
|
115626
|
+
credentials.policyValidUntil = BigInt(activeWallet.policyValidUntil);
|
|
115627
|
+
}
|
|
115628
|
+
} else if (walletsFetchFailed && virtualWalletId) {
|
|
115532
115629
|
console.warn(
|
|
115533
|
-
|
|
115534
|
-
backendError instanceof Error ? backendError.message : String(backendError)
|
|
115630
|
+
`[start_session] \u26A0\uFE0F could not verify the active grant \u2014 signing with the cached one (virtual wallet ${virtualWalletId}, policy ${policyId ?? "unknown"}). If a newer grant was approved since, spends are metered against the older one and may hit its spending limit.`
|
|
115535
115631
|
);
|
|
115536
115632
|
}
|
|
115633
|
+
} catch (backendError) {
|
|
115634
|
+
console.warn(
|
|
115635
|
+
"[start_session] Could not check backend:",
|
|
115636
|
+
backendError instanceof Error ? backendError.message : String(backendError)
|
|
115637
|
+
);
|
|
115537
115638
|
}
|
|
115538
115639
|
const mergedAuth = credentials.authorizationStatus !== void 0 ? credentials.authorizationStatus : ctx.authorizationStatus;
|
|
115539
115640
|
console.error(
|
|
115540
115641
|
`[start_session] State check \u2014 walletAddress: ${walletAddress}, virtualWalletId: ${virtualWalletId}, paymentManagerAddress: ${paymentManagerAddress}, policyId: ${policyId}`
|
|
115541
115642
|
);
|
|
115542
115643
|
if (mergedAuth === "expired") {
|
|
115543
|
-
return { responseKey: "unlocked_expired", credentials, timingReport: timing.report() };
|
|
115644
|
+
return { responseKey: "unlocked_expired", credentials, timingReport: timing.report(), grantChanged };
|
|
115544
115645
|
}
|
|
115545
115646
|
if (walletAddress && virtualWalletId && paymentManagerAddress && policyId) {
|
|
115546
115647
|
let privateKey = null;
|
|
@@ -115562,12 +115663,9 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
|
|
|
115562
115663
|
}
|
|
115563
115664
|
credentials.walletAddress = walletAddress;
|
|
115564
115665
|
credentials.paymentManagerAddress = paymentManagerAddress;
|
|
115565
|
-
const
|
|
115566
|
-
|
|
115567
|
-
|
|
115568
|
-
const preloadedWalletData = approvedWallet != null && approvedWallet.policyValidAfter != null && approvedWallet.policyValidUntil != null ? {
|
|
115569
|
-
policyValidAfter: Number(approvedWallet.policyValidAfter),
|
|
115570
|
-
policyValidUntil: Number(approvedWallet.policyValidUntil)
|
|
115666
|
+
const preloadedWalletData = activeWallet != null && activeWallet.policyValidAfter != null && activeWallet.policyValidUntil != null ? {
|
|
115667
|
+
policyValidAfter: Number(activeWallet.policyValidAfter),
|
|
115668
|
+
policyValidUntil: Number(activeWallet.policyValidUntil)
|
|
115571
115669
|
} : void 0;
|
|
115572
115670
|
if (ctx.readPolicyActiveOnchain) {
|
|
115573
115671
|
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
@@ -115589,7 +115687,7 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
|
|
|
115589
115687
|
maxAttempts
|
|
115590
115688
|
});
|
|
115591
115689
|
if (bootAction === "reauthorize") {
|
|
115592
|
-
return { responseKey: "unlocked_pending_authorization", credentials, timingReport: timing.report() };
|
|
115690
|
+
return { responseKey: "unlocked_pending_authorization", credentials, timingReport: timing.report(), grantChanged };
|
|
115593
115691
|
}
|
|
115594
115692
|
}
|
|
115595
115693
|
const { kernelClient: virtualWalletKernelAccountClient, permissionPlugin: virtualWalletPermissionPlugin, kernelDomain } = await virtualWalletsManagers2.configureVirtualWalletKernelAccount(
|
|
@@ -115619,13 +115717,14 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
|
|
|
115619
115717
|
console.warn("[start_session] \u26A0\uFE0F Could not fetch credit token address (Path B routing disabled):", e);
|
|
115620
115718
|
}
|
|
115621
115719
|
}
|
|
115622
|
-
return { responseKey: "unlocked_and_authorized", credentials, timingReport: timing.report() };
|
|
115720
|
+
return { responseKey: "unlocked_and_authorized", credentials, timingReport: timing.report(), grantChanged };
|
|
115623
115721
|
} catch (error) {
|
|
115624
115722
|
console.error("\u274C [start_session] Kernel client creation failed:", error);
|
|
115625
115723
|
return {
|
|
115626
115724
|
responseKey: "unlocked_not_configured",
|
|
115627
115725
|
credentials,
|
|
115628
115726
|
timingReport: timing.report(),
|
|
115727
|
+
grantChanged,
|
|
115629
115728
|
kernelErrorMessage: error instanceof Error ? error.message : String(error)
|
|
115630
115729
|
};
|
|
115631
115730
|
} finally {
|
|
@@ -115633,9 +115732,9 @@ async function runStartSession(ctx, timing = new TimingCollector("start_session"
|
|
|
115633
115732
|
}
|
|
115634
115733
|
}
|
|
115635
115734
|
if (!walletsFetchFailed && wallets.length === 0) {
|
|
115636
|
-
return { responseKey: "unlocked_provisioning_pending", credentials, timingReport: timing.report() };
|
|
115735
|
+
return { responseKey: "unlocked_provisioning_pending", credentials, timingReport: timing.report(), grantChanged };
|
|
115637
115736
|
}
|
|
115638
|
-
return { responseKey: "unlocked_pending_authorization", credentials, timingReport: timing.report() };
|
|
115737
|
+
return { responseKey: "unlocked_pending_authorization", credentials, timingReport: timing.report(), grantChanged };
|
|
115639
115738
|
}
|
|
115640
115739
|
var WALLET_SCOPED_CREDENTIAL_KEYS = [
|
|
115641
115740
|
"virtualWalletId",
|
|
@@ -115665,9 +115764,9 @@ function clearWalletScopedCredentials(target) {
|
|
|
115665
115764
|
function sameAddress(a, b) {
|
|
115666
115765
|
return String(a ?? "").toLowerCase() === String(b ?? "").toLowerCase();
|
|
115667
115766
|
}
|
|
115668
|
-
function mergeStartSessionCredentials(target, patch, responseKey) {
|
|
115767
|
+
function mergeStartSessionCredentials(target, patch, responseKey, options = {}) {
|
|
115669
115768
|
const walletChanged = patch.eoaAddress !== void 0 && !sameAddress(patch.eoaAddress, target.eoaAddress);
|
|
115670
|
-
if (walletChanged) {
|
|
115769
|
+
if (walletChanged || options.grantChanged) {
|
|
115671
115770
|
clearWalletScopedCredentials(target);
|
|
115672
115771
|
}
|
|
115673
115772
|
if (patch.walletKeystoreJson !== void 0) {
|
|
@@ -116848,1012 +116947,1047 @@ init_local_state();
|
|
|
116848
116947
|
init_esm_shims();
|
|
116849
116948
|
init_paths();
|
|
116850
116949
|
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
116950
|
+
import { join as join8 } from "path";
|
|
116951
|
+
|
|
116952
|
+
// src/loops/state-docs.ts
|
|
116953
|
+
init_esm_shims();
|
|
116954
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
116851
116955
|
import { join as join7 } from "path";
|
|
116852
|
-
|
|
116853
|
-
|
|
116854
|
-
|
|
116855
|
-
|
|
116856
|
-
|
|
116857
|
-
|
|
116858
|
-
|
|
116859
|
-
];
|
|
116860
|
-
|
|
116861
|
-
|
|
116862
|
-
|
|
116863
|
-
|
|
116864
|
-
|
|
116865
|
-
|
|
116866
|
-
|
|
116867
|
-
|
|
116868
|
-
|
|
116869
|
-
files[label2] = filename;
|
|
116870
|
-
} else {
|
|
116871
|
-
skipped.push(label2);
|
|
116956
|
+
|
|
116957
|
+
// src/loops/shipback.ts
|
|
116958
|
+
init_esm_shims();
|
|
116959
|
+
function scanHeadings(text) {
|
|
116960
|
+
return scanDoc(text).items;
|
|
116961
|
+
}
|
|
116962
|
+
function scanDoc(text) {
|
|
116963
|
+
const out = [];
|
|
116964
|
+
let offset = 0;
|
|
116965
|
+
let inFence = false;
|
|
116966
|
+
for (const line of text.split("\n")) {
|
|
116967
|
+
const bare = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
116968
|
+
const trimmed = bare.trimStart();
|
|
116969
|
+
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) inFence = !inFence;
|
|
116970
|
+
else if (!inFence) {
|
|
116971
|
+
const m = /^###\s+(.*)$/.exec(bare);
|
|
116972
|
+
if (m) out.push({ heading: m[1].trim(), start: offset });
|
|
116872
116973
|
}
|
|
116974
|
+
offset += line.length + 1;
|
|
116873
116975
|
}
|
|
116874
|
-
|
|
116875
|
-
join7(dir, "STATUS.md"),
|
|
116876
|
-
`# STATUS \u2014 ${task.slug}
|
|
116877
|
-
|
|
116878
|
-
task_id: ${task.id}
|
|
116879
|
-
run_id: ${runId}
|
|
116880
|
-
started: pending
|
|
116881
|
-
queue: not started
|
|
116882
|
-
`,
|
|
116883
|
-
{ mode: 384 }
|
|
116884
|
-
);
|
|
116885
|
-
files.status = "STATUS.md";
|
|
116886
|
-
return { dir, files, skipped };
|
|
116976
|
+
return { items: out, unbalanced: inFence };
|
|
116887
116977
|
}
|
|
116888
|
-
|
|
116889
|
-
|
|
116890
|
-
|
|
116891
|
-
|
|
116892
|
-
|
|
116893
|
-
|
|
116894
|
-
import { join as join8 } from "path";
|
|
116895
|
-
var DEFAULT_PORT = 4477;
|
|
116896
|
-
var DASHBOARD_PORT_ENV = "AMETYST_LOOP_DASHBOARD_PORT";
|
|
116897
|
-
var DASHBOARD_NO_OPEN_ENV = "AMETYST_DASHBOARD_NO_OPEN";
|
|
116898
|
-
var NO_DASHBOARD_MESSAGE = "no dashboard on this task \u2014 ask your agent to create one";
|
|
116899
|
-
var MAX_PORT_RETRIES = 20;
|
|
116900
|
-
var SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
116901
|
-
function parseManifestFiles(manifest) {
|
|
116902
|
-
if (typeof manifest !== "string" || !manifest.trim()) return [];
|
|
116903
|
-
let parsed;
|
|
116904
|
-
try {
|
|
116905
|
-
parsed = JSON.parse(manifest);
|
|
116906
|
-
} catch {
|
|
116907
|
-
return [];
|
|
116978
|
+
function headingsIgnoringFences(text) {
|
|
116979
|
+
const out = [];
|
|
116980
|
+
for (const line of text.split("\n")) {
|
|
116981
|
+
const bare = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
116982
|
+
const m = /^ {0,3}###\s+(.*)$/.exec(bare);
|
|
116983
|
+
if (m) out.push(m[1].trim());
|
|
116908
116984
|
}
|
|
116909
|
-
|
|
116910
|
-
return list2.filter((f) => typeof f === "string" && SAFE_NAME.test(f));
|
|
116985
|
+
return out;
|
|
116911
116986
|
}
|
|
116912
|
-
function
|
|
116913
|
-
|
|
116914
|
-
return Number.isInteger(raw) && raw > 0 && raw < 65536 ? raw : DEFAULT_PORT;
|
|
116987
|
+
function headings(text) {
|
|
116988
|
+
return scanHeadings(text).map((h) => h.heading);
|
|
116915
116989
|
}
|
|
116916
|
-
function
|
|
116917
|
-
const
|
|
116918
|
-
|
|
116919
|
-
|
|
116920
|
-
if (!isTTY) return false;
|
|
116921
|
-
const platform = deps.platform ?? process.platform;
|
|
116922
|
-
const cmd = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : null;
|
|
116923
|
-
if (!cmd) return false;
|
|
116924
|
-
const spawn4 = deps.spawn ?? realSpawn;
|
|
116925
|
-
try {
|
|
116926
|
-
const child = spawn4(cmd, [url2], { stdio: "ignore", detached: true });
|
|
116927
|
-
child.once?.("error", () => {
|
|
116928
|
-
});
|
|
116929
|
-
child.unref?.();
|
|
116930
|
-
return true;
|
|
116931
|
-
} catch {
|
|
116932
|
-
return false;
|
|
116933
|
-
}
|
|
116990
|
+
function countByHeading(list2) {
|
|
116991
|
+
const m = /* @__PURE__ */ new Map();
|
|
116992
|
+
for (const h of list2) m.set(h, (m.get(h) ?? 0) + 1);
|
|
116993
|
+
return m;
|
|
116934
116994
|
}
|
|
116935
|
-
function
|
|
116936
|
-
const
|
|
116937
|
-
if (
|
|
116938
|
-
const
|
|
116939
|
-
const
|
|
116940
|
-
|
|
116941
|
-
|
|
116942
|
-
|
|
116943
|
-
|
|
116944
|
-
|
|
116945
|
-
|
|
116946
|
-
|
|
116947
|
-
|
|
116948
|
-
|
|
116949
|
-
|
|
116950
|
-
|
|
116951
|
-
|
|
116952
|
-
|
|
116953
|
-
|
|
116954
|
-
|
|
116955
|
-
|
|
116956
|
-
|
|
116957
|
-
|
|
116958
|
-
|
|
116959
|
-
|
|
116960
|
-
|
|
116961
|
-
|
|
116962
|
-
|
|
116963
|
-
|
|
116964
|
-
|
|
116965
|
-
}
|
|
116966
|
-
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
116967
|
-
res.end(
|
|
116968
|
-
JSON.stringify({
|
|
116969
|
-
loop: args.loop.slug,
|
|
116970
|
-
files: data,
|
|
116971
|
-
state,
|
|
116972
|
-
mode: "live",
|
|
116973
|
-
asOf: (/* @__PURE__ */ new Date()).toISOString()
|
|
116974
|
-
})
|
|
116975
|
-
);
|
|
116976
|
-
return;
|
|
116977
|
-
}
|
|
116978
|
-
res.writeHead(404, { "content-type": "text/plain" });
|
|
116979
|
-
res.end("not found");
|
|
116980
|
-
} catch {
|
|
116981
|
-
try {
|
|
116982
|
-
res.writeHead(500);
|
|
116983
|
-
res.end();
|
|
116984
|
-
} catch {
|
|
116985
|
-
}
|
|
116995
|
+
function sections(fragment) {
|
|
116996
|
+
const found = scanHeadings(fragment);
|
|
116997
|
+
if (found.length === 0) return { preamble: fragment, blocks: [] };
|
|
116998
|
+
const preamble = fragment.slice(0, found[0].start);
|
|
116999
|
+
const blocks = found.map((h, i) => ({
|
|
117000
|
+
heading: h.heading,
|
|
117001
|
+
text: fragment.slice(h.start, i + 1 < found.length ? found[i + 1].start : fragment.length)
|
|
117002
|
+
}));
|
|
117003
|
+
return { preamble, blocks };
|
|
117004
|
+
}
|
|
117005
|
+
function norm(s) {
|
|
117006
|
+
return s.replace(/\r\n/g, "\n").trimEnd();
|
|
117007
|
+
}
|
|
117008
|
+
function mergeConstraints(boot, materialized, fresh) {
|
|
117009
|
+
if (materialized === boot) return { next: void 0, added: [], addedBlocks: [], cleanAppend: true };
|
|
117010
|
+
const recordUnchanged = fresh === boot;
|
|
117011
|
+
let next;
|
|
117012
|
+
let cleanAppend;
|
|
117013
|
+
if (materialized.startsWith(boot)) {
|
|
117014
|
+
const suffix = materialized.slice(boot.length);
|
|
117015
|
+
if (recordUnchanged) {
|
|
117016
|
+
next = materialized;
|
|
117017
|
+
} else {
|
|
117018
|
+
const { preamble, blocks } = sections(suffix);
|
|
117019
|
+
const present = new Set(sections(fresh).blocks.map((b) => norm(b.text)));
|
|
117020
|
+
const novel = blocks.filter((b) => !present.has(norm(b.text)));
|
|
117021
|
+
const keepPreamble = preamble.trim().length > 0 && !norm(fresh).endsWith(norm(preamble));
|
|
117022
|
+
const appended = (keepPreamble ? preamble : "") + novel.map((b) => b.text).join("");
|
|
117023
|
+
const seam = appended.length > 0 && fresh.length > 0 && !fresh.endsWith("\n") ? "\n" : "";
|
|
117024
|
+
next = fresh + seam + appended;
|
|
116986
117025
|
}
|
|
116987
|
-
|
|
116988
|
-
|
|
116989
|
-
|
|
116990
|
-
|
|
116991
|
-
|
|
116992
|
-
|
|
116993
|
-
|
|
116994
|
-
|
|
116995
|
-
|
|
116996
|
-
|
|
116997
|
-
|
|
116998
|
-
|
|
116999
|
-
|
|
117000
|
-
|
|
117001
|
-
|
|
117002
|
-
|
|
117003
|
-
|
|
117004
|
-
|
|
117005
|
-
|
|
117006
|
-
|
|
117007
|
-
|
|
117008
|
-
|
|
117009
|
-
|
|
117010
|
-
|
|
117011
|
-
|
|
117012
|
-
|
|
117013
|
-
|
|
117014
|
-
|
|
117015
|
-
|
|
117016
|
-
|
|
117017
|
-
|
|
117018
|
-
|
|
117019
|
-
|
|
117020
|
-
|
|
117021
|
-
|
|
117022
|
-
}
|
|
117023
|
-
if (attempt.err.code !== "EADDRINUSE") {
|
|
117024
|
-
log(
|
|
117025
|
-
` (loop dashboard not started on :${port} \u2014 ${attempt.err.code ?? attempt.err.message}; run continues)`
|
|
117026
|
-
);
|
|
117027
|
-
return null;
|
|
117028
|
-
}
|
|
117026
|
+
cleanAppend = true;
|
|
117027
|
+
} else {
|
|
117028
|
+
next = materialized;
|
|
117029
|
+
cleanAppend = false;
|
|
117030
|
+
}
|
|
117031
|
+
const guardCounts = (text) => countByHeading(headingsIgnoringFences(text));
|
|
117032
|
+
const freshCounts = guardCounts(fresh);
|
|
117033
|
+
const nextCounts = guardCounts(next);
|
|
117034
|
+
const countAll = (m) => [...m.values()].reduce((a, b) => a + b, 0);
|
|
117035
|
+
const lost = [];
|
|
117036
|
+
for (const [h, n] of freshCounts) {
|
|
117037
|
+
const kept = nextCounts.get(h) ?? 0;
|
|
117038
|
+
if (kept < n) lost.push(`${h}${n > 1 ? ` (${n}\u2192${kept})` : ""}`);
|
|
117039
|
+
}
|
|
117040
|
+
if (lost.length > 0) {
|
|
117041
|
+
const before = countAll(freshCounts);
|
|
117042
|
+
const after = countAll(nextCounts);
|
|
117043
|
+
return {
|
|
117044
|
+
refusal: `ship-back REFUSED: ${lost.length} section(s) present on the record would be lost by this write (${before} \u2192 ${after} headings). Lost: ${lost.slice(0, 5).map((h) => JSON.stringify(h)).join(", ")}${lost.length > 5 ? ` \u2026and ${lost.length - 5} more` : ""}. TO RECOVER: put the bare \`### \` heading line(s) named above back into the refused document and re-run \u2014 the guard counts headings, so an empty section is enough to let the write through.`,
|
|
117045
|
+
added: [],
|
|
117046
|
+
addedBlocks: [],
|
|
117047
|
+
cleanAppend
|
|
117048
|
+
};
|
|
117049
|
+
}
|
|
117050
|
+
if (!cleanAppend) {
|
|
117051
|
+
const bootBlocks = new Set(sections(boot).blocks.map((b) => norm(b.text)));
|
|
117052
|
+
const nextBlocks = new Set(sections(next).blocks.map((b) => norm(b.text)));
|
|
117053
|
+
const clobbered = sections(fresh).blocks.filter((b) => !bootBlocks.has(norm(b.text))).filter((b) => !nextBlocks.has(norm(b.text)));
|
|
117054
|
+
if (clobbered.length > 0) {
|
|
117055
|
+
return {
|
|
117056
|
+
refusal: `ship-back REFUSED: ${clobbered.length} section(s) added to the record while this fire was running would be REWRITTEN by this fire's document, which never saw them. Affected: ${clobbered.slice(0, 5).map((b) => JSON.stringify(b.heading)).join(", ")}${clobbered.length > 5 ? ` \u2026and ${clobbered.length - 5} more` : ""}. TO RECOVER: copy those section(s) verbatim off the record into the refused document and re-run \u2014 here it is the section BODY that must survive, so a bare heading is not enough on this branch.`,
|
|
117057
|
+
added: [],
|
|
117058
|
+
addedBlocks: [],
|
|
117059
|
+
cleanAppend
|
|
117060
|
+
};
|
|
117029
117061
|
}
|
|
117030
|
-
|
|
117031
|
-
|
|
117032
|
-
|
|
117062
|
+
}
|
|
117063
|
+
if (next === fresh) return { next: void 0, added: [], addedBlocks: [], cleanAppend };
|
|
117064
|
+
const added = [];
|
|
117065
|
+
const remaining = new Map(freshCounts);
|
|
117066
|
+
for (const h of headings(next)) {
|
|
117067
|
+
const owed = remaining.get(h) ?? 0;
|
|
117068
|
+
if (owed > 0) remaining.set(h, owed - 1);
|
|
117069
|
+
else added.push(h);
|
|
117070
|
+
}
|
|
117071
|
+
const freshBlocks = new Set(sections(fresh).blocks.map((b) => norm(b.text)));
|
|
117072
|
+
const addedBlocks = sections(next).blocks.map((b) => norm(b.text)).filter((t) => !freshBlocks.has(t));
|
|
117073
|
+
return { next, added, addedBlocks, cleanAppend };
|
|
117074
|
+
}
|
|
117075
|
+
function verifyLanded(afterWrite, expectedBlocks) {
|
|
117076
|
+
const have = new Set(sections(afterWrite).blocks.map((b) => norm(b.text)));
|
|
117077
|
+
return expectedBlocks.filter((t) => !have.has(t));
|
|
117033
117078
|
}
|
|
117034
117079
|
|
|
117035
|
-
// src/
|
|
117080
|
+
// src/loops/memory-manifest.ts
|
|
117036
117081
|
init_esm_shims();
|
|
117037
|
-
var
|
|
117038
|
-
var
|
|
117039
|
-
|
|
117040
|
-
|
|
117041
|
-
const
|
|
117042
|
-
|
|
117043
|
-
|
|
117044
|
-
|
|
117082
|
+
var MEMORY_MANIFEST_MAX_DOCS = 64;
|
|
117083
|
+
var MEMORY_MANIFEST_MAX_RECORDS = 16;
|
|
117084
|
+
var KEY_MAX_LENGTH = 128;
|
|
117085
|
+
function isReservedManifestKey(key, reserved) {
|
|
117086
|
+
const k = key.toLowerCase();
|
|
117087
|
+
return reserved.some((r) => {
|
|
117088
|
+
const name = r.toLowerCase();
|
|
117089
|
+
return k === name || name.endsWith(".md") && k === name.slice(0, -3);
|
|
117090
|
+
});
|
|
117045
117091
|
}
|
|
117046
|
-
function
|
|
117047
|
-
if (
|
|
117048
|
-
|
|
117049
|
-
|
|
117050
|
-
|
|
117051
|
-
|
|
117052
|
-
}
|
|
117092
|
+
function isSafeKey(key) {
|
|
117093
|
+
if (key.length === 0 || key.length > KEY_MAX_LENGTH) return false;
|
|
117094
|
+
if (key === "." || key === "..") return false;
|
|
117095
|
+
if (key.startsWith(".")) return false;
|
|
117096
|
+
if (key.includes("/") || key.includes("\\") || key.includes("\0")) return false;
|
|
117097
|
+
return true;
|
|
117053
117098
|
}
|
|
117054
|
-
function
|
|
117055
|
-
|
|
117056
|
-
|
|
117057
|
-
|
|
117058
|
-
|
|
117059
|
-
|
|
117099
|
+
function normalizeMemoryScope(scope) {
|
|
117100
|
+
switch (scope) {
|
|
117101
|
+
case "shared":
|
|
117102
|
+
case "workspace":
|
|
117103
|
+
return "shared";
|
|
117104
|
+
case "member":
|
|
117105
|
+
case "profile":
|
|
117106
|
+
return "member";
|
|
117107
|
+
default:
|
|
117108
|
+
return void 0;
|
|
117109
|
+
}
|
|
117060
117110
|
}
|
|
117061
|
-
function
|
|
117062
|
-
|
|
117063
|
-
|
|
117064
|
-
|
|
117065
|
-
|
|
117066
|
-
|
|
117111
|
+
function validateMemoryManifest(value2, reserved) {
|
|
117112
|
+
if (value2 === null) return { ok: true, manifest: null };
|
|
117113
|
+
if (Array.isArray(value2)) {
|
|
117114
|
+
return {
|
|
117115
|
+
ok: false,
|
|
117116
|
+
error: 'stateDocs is an ARRAY \u2014 that is the retired declaration shape. The memory manifest is an object: {"docs":[{"key":"\u2026","scope":"shared|member"}],"records":[{"kind":"run|decision|error|import|<free kind>","scope":"shared|member"}]}. Send "null" to clear, or the object form.'
|
|
117117
|
+
};
|
|
117067
117118
|
}
|
|
117068
|
-
|
|
117119
|
+
if (typeof value2 !== "object") {
|
|
117120
|
+
return { ok: false, error: "stateDocs must be a JSON object with `docs` and `records` arrays, or null." };
|
|
117121
|
+
}
|
|
117122
|
+
const obj = value2;
|
|
117123
|
+
const extra = Object.keys(obj).filter((k) => k !== "docs" && k !== "records");
|
|
117124
|
+
if (extra.length > 0) {
|
|
117125
|
+
return { ok: false, error: `stateDocs carries unknown propert${extra.length === 1 ? "y" : "ies"} ${extra.join(", ")} \u2014 only \`docs\` and \`records\` are allowed.` };
|
|
117126
|
+
}
|
|
117127
|
+
if (!Array.isArray(obj.docs) || !Array.isArray(obj.records)) {
|
|
117128
|
+
return { ok: false, error: "stateDocs needs BOTH `docs` and `records` arrays (either may be empty)." };
|
|
117129
|
+
}
|
|
117130
|
+
if (obj.docs.length > MEMORY_MANIFEST_MAX_DOCS) {
|
|
117131
|
+
return { ok: false, error: `stateDocs.docs holds ${obj.docs.length} entries \u2014 the maximum is ${MEMORY_MANIFEST_MAX_DOCS}.` };
|
|
117132
|
+
}
|
|
117133
|
+
if (obj.records.length > MEMORY_MANIFEST_MAX_RECORDS) {
|
|
117134
|
+
return { ok: false, error: `stateDocs.records holds ${obj.records.length} entries \u2014 the maximum is ${MEMORY_MANIFEST_MAX_RECORDS}.` };
|
|
117135
|
+
}
|
|
117136
|
+
const docs = [];
|
|
117137
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
117138
|
+
for (const [i, raw] of obj.docs.entries()) {
|
|
117139
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
117140
|
+
return { ok: false, error: `stateDocs.docs[${i}] must be an object {key, scope}.` };
|
|
117141
|
+
}
|
|
117142
|
+
const d = raw;
|
|
117143
|
+
const extraDoc = Object.keys(d).filter((k) => k !== "key" && k !== "scope");
|
|
117144
|
+
if (extraDoc.length > 0) {
|
|
117145
|
+
return { ok: false, error: `stateDocs.docs[${i}] carries unknown propert${extraDoc.length === 1 ? "y" : "ies"} ${extraDoc.join(", ")} \u2014 a doc is exactly {key, scope}.` };
|
|
117146
|
+
}
|
|
117147
|
+
if (typeof d.key !== "string" || !isSafeKey(d.key)) {
|
|
117148
|
+
return { ok: false, error: `stateDocs.docs[${i}].key must be a safe basename of 1..${KEY_MAX_LENGTH} chars (no \`/\`, \`\\\`, NUL, no leading dot).` };
|
|
117149
|
+
}
|
|
117150
|
+
if (isReservedManifestKey(d.key, reserved)) {
|
|
117151
|
+
return { ok: false, error: `stateDocs.docs[${i}].key "${d.key}" names a file the runner writes itself (${reserved.join(", ")}) \u2014 pick another key.` };
|
|
117152
|
+
}
|
|
117153
|
+
const lower = d.key.toLowerCase();
|
|
117154
|
+
if (seenKeys.has(lower)) {
|
|
117155
|
+
return { ok: false, error: `stateDocs.docs has "${d.key}" more than once (keys are unique case-insensitively).` };
|
|
117156
|
+
}
|
|
117157
|
+
seenKeys.add(lower);
|
|
117158
|
+
if (d.scope !== "shared" && d.scope !== "member") {
|
|
117159
|
+
return { ok: false, error: `stateDocs.docs[${i}].scope must be "shared" or "member".` };
|
|
117160
|
+
}
|
|
117161
|
+
docs.push({ key: d.key, scope: d.scope });
|
|
117162
|
+
}
|
|
117163
|
+
const records = [];
|
|
117164
|
+
const seenKinds = /* @__PURE__ */ new Set();
|
|
117165
|
+
for (const [i, raw] of obj.records.entries()) {
|
|
117166
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
117167
|
+
return { ok: false, error: `stateDocs.records[${i}] must be an object {kind, scope}.` };
|
|
117168
|
+
}
|
|
117169
|
+
const r = raw;
|
|
117170
|
+
const extraRec = Object.keys(r).filter((k) => k !== "kind" && k !== "scope");
|
|
117171
|
+
if (extraRec.length > 0) {
|
|
117172
|
+
return { ok: false, error: `stateDocs.records[${i}] carries unknown propert${extraRec.length === 1 ? "y" : "ies"} ${extraRec.join(", ")} \u2014 a record is exactly {kind, scope}.` };
|
|
117173
|
+
}
|
|
117174
|
+
if (!isRecordKindToken(r.kind)) {
|
|
117175
|
+
return {
|
|
117176
|
+
ok: false,
|
|
117177
|
+
error: `stateDocs.records[${i}].kind must be a token matching [a-z0-9-]{1,64} \u2014 a reserved kind (${RESERVED_RECORD_KINDS.join(", ")}) or a free kind of the task's own (e.g. "pbi").`
|
|
117178
|
+
};
|
|
117179
|
+
}
|
|
117180
|
+
if (seenKinds.has(r.kind)) {
|
|
117181
|
+
return { ok: false, error: `stateDocs.records declares "${r.kind}" more than once.` };
|
|
117182
|
+
}
|
|
117183
|
+
seenKinds.add(r.kind);
|
|
117184
|
+
if (r.scope !== "shared" && r.scope !== "member") {
|
|
117185
|
+
return { ok: false, error: `stateDocs.records[${i}].scope must be "shared" or "member".` };
|
|
117186
|
+
}
|
|
117187
|
+
records.push({ kind: r.kind, scope: r.scope });
|
|
117188
|
+
}
|
|
117189
|
+
return { ok: true, manifest: { docs, records } };
|
|
117069
117190
|
}
|
|
117070
|
-
function
|
|
117071
|
-
if (typeof
|
|
117072
|
-
const
|
|
117073
|
-
const
|
|
117074
|
-
|
|
117075
|
-
|
|
117076
|
-
|
|
117077
|
-
|
|
117078
|
-
const
|
|
117079
|
-
const
|
|
117080
|
-
|
|
117081
|
-
|
|
117191
|
+
function normalizeMemoryManifest(manifest) {
|
|
117192
|
+
if (!manifest || typeof manifest !== "object") return null;
|
|
117193
|
+
const docs = [];
|
|
117194
|
+
for (const d of Array.isArray(manifest.docs) ? manifest.docs : []) {
|
|
117195
|
+
const scope = normalizeMemoryScope(d?.scope);
|
|
117196
|
+
if (typeof d?.key !== "string" || d.key.length === 0 || !scope) continue;
|
|
117197
|
+
docs.push({ key: d.key, scope });
|
|
117198
|
+
}
|
|
117199
|
+
const records = [];
|
|
117200
|
+
for (const r of Array.isArray(manifest.records) ? manifest.records : []) {
|
|
117201
|
+
const scope = normalizeMemoryScope(r?.scope);
|
|
117202
|
+
if (!isRecordKindToken(r?.kind) || !scope) continue;
|
|
117203
|
+
records.push({ kind: r.kind, scope });
|
|
117204
|
+
}
|
|
117205
|
+
return { docs, records };
|
|
117082
117206
|
}
|
|
117083
|
-
function
|
|
117084
|
-
|
|
117085
|
-
|
|
117086
|
-
|
|
117207
|
+
async function readTaskManifest(sdk, apiKey, slug) {
|
|
117208
|
+
try {
|
|
117209
|
+
const got = await sdk.tasks.get(apiKey, slug);
|
|
117210
|
+
if (got?.status !== "ok") return void 0;
|
|
117211
|
+
return normalizeMemoryManifest(got.task?.stateDocs ?? null);
|
|
117212
|
+
} catch {
|
|
117213
|
+
return void 0;
|
|
117087
117214
|
}
|
|
117088
|
-
if (arg.kind === "valid") return { ok: true, mode: arg.mode, source: "explicit" };
|
|
117089
|
-
const fromBody = parseDefaultRunMode(body);
|
|
117090
|
-
if (fromBody) return { ok: true, mode: fromBody, source: "frontmatter" };
|
|
117091
|
-
return { ok: true, mode: "in-chat", source: "default" };
|
|
117092
117215
|
}
|
|
117093
|
-
|
|
117094
|
-
|
|
117095
|
-
|
|
117096
|
-
function
|
|
117097
|
-
|
|
117098
|
-
|
|
117099
|
-
|
|
117100
|
-
|
|
117101
|
-
|
|
117102
|
-
|
|
117103
|
-
|
|
117104
|
-
const
|
|
117105
|
-
|
|
117216
|
+
function declaredDocScope(manifest, key) {
|
|
117217
|
+
return manifest?.docs.find((d) => d.key.toLowerCase() === key.toLowerCase())?.scope;
|
|
117218
|
+
}
|
|
117219
|
+
function declaredRecordScope(manifest, kind) {
|
|
117220
|
+
return manifest?.records.find((r) => r.kind === kind)?.scope;
|
|
117221
|
+
}
|
|
117222
|
+
function declaredRecordKinds(manifest) {
|
|
117223
|
+
if (manifest === void 0) return void 0;
|
|
117224
|
+
return manifest?.records.map((r) => r.kind) ?? [];
|
|
117225
|
+
}
|
|
117226
|
+
function formatMemoryManifest(manifest) {
|
|
117227
|
+
const normalized = normalizeMemoryManifest(manifest);
|
|
117228
|
+
if (!normalized) return "(none)";
|
|
117229
|
+
const lines = [
|
|
117230
|
+
...normalized.docs.map((d) => `doc ${d.key} \xB7 ${d.scope}`),
|
|
117231
|
+
...normalized.records.map((r) => `record ${r.kind} \xB7 ${r.scope}`)
|
|
117232
|
+
];
|
|
117233
|
+
return lines.length ? lines.join("\n") : "(declared empty: no docs, no records)";
|
|
117106
117234
|
}
|
|
117107
117235
|
|
|
117108
|
-
// src/loops/
|
|
117109
|
-
|
|
117110
|
-
|
|
117236
|
+
// src/loops/state-docs.ts
|
|
117237
|
+
var RESERVED_FIRE_FILENAMES = [
|
|
117238
|
+
"SKILL.md",
|
|
117111
117239
|
"VISION.md",
|
|
117112
117240
|
"CONSTRAINTS.md",
|
|
117113
|
-
"QUEUE.md",
|
|
117114
|
-
"STATUS.md",
|
|
117115
117241
|
"README.md",
|
|
117116
|
-
"
|
|
117242
|
+
"STATUS.md",
|
|
117243
|
+
"dashboard.html",
|
|
117244
|
+
"dashboard.manifest.json"
|
|
117117
117245
|
];
|
|
117118
|
-
|
|
117119
|
-
|
|
117120
|
-
|
|
117121
|
-
try {
|
|
117122
|
-
parsed = JSON.parse(manifest);
|
|
117123
|
-
} catch {
|
|
117124
|
-
return null;
|
|
117125
|
-
}
|
|
117126
|
-
const raw = parsed?.processSpec;
|
|
117127
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
117128
|
-
const spec = raw;
|
|
117129
|
-
const stages = (Array.isArray(spec.stages) ? spec.stages : []).map((s) => {
|
|
117130
|
-
if (typeof s === "string" && s.trim()) return { label: s.trim() };
|
|
117131
|
-
if (s && typeof s === "object" && !Array.isArray(s)) {
|
|
117132
|
-
const o = s;
|
|
117133
|
-
if (typeof o.label === "string" && o.label.trim()) {
|
|
117134
|
-
return {
|
|
117135
|
-
label: o.label.trim(),
|
|
117136
|
-
...typeof o.id === "string" && o.id.trim() ? { id: o.id.trim() } : {},
|
|
117137
|
-
...typeof o.detail === "string" && o.detail.trim() ? { detail: o.detail.trim() } : {}
|
|
117138
|
-
};
|
|
117139
|
-
}
|
|
117140
|
-
}
|
|
117141
|
-
return null;
|
|
117142
|
-
}).filter((s) => s !== null);
|
|
117143
|
-
const strings = (v) => (Array.isArray(v) ? v : []).filter((x) => typeof x === "string" && x.trim() !== "");
|
|
117144
|
-
const out = {
|
|
117145
|
-
stages,
|
|
117146
|
-
inputs: strings(spec.inputs),
|
|
117147
|
-
outputs: strings(spec.outputs),
|
|
117148
|
-
...typeof spec.title === "string" && spec.title.trim() ? { title: spec.title.trim() } : {}
|
|
117149
|
-
};
|
|
117150
|
-
return stages.length || out.inputs.length || out.outputs.length ? out : null;
|
|
117151
|
-
}
|
|
117152
|
-
function defaultDashboardManifest() {
|
|
117153
|
-
return JSON.stringify({ files: [...DEFAULT_DASHBOARD_FILES] });
|
|
117246
|
+
var KEY_EXTENSION = /\.[A-Za-z0-9]{1,8}$/;
|
|
117247
|
+
function filenameForKey(key) {
|
|
117248
|
+
return KEY_EXTENSION.test(key) ? key : `${key}.md`;
|
|
117154
117249
|
}
|
|
117155
|
-
function
|
|
117156
|
-
|
|
117250
|
+
function isSafeBasename(filename) {
|
|
117251
|
+
if (filename.length === 0) return false;
|
|
117252
|
+
if (filename.startsWith(".")) return false;
|
|
117253
|
+
if (filename.includes("/") || filename.includes("\\")) return false;
|
|
117254
|
+
if (filename === ".." || filename.includes("\0")) return false;
|
|
117255
|
+
return true;
|
|
117157
117256
|
}
|
|
117158
|
-
function
|
|
117159
|
-
return
|
|
117257
|
+
function isReservedName(filename) {
|
|
117258
|
+
return RESERVED_FIRE_FILENAMES.some((r) => r.toLowerCase() === filename.toLowerCase());
|
|
117160
117259
|
}
|
|
117161
|
-
|
|
117162
|
-
|
|
117163
|
-
const
|
|
117164
|
-
|
|
117165
|
-
|
|
117166
|
-
|
|
117167
|
-
|
|
117168
|
-
|
|
117169
|
-
|
|
117170
|
-
|
|
117171
|
-
|
|
117172
|
-
|
|
117173
|
-
|
|
117174
|
-
<style>
|
|
117175
|
-
:root { --bg:#f7f7fb; --panel:#fff; --border:#e3e3ee; --muted:#6b6b80; --accent:#5b5bd6; --ok:#1a7f37; --bad:#b42318; }
|
|
117176
|
-
* { box-sizing: border-box; }
|
|
117177
|
-
body { margin:0; font:14px/1.5 -apple-system, "Segoe UI", Roboto, sans-serif; background:var(--bg); color:#1d1d2b; padding:24px; }
|
|
117178
|
-
h1 { font-size:20px; margin:0 0 4px; }
|
|
117179
|
-
h2 { font-size:14px; margin:24px 0 8px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
|
|
117180
|
-
.sub { color:var(--muted); margin:0 0 16px; }
|
|
117181
|
-
.panel { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px; }
|
|
117182
|
-
.map { display:flex; flex-wrap:wrap; align-items:stretch; gap:8px; }
|
|
117183
|
-
.stage { background:var(--panel); border:1px solid var(--border); border-left:3px solid var(--accent); border-radius:10px; padding:10px 14px; min-width:140px; flex:1; }
|
|
117184
|
-
.stage .label { font-weight:600; }
|
|
117185
|
-
.stage .detail { color:var(--muted); font-size:12px; margin-top:2px; }
|
|
117186
|
-
.arrow { align-self:center; color:var(--muted); }
|
|
117187
|
-
.io { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
|
117188
|
-
ul { margin:6px 0 0; padding-left:18px; }
|
|
117189
|
-
table { width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--border); border-radius:12px; overflow:hidden; }
|
|
117190
|
-
th, td { text-align:left; padding:8px 12px; border-top:1px solid var(--border); font-size:13px; vertical-align:top; }
|
|
117191
|
-
thead th { border-top:none; background:var(--bg); color:var(--muted); font-weight:600; }
|
|
117192
|
-
.ok { color:var(--ok); } .bad { color:var(--bad); }
|
|
117193
|
-
details { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:10px 14px; margin-bottom:8px; }
|
|
117194
|
-
summary { cursor:pointer; font-weight:600; }
|
|
117195
|
-
pre { overflow-x:auto; font-size:12px; background:var(--bg); border-radius:8px; padding:10px; }
|
|
117196
|
-
.empty { color:var(--muted); font-style:italic; }
|
|
117197
|
-
#live { font-size:12px; color:var(--muted); float:right; }
|
|
117198
|
-
</style>
|
|
117199
|
-
</head>
|
|
117200
|
-
<body>
|
|
117201
|
-
<span id="live">loading\u2026</span>
|
|
117202
|
-
<h1 id="title"></h1>
|
|
117203
|
-
<p class="sub" id="desc"></p>
|
|
117204
|
-
|
|
117205
|
-
<h2>Process</h2>
|
|
117206
|
-
<div id="map" class="map panel"></div>
|
|
117207
|
-
|
|
117208
|
-
<h2>Inputs & outputs</h2>
|
|
117209
|
-
<div class="io">
|
|
117210
|
-
<div class="panel"><strong>Inputs</strong><ul id="inputs"></ul></div>
|
|
117211
|
-
<div class="panel"><strong>Outputs</strong><ul id="outputs"></ul></div>
|
|
117212
|
-
</div>
|
|
117213
|
-
|
|
117214
|
-
<h2>Rounds</h2>
|
|
117215
|
-
<div id="rounds"></div>
|
|
117216
|
-
|
|
117217
|
-
<h2>Files</h2>
|
|
117218
|
-
<div id="files"></div>
|
|
117219
|
-
|
|
117220
|
-
<script type="application/json" id="loop-seed">${seed}</script>
|
|
117221
|
-
<script>
|
|
117222
|
-
(function () {
|
|
117223
|
-
"use strict";
|
|
117224
|
-
var seed = JSON.parse(document.getElementById("loop-seed").textContent);
|
|
117225
|
-
document.getElementById("title").textContent = (seed.processSpec && seed.processSpec.title) || seed.slug;
|
|
117226
|
-
document.getElementById("desc").textContent = seed.descriptionShort || "";
|
|
117227
|
-
document.title = seed.slug + " \u2014 loop dashboard";
|
|
117228
|
-
|
|
117229
|
-
function el(tag, cls, text) {
|
|
117230
|
-
var e = document.createElement(tag);
|
|
117231
|
-
if (cls) e.className = cls;
|
|
117232
|
-
if (text !== undefined) e.textContent = text;
|
|
117233
|
-
return e;
|
|
117234
|
-
}
|
|
117235
|
-
|
|
117236
|
-
// \u2500\u2500 Process map + IO from the embedded spec (static \u2014 rendered once). \u2500\u2500
|
|
117237
|
-
var map = document.getElementById("map");
|
|
117238
|
-
var spec = seed.processSpec;
|
|
117239
|
-
if (spec && spec.stages && spec.stages.length) {
|
|
117240
|
-
spec.stages.forEach(function (s, i) {
|
|
117241
|
-
if (i > 0) map.appendChild(el("div", "arrow", "\\u2192"));
|
|
117242
|
-
var box = el("div", "stage");
|
|
117243
|
-
box.appendChild(el("div", "label", s.label));
|
|
117244
|
-
if (s.detail) box.appendChild(el("div", "detail", s.detail));
|
|
117245
|
-
map.appendChild(box);
|
|
117246
|
-
});
|
|
117247
|
-
} else {
|
|
117248
|
-
map.appendChild(el("span", "empty", "No process spec on this loop \\u2014 live files and rounds below."));
|
|
117260
|
+
var DOC_LIST_LIMIT = 500;
|
|
117261
|
+
async function discoverMemoryDocs(sdk, apiKey, slug) {
|
|
117262
|
+
const out = { own: [], shared: [], failed: [], truncated: [] };
|
|
117263
|
+
for (const scope of ["member", "shared"]) {
|
|
117264
|
+
const res = await sdk.loops.memory.listDocs(apiKey, slug, { scope, limit: DOC_LIST_LIMIT });
|
|
117265
|
+
if (res.status !== "ok") {
|
|
117266
|
+
if (scope === "member" && isNotFound(res)) continue;
|
|
117267
|
+
out.failed.push(scope);
|
|
117268
|
+
continue;
|
|
117269
|
+
}
|
|
117270
|
+
if (res.items.length >= DOC_LIST_LIMIT) out.truncated.push(scope);
|
|
117271
|
+
if (scope === "member") out.own = res.items;
|
|
117272
|
+
else out.shared = res.items;
|
|
117249
117273
|
}
|
|
117250
|
-
|
|
117251
|
-
|
|
117252
|
-
|
|
117253
|
-
|
|
117254
|
-
|
|
117274
|
+
return out;
|
|
117275
|
+
}
|
|
117276
|
+
function planStateDocs(stateDocs, discovered) {
|
|
117277
|
+
const manifest = normalizeMemoryManifest(stateDocs);
|
|
117278
|
+
const candidates = [];
|
|
117279
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
117280
|
+
for (const d of manifest?.docs ?? []) {
|
|
117281
|
+
claimed.add(d.key);
|
|
117282
|
+
candidates.push({ key: d.key, scope: d.scope, declared: true });
|
|
117255
117283
|
}
|
|
117256
|
-
|
|
117257
|
-
|
|
117258
|
-
|
|
117259
|
-
|
|
117260
|
-
|
|
117261
|
-
|
|
117262
|
-
|
|
117263
|
-
|
|
117264
|
-
|
|
117265
|
-
try { rows.push(JSON.parse(line)); } catch (e) { /* skip malformed line */ }
|
|
117266
|
-
});
|
|
117267
|
-
return rows;
|
|
117284
|
+
for (const [rows, scope] of [
|
|
117285
|
+
[discovered.own, "member"],
|
|
117286
|
+
[discovered.shared, "shared"]
|
|
117287
|
+
]) {
|
|
117288
|
+
for (const m of rows) {
|
|
117289
|
+
if (claimed.has(m.key)) continue;
|
|
117290
|
+
claimed.add(m.key);
|
|
117291
|
+
candidates.push({ key: m.key, scope, declared: false });
|
|
117292
|
+
}
|
|
117268
117293
|
}
|
|
117269
|
-
|
|
117270
|
-
|
|
117271
|
-
|
|
117272
|
-
|
|
117273
|
-
|
|
117274
|
-
|
|
117275
|
-
|
|
117276
|
-
|
|
117294
|
+
const docs = [];
|
|
117295
|
+
const skipped = [];
|
|
117296
|
+
const taken = /* @__PURE__ */ new Map();
|
|
117297
|
+
for (const c of candidates) {
|
|
117298
|
+
const filename = filenameForKey(c.key);
|
|
117299
|
+
if (!isSafeBasename(filename)) {
|
|
117300
|
+
skipped.push({ key: c.key, reason: `'${filename}' is not a safe basename` });
|
|
117301
|
+
continue;
|
|
117277
117302
|
}
|
|
117278
|
-
|
|
117279
|
-
|
|
117280
|
-
|
|
117281
|
-
|
|
117282
|
-
|
|
117283
|
-
|
|
117284
|
-
thead.appendChild(hr); table.appendChild(thead);
|
|
117285
|
-
var tbody = document.createElement("tbody");
|
|
117286
|
-
var n = Math.max(fires.length, rounds.length);
|
|
117287
|
-
for (var i = n - 1; i >= 0; i--) { // newest first
|
|
117288
|
-
var f = fires[i] || {};
|
|
117289
|
-
var r = rounds[i];
|
|
117290
|
-
var tr = document.createElement("tr");
|
|
117291
|
-
tr.appendChild(el("td", null, f.ts || (r && r.ts) || "\\u2014"));
|
|
117292
|
-
tr.appendChild(el("td", null, f.duration_s != null ? f.duration_s + "s" : "\\u2014"));
|
|
117293
|
-
tr.appendChild(el("td", null, f.turns != null ? String(f.turns) : "\\u2014"));
|
|
117294
|
-
tr.appendChild(el("td", null, f.tokens_total != null ? Number(f.tokens_total).toLocaleString() : "\\u2014"));
|
|
117295
|
-
tr.appendChild(el("td", f.exit === 0 ? "ok" : f.exit != null ? "bad" : null, f.exit != null ? String(f.exit) : "\\u2014"));
|
|
117296
|
-
var detail = el("td");
|
|
117297
|
-
if (f.launch_failed) { detail.textContent = "launch failed \\u2014 " + (f.reason || "unknown reason"); }
|
|
117298
|
-
else if (r) { var pre = document.createElement("pre"); pre.textContent = JSON.stringify(r, null, 1); detail.appendChild(pre); }
|
|
117299
|
-
else detail.textContent = "\\u2014";
|
|
117300
|
-
tr.appendChild(detail);
|
|
117301
|
-
tbody.appendChild(tr);
|
|
117303
|
+
if (isReservedName(filename)) {
|
|
117304
|
+
skipped.push({
|
|
117305
|
+
key: c.key,
|
|
117306
|
+
reason: `'${filename}' is a file the wrapper writes itself \u2014 rename the key`
|
|
117307
|
+
});
|
|
117308
|
+
continue;
|
|
117302
117309
|
}
|
|
117303
|
-
|
|
117304
|
-
|
|
117310
|
+
const owner = taken.get(filename.toLowerCase());
|
|
117311
|
+
if (owner !== void 0) {
|
|
117312
|
+
skipped.push({ key: c.key, reason: `'${filename}' is already taken by key '${owner}'` });
|
|
117313
|
+
continue;
|
|
117314
|
+
}
|
|
117315
|
+
taken.set(filename.toLowerCase(), c.key);
|
|
117316
|
+
docs.push({ key: c.key, filename, scope: c.scope, declared: c.declared });
|
|
117305
117317
|
}
|
|
117306
|
-
|
|
117307
|
-
|
|
117308
|
-
|
|
117309
|
-
|
|
117310
|
-
|
|
117311
|
-
|
|
117312
|
-
|
|
117313
|
-
|
|
117314
|
-
|
|
117315
|
-
|
|
117316
|
-
|
|
117317
|
-
|
|
117318
|
-
|
|
117319
|
-
|
|
117318
|
+
return { docs, skipped, manifest };
|
|
117319
|
+
}
|
|
117320
|
+
function describeSource(doc) {
|
|
117321
|
+
const from14 = doc.scope === "shared" ? "shared" : "own";
|
|
117322
|
+
const notes = [];
|
|
117323
|
+
if (doc.created) notes.push(doc.seeded ? "created, seeded from shared" : "created");
|
|
117324
|
+
if (!doc.declared) notes.push("not in manifest");
|
|
117325
|
+
return `${doc.filename} \u2190 ${from14}${notes.length ? ` (${notes.join(", ")})` : ""}`;
|
|
117326
|
+
}
|
|
117327
|
+
async function readScoped(sdk, apiKey, slug, key, scope) {
|
|
117328
|
+
const res = await sdk.loops.memory.getDoc(apiKey, slug, key, { scope });
|
|
117329
|
+
if (res.status === "ok") return { status: "ok", body: res.doc.content ?? "" };
|
|
117330
|
+
if (isNotFound(res)) return { status: "absent" };
|
|
117331
|
+
return { status: "failed" };
|
|
117332
|
+
}
|
|
117333
|
+
async function fetchStateDocs(sdk, apiKey, slug, docs) {
|
|
117334
|
+
const fetched = [];
|
|
117335
|
+
const failed = [];
|
|
117336
|
+
for (const doc of docs) {
|
|
117337
|
+
const read = await readScoped(sdk, apiKey, slug, doc.key, doc.scope);
|
|
117338
|
+
if (read.status === "ok") {
|
|
117339
|
+
fetched.push({ ...doc, body: read.body, created: false, seeded: false });
|
|
117340
|
+
continue;
|
|
117341
|
+
}
|
|
117342
|
+
if (read.status === "failed") {
|
|
117343
|
+
failed.push({ key: doc.key, reason: `could not read the ${doc.scope} row` });
|
|
117344
|
+
continue;
|
|
117345
|
+
}
|
|
117346
|
+
if (!doc.declared) {
|
|
117347
|
+
failed.push({ key: doc.key, reason: `the ${doc.scope} row vanished after the listing` });
|
|
117348
|
+
continue;
|
|
117349
|
+
}
|
|
117350
|
+
let seed = "";
|
|
117351
|
+
let seeded = false;
|
|
117352
|
+
if (doc.scope === "member") {
|
|
117353
|
+
const shared = await readScoped(sdk, apiKey, slug, doc.key, "shared");
|
|
117354
|
+
if (shared.status === "failed") {
|
|
117355
|
+
failed.push({ key: doc.key, reason: "could not read the shared row to seed the member row" });
|
|
117356
|
+
continue;
|
|
117357
|
+
}
|
|
117358
|
+
if (shared.status === "ok") {
|
|
117359
|
+
seed = shared.body;
|
|
117360
|
+
seeded = true;
|
|
117361
|
+
}
|
|
117362
|
+
}
|
|
117363
|
+
const put = await sdk.loops.memory.putDoc(apiKey, slug, doc.key, { content: seed, scope: doc.scope });
|
|
117364
|
+
if (put.status !== "ok") {
|
|
117365
|
+
failed.push({ key: doc.key, reason: `could not create the ${doc.scope} row (${put.error ?? "write refused"})` });
|
|
117366
|
+
continue;
|
|
117367
|
+
}
|
|
117368
|
+
fetched.push({ ...doc, body: seed, created: true, seeded });
|
|
117320
117369
|
}
|
|
117321
|
-
|
|
117322
|
-
|
|
117323
|
-
|
|
117324
|
-
|
|
117325
|
-
|
|
117326
|
-
|
|
117327
|
-
|
|
117328
|
-
|
|
117329
|
-
|
|
117330
|
-
document.getElementById("live").textContent = "server stopped";
|
|
117331
|
-
});
|
|
117370
|
+
return { fetched, failed };
|
|
117371
|
+
}
|
|
117372
|
+
function isNotFound(res) {
|
|
117373
|
+
if (typeof res !== "object" || res === null) return false;
|
|
117374
|
+
return res.code === 404;
|
|
117375
|
+
}
|
|
117376
|
+
function docMergeRefusal(boot, fresh, next, cleanAppend) {
|
|
117377
|
+
if (next.trim() === "" && fresh.trim() !== "") {
|
|
117378
|
+
return "would TRUNCATE the stored document to empty while the record still holds content \u2014 an emptied file is not an instruction to erase the record";
|
|
117332
117379
|
}
|
|
117333
|
-
|
|
117334
|
-
|
|
117335
|
-
}
|
|
117336
|
-
|
|
117337
|
-
</body>
|
|
117338
|
-
</html>
|
|
117339
|
-
`;
|
|
117380
|
+
if (!cleanAppend && fresh !== boot && headings(fresh).length === 0) {
|
|
117381
|
+
return "this fire rewrote the document rather than appending to it, the record MOVED under us, and the document has no headings for the shrink guard to count \u2014 so accepting this write would silently discard whatever the concurrent writer added";
|
|
117382
|
+
}
|
|
117383
|
+
return void 0;
|
|
117340
117384
|
}
|
|
117341
|
-
function
|
|
117342
|
-
const
|
|
117343
|
-
|
|
117344
|
-
|
|
117345
|
-
|
|
117346
|
-
|
|
117347
|
-
|
|
117385
|
+
async function shipBackStateDocs(sdk, apiKey, slug, dir, boot) {
|
|
117386
|
+
const outcomes = [];
|
|
117387
|
+
for (const doc of boot) {
|
|
117388
|
+
const path2 = join7(dir, doc.filename);
|
|
117389
|
+
if (!existsSync9(path2)) {
|
|
117390
|
+
outcomes.push({ key: doc.key, outcome: "unchanged" });
|
|
117391
|
+
continue;
|
|
117392
|
+
}
|
|
117393
|
+
const materialized = readFileSync8(path2, "utf-8");
|
|
117394
|
+
const reread = await readScoped(sdk, apiKey, slug, doc.key, doc.scope);
|
|
117395
|
+
const fresh = reread.status === "ok" ? reread.body : reread.status === "absent" && doc.body === "" ? "" : void 0;
|
|
117396
|
+
if (fresh === void 0) {
|
|
117397
|
+
outcomes.push({
|
|
117398
|
+
key: doc.key,
|
|
117399
|
+
outcome: "failed",
|
|
117400
|
+
detail: `could not re-read the ${doc.scope} row; ${path2} kept for recovery`
|
|
117401
|
+
});
|
|
117402
|
+
continue;
|
|
117403
|
+
}
|
|
117404
|
+
const merged = mergeConstraints(doc.body, materialized, fresh);
|
|
117405
|
+
if (merged.refusal) {
|
|
117406
|
+
outcomes.push({ key: doc.key, outcome: "refused", detail: merged.refusal });
|
|
117407
|
+
continue;
|
|
117408
|
+
}
|
|
117409
|
+
if (merged.next === void 0) {
|
|
117410
|
+
outcomes.push({ key: doc.key, outcome: "unchanged" });
|
|
117411
|
+
continue;
|
|
117412
|
+
}
|
|
117413
|
+
const docRefusal = docMergeRefusal(doc.body, fresh, merged.next, merged.cleanAppend);
|
|
117414
|
+
if (docRefusal) {
|
|
117415
|
+
outcomes.push({
|
|
117416
|
+
key: doc.key,
|
|
117417
|
+
outcome: "refused",
|
|
117418
|
+
detail: `${docRefusal}; ${path2} kept for recovery`
|
|
117419
|
+
});
|
|
117420
|
+
continue;
|
|
117421
|
+
}
|
|
117422
|
+
const put = await sdk.loops.memory.putDoc(apiKey, slug, doc.key, {
|
|
117423
|
+
content: merged.next,
|
|
117424
|
+
scope: doc.scope
|
|
117425
|
+
});
|
|
117426
|
+
if (put.status !== "ok") {
|
|
117427
|
+
outcomes.push({
|
|
117428
|
+
key: doc.key,
|
|
117429
|
+
outcome: "failed",
|
|
117430
|
+
detail: `write to the ${doc.scope} row rejected; ${path2} kept for recovery`
|
|
117431
|
+
});
|
|
117432
|
+
continue;
|
|
117433
|
+
}
|
|
117434
|
+
const after = await sdk.loops.memory.getDoc(apiKey, slug, doc.key, { scope: doc.scope });
|
|
117435
|
+
if (after.status !== "ok") {
|
|
117436
|
+
outcomes.push({ key: doc.key, outcome: "shipped", detail: "could not verify it landed" });
|
|
117437
|
+
continue;
|
|
117348
117438
|
}
|
|
117439
|
+
const afterText = after.doc.content ?? "";
|
|
117440
|
+
const missing = merged.addedBlocks.length > 0 ? verifyLanded(afterText, merged.addedBlocks) : afterText === merged.next ? [] : ["<rewrite did not survive>"];
|
|
117441
|
+
outcomes.push(
|
|
117442
|
+
missing.length === 0 ? { key: doc.key, outcome: "shipped" } : {
|
|
117443
|
+
key: doc.key,
|
|
117444
|
+
outcome: "failed",
|
|
117445
|
+
detail: `${missing.length} section(s) did not survive a concurrent write; ${path2} kept`
|
|
117446
|
+
}
|
|
117447
|
+
);
|
|
117349
117448
|
}
|
|
117350
|
-
|
|
117351
|
-
loop2.dashboardHtml = renderLoopDashboardTemplate({
|
|
117352
|
-
slug: typeof loop2.slug === "string" ? loop2.slug : "loop",
|
|
117353
|
-
descriptionShort: typeof loop2.descriptionShort === "string" ? loop2.descriptionShort : void 0,
|
|
117354
|
-
processSpec: parseProcessSpec(manifest)
|
|
117355
|
-
});
|
|
117356
|
-
if (!manifest || !manifest.trim()) loop2.dashboardManifest = defaultDashboardManifest();
|
|
117449
|
+
return outcomes;
|
|
117357
117450
|
}
|
|
117358
117451
|
|
|
117359
|
-
// src/
|
|
117360
|
-
|
|
117361
|
-
|
|
117362
|
-
|
|
117452
|
+
// src/mcp-server/materialize-task.ts
|
|
117453
|
+
var TASK_DEFINITION_FILES = [
|
|
117454
|
+
["skill", "SKILL.md", "markdownBody"],
|
|
117455
|
+
["vision", "VISION.md", "visionMd"],
|
|
117456
|
+
["constraints", "CONSTRAINTS.md", "constraintsMd"],
|
|
117457
|
+
["readme", "README.md", "readmeMd"],
|
|
117458
|
+
["dashboardHtml", "dashboard.html", "dashboardHtml"],
|
|
117459
|
+
["dashboardManifest", "dashboard.manifest.json", "dashboardManifest"]
|
|
117460
|
+
];
|
|
117461
|
+
function materializeTask(task, runId) {
|
|
117462
|
+
const dir = loopFireDir(task.slug, runId);
|
|
117463
|
+
mkdirSync6(dir, { recursive: true, mode: 448 });
|
|
117464
|
+
const files = {};
|
|
117465
|
+
const skipped = [];
|
|
117466
|
+
for (const [label2, filename, field] of TASK_DEFINITION_FILES) {
|
|
117467
|
+
const body = task[field];
|
|
117468
|
+
if (typeof body === "string" && body.length > 0) {
|
|
117469
|
+
writeFileSync8(join8(dir, filename), body, { mode: 384 });
|
|
117470
|
+
files[label2] = filename;
|
|
117471
|
+
} else {
|
|
117472
|
+
skipped.push(label2);
|
|
117473
|
+
}
|
|
117474
|
+
}
|
|
117475
|
+
writeFileSync8(
|
|
117476
|
+
join8(dir, "STATUS.md"),
|
|
117477
|
+
`# STATUS \u2014 ${task.slug}
|
|
117363
117478
|
|
|
117364
|
-
|
|
117365
|
-
|
|
117366
|
-
|
|
117367
|
-
|
|
117479
|
+
task_id: ${task.id}
|
|
117480
|
+
run_id: ${runId}
|
|
117481
|
+
started: pending
|
|
117482
|
+
queue: not started
|
|
117483
|
+
`,
|
|
117484
|
+
{ mode: 384 }
|
|
117485
|
+
);
|
|
117486
|
+
files.status = "STATUS.md";
|
|
117487
|
+
return { dir, files, skipped };
|
|
117368
117488
|
}
|
|
117369
|
-
function
|
|
117370
|
-
const
|
|
117371
|
-
|
|
117372
|
-
|
|
117373
|
-
|
|
117374
|
-
const
|
|
117375
|
-
|
|
117376
|
-
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) inFence = !inFence;
|
|
117377
|
-
else if (!inFence) {
|
|
117378
|
-
const m = /^###\s+(.*)$/.exec(bare);
|
|
117379
|
-
if (m) out.push({ heading: m[1].trim(), start: offset });
|
|
117489
|
+
async function materializeMemoryDocs(sdk, apiKey, task, dir) {
|
|
117490
|
+
const files = {};
|
|
117491
|
+
const notes = [];
|
|
117492
|
+
try {
|
|
117493
|
+
const discovered = await discoverMemoryDocs(sdk, apiKey, task.slug);
|
|
117494
|
+
for (const scope of discovered.failed) {
|
|
117495
|
+
notes.push(`could not list the ${scope} memory docs \u2014 undeclared docs there are not materialized`);
|
|
117380
117496
|
}
|
|
117381
|
-
|
|
117497
|
+
const plan = planStateDocs(task.stateDocs ?? null, discovered);
|
|
117498
|
+
for (const s of plan.skipped) {
|
|
117499
|
+
notes.push(`memory doc '${s.key}' not materialized: ${s.reason}`);
|
|
117500
|
+
}
|
|
117501
|
+
if (plan.docs.length === 0) return { files, notes };
|
|
117502
|
+
const { fetched, failed } = await fetchStateDocs(sdk, apiKey, task.slug, plan.docs);
|
|
117503
|
+
for (const f of failed) {
|
|
117504
|
+
notes.push(`memory doc '${f.key}' not materialized: ${f.reason}`);
|
|
117505
|
+
}
|
|
117506
|
+
for (const doc of fetched) {
|
|
117507
|
+
writeFileSync8(join8(dir, doc.filename), doc.body, { mode: 384 });
|
|
117508
|
+
files[`doc:${doc.key}`] = doc.filename;
|
|
117509
|
+
notes.push(describeSource(doc));
|
|
117510
|
+
}
|
|
117511
|
+
return { files, notes };
|
|
117512
|
+
} catch (err) {
|
|
117513
|
+
notes.push(
|
|
117514
|
+
`memory docs not materialized (${err instanceof Error ? err.message : String(err)}) \u2014 the run continues; read them with taskMemoryGet instead`
|
|
117515
|
+
);
|
|
117516
|
+
return { files, notes };
|
|
117382
117517
|
}
|
|
117383
|
-
return { items: out, unbalanced: inFence };
|
|
117384
|
-
}
|
|
117385
|
-
function headingsIgnoringFences(text) {
|
|
117386
|
-
const out = [];
|
|
117387
|
-
for (const line of text.split("\n")) {
|
|
117388
|
-
const bare = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
117389
|
-
const m = /^ {0,3}###\s+(.*)$/.exec(bare);
|
|
117390
|
-
if (m) out.push(m[1].trim());
|
|
117391
|
-
}
|
|
117392
|
-
return out;
|
|
117393
|
-
}
|
|
117394
|
-
function headings(text) {
|
|
117395
|
-
return scanHeadings(text).map((h) => h.heading);
|
|
117396
|
-
}
|
|
117397
|
-
function countByHeading(list2) {
|
|
117398
|
-
const m = /* @__PURE__ */ new Map();
|
|
117399
|
-
for (const h of list2) m.set(h, (m.get(h) ?? 0) + 1);
|
|
117400
|
-
return m;
|
|
117401
|
-
}
|
|
117402
|
-
function sections(fragment) {
|
|
117403
|
-
const found = scanHeadings(fragment);
|
|
117404
|
-
if (found.length === 0) return { preamble: fragment, blocks: [] };
|
|
117405
|
-
const preamble = fragment.slice(0, found[0].start);
|
|
117406
|
-
const blocks = found.map((h, i) => ({
|
|
117407
|
-
heading: h.heading,
|
|
117408
|
-
text: fragment.slice(h.start, i + 1 < found.length ? found[i + 1].start : fragment.length)
|
|
117409
|
-
}));
|
|
117410
|
-
return { preamble, blocks };
|
|
117411
|
-
}
|
|
117412
|
-
function norm(s) {
|
|
117413
|
-
return s.replace(/\r\n/g, "\n").trimEnd();
|
|
117414
|
-
}
|
|
117415
|
-
function mergeConstraints(boot, materialized, fresh) {
|
|
117416
|
-
if (materialized === boot) return { next: void 0, added: [], addedBlocks: [], cleanAppend: true };
|
|
117417
|
-
const recordUnchanged = fresh === boot;
|
|
117418
|
-
let next;
|
|
117419
|
-
let cleanAppend;
|
|
117420
|
-
if (materialized.startsWith(boot)) {
|
|
117421
|
-
const suffix = materialized.slice(boot.length);
|
|
117422
|
-
if (recordUnchanged) {
|
|
117423
|
-
next = materialized;
|
|
117424
|
-
} else {
|
|
117425
|
-
const { preamble, blocks } = sections(suffix);
|
|
117426
|
-
const present = new Set(sections(fresh).blocks.map((b) => norm(b.text)));
|
|
117427
|
-
const novel = blocks.filter((b) => !present.has(norm(b.text)));
|
|
117428
|
-
const keepPreamble = preamble.trim().length > 0 && !norm(fresh).endsWith(norm(preamble));
|
|
117429
|
-
const appended = (keepPreamble ? preamble : "") + novel.map((b) => b.text).join("");
|
|
117430
|
-
const seam = appended.length > 0 && fresh.length > 0 && !fresh.endsWith("\n") ? "\n" : "";
|
|
117431
|
-
next = fresh + seam + appended;
|
|
117432
|
-
}
|
|
117433
|
-
cleanAppend = true;
|
|
117434
|
-
} else {
|
|
117435
|
-
next = materialized;
|
|
117436
|
-
cleanAppend = false;
|
|
117437
|
-
}
|
|
117438
|
-
const guardCounts = (text) => countByHeading(headingsIgnoringFences(text));
|
|
117439
|
-
const freshCounts = guardCounts(fresh);
|
|
117440
|
-
const nextCounts = guardCounts(next);
|
|
117441
|
-
const countAll = (m) => [...m.values()].reduce((a, b) => a + b, 0);
|
|
117442
|
-
const lost = [];
|
|
117443
|
-
for (const [h, n] of freshCounts) {
|
|
117444
|
-
const kept = nextCounts.get(h) ?? 0;
|
|
117445
|
-
if (kept < n) lost.push(`${h}${n > 1 ? ` (${n}\u2192${kept})` : ""}`);
|
|
117446
|
-
}
|
|
117447
|
-
if (lost.length > 0) {
|
|
117448
|
-
const before = countAll(freshCounts);
|
|
117449
|
-
const after = countAll(nextCounts);
|
|
117450
|
-
return {
|
|
117451
|
-
refusal: `ship-back REFUSED: ${lost.length} section(s) present on the record would be lost by this write (${before} \u2192 ${after} headings). Lost: ${lost.slice(0, 5).map((h) => JSON.stringify(h)).join(", ")}${lost.length > 5 ? ` \u2026and ${lost.length - 5} more` : ""}. TO RECOVER: put the bare \`### \` heading line(s) named above back into the refused document and re-run \u2014 the guard counts headings, so an empty section is enough to let the write through.`,
|
|
117452
|
-
added: [],
|
|
117453
|
-
addedBlocks: [],
|
|
117454
|
-
cleanAppend
|
|
117455
|
-
};
|
|
117456
|
-
}
|
|
117457
|
-
if (!cleanAppend) {
|
|
117458
|
-
const bootBlocks = new Set(sections(boot).blocks.map((b) => norm(b.text)));
|
|
117459
|
-
const nextBlocks = new Set(sections(next).blocks.map((b) => norm(b.text)));
|
|
117460
|
-
const clobbered = sections(fresh).blocks.filter((b) => !bootBlocks.has(norm(b.text))).filter((b) => !nextBlocks.has(norm(b.text)));
|
|
117461
|
-
if (clobbered.length > 0) {
|
|
117462
|
-
return {
|
|
117463
|
-
refusal: `ship-back REFUSED: ${clobbered.length} section(s) added to the record while this fire was running would be REWRITTEN by this fire's document, which never saw them. Affected: ${clobbered.slice(0, 5).map((b) => JSON.stringify(b.heading)).join(", ")}${clobbered.length > 5 ? ` \u2026and ${clobbered.length - 5} more` : ""}. TO RECOVER: copy those section(s) verbatim off the record into the refused document and re-run \u2014 here it is the section BODY that must survive, so a bare heading is not enough on this branch.`,
|
|
117464
|
-
added: [],
|
|
117465
|
-
addedBlocks: [],
|
|
117466
|
-
cleanAppend
|
|
117467
|
-
};
|
|
117468
|
-
}
|
|
117469
|
-
}
|
|
117470
|
-
if (next === fresh) return { next: void 0, added: [], addedBlocks: [], cleanAppend };
|
|
117471
|
-
const added = [];
|
|
117472
|
-
const remaining = new Map(freshCounts);
|
|
117473
|
-
for (const h of headings(next)) {
|
|
117474
|
-
const owed = remaining.get(h) ?? 0;
|
|
117475
|
-
if (owed > 0) remaining.set(h, owed - 1);
|
|
117476
|
-
else added.push(h);
|
|
117477
|
-
}
|
|
117478
|
-
const freshBlocks = new Set(sections(fresh).blocks.map((b) => norm(b.text)));
|
|
117479
|
-
const addedBlocks = sections(next).blocks.map((b) => norm(b.text)).filter((t) => !freshBlocks.has(t));
|
|
117480
|
-
return { next, added, addedBlocks, cleanAppend };
|
|
117481
|
-
}
|
|
117482
|
-
function verifyLanded(afterWrite, expectedBlocks) {
|
|
117483
|
-
const have = new Set(sections(afterWrite).blocks.map((b) => norm(b.text)));
|
|
117484
|
-
return expectedBlocks.filter((t) => !have.has(t));
|
|
117485
117518
|
}
|
|
117486
117519
|
|
|
117487
|
-
// src/
|
|
117520
|
+
// src/mcp-server/index.ts
|
|
117521
|
+
import { existsSync as existsSync12 } from "fs";
|
|
117522
|
+
|
|
117523
|
+
// src/loops/dashboard.ts
|
|
117488
117524
|
init_esm_shims();
|
|
117489
|
-
|
|
117490
|
-
|
|
117491
|
-
|
|
117492
|
-
|
|
117493
|
-
|
|
117494
|
-
|
|
117495
|
-
|
|
117496
|
-
|
|
117497
|
-
|
|
117525
|
+
import { createServer as createServer2 } from "http";
|
|
117526
|
+
import * as realFs from "fs";
|
|
117527
|
+
import { spawn as realSpawn } from "child_process";
|
|
117528
|
+
import { join as join9 } from "path";
|
|
117529
|
+
var DEFAULT_PORT = 4477;
|
|
117530
|
+
var DASHBOARD_PORT_ENV = "AMETYST_LOOP_DASHBOARD_PORT";
|
|
117531
|
+
var DASHBOARD_NO_OPEN_ENV = "AMETYST_DASHBOARD_NO_OPEN";
|
|
117532
|
+
var NO_DASHBOARD_MESSAGE = "no dashboard on this task \u2014 ask your agent to create one";
|
|
117533
|
+
var MAX_PORT_RETRIES = 20;
|
|
117534
|
+
var SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
117535
|
+
function parseManifestFiles(manifest) {
|
|
117536
|
+
if (typeof manifest !== "string" || !manifest.trim()) return [];
|
|
117537
|
+
let parsed;
|
|
117538
|
+
try {
|
|
117539
|
+
parsed = JSON.parse(manifest);
|
|
117540
|
+
} catch {
|
|
117541
|
+
return [];
|
|
117542
|
+
}
|
|
117543
|
+
const list2 = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.files) ? parsed.files : [];
|
|
117544
|
+
return list2.filter((f) => typeof f === "string" && SAFE_NAME.test(f));
|
|
117498
117545
|
}
|
|
117499
|
-
function
|
|
117500
|
-
|
|
117501
|
-
|
|
117502
|
-
if (key.startsWith(".")) return false;
|
|
117503
|
-
if (key.includes("/") || key.includes("\\") || key.includes("\0")) return false;
|
|
117504
|
-
return true;
|
|
117546
|
+
function dashboardPort(env = process.env) {
|
|
117547
|
+
const raw = Number(env[DASHBOARD_PORT_ENV]);
|
|
117548
|
+
return Number.isInteger(raw) && raw > 0 && raw < 65536 ? raw : DEFAULT_PORT;
|
|
117505
117549
|
}
|
|
117506
|
-
function
|
|
117507
|
-
|
|
117508
|
-
|
|
117509
|
-
|
|
117510
|
-
|
|
117511
|
-
|
|
117512
|
-
|
|
117513
|
-
|
|
117514
|
-
|
|
117515
|
-
|
|
117550
|
+
function openDashboardInBrowser(url2, deps = {}) {
|
|
117551
|
+
const env = deps.env ?? process.env;
|
|
117552
|
+
if (env[DASHBOARD_NO_OPEN_ENV] === "1") return false;
|
|
117553
|
+
const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
|
|
117554
|
+
if (!isTTY) return false;
|
|
117555
|
+
const platform = deps.platform ?? process.platform;
|
|
117556
|
+
const cmd = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : null;
|
|
117557
|
+
if (!cmd) return false;
|
|
117558
|
+
const spawn4 = deps.spawn ?? realSpawn;
|
|
117559
|
+
try {
|
|
117560
|
+
const child = spawn4(cmd, [url2], { stdio: "ignore", detached: true });
|
|
117561
|
+
child.once?.("error", () => {
|
|
117562
|
+
});
|
|
117563
|
+
child.unref?.();
|
|
117564
|
+
return true;
|
|
117565
|
+
} catch {
|
|
117566
|
+
return false;
|
|
117516
117567
|
}
|
|
117517
117568
|
}
|
|
117518
|
-
function
|
|
117519
|
-
|
|
117520
|
-
if (
|
|
117521
|
-
|
|
117522
|
-
|
|
117523
|
-
|
|
117524
|
-
|
|
117525
|
-
|
|
117526
|
-
|
|
117527
|
-
|
|
117528
|
-
|
|
117529
|
-
|
|
117530
|
-
|
|
117531
|
-
|
|
117532
|
-
|
|
117533
|
-
|
|
117534
|
-
|
|
117535
|
-
|
|
117536
|
-
|
|
117537
|
-
|
|
117538
|
-
|
|
117539
|
-
|
|
117540
|
-
|
|
117541
|
-
|
|
117542
|
-
|
|
117543
|
-
|
|
117544
|
-
|
|
117545
|
-
|
|
117546
|
-
|
|
117547
|
-
|
|
117548
|
-
|
|
117549
|
-
|
|
117550
|
-
|
|
117551
|
-
|
|
117552
|
-
|
|
117553
|
-
|
|
117554
|
-
|
|
117555
|
-
|
|
117556
|
-
|
|
117557
|
-
|
|
117558
|
-
|
|
117559
|
-
|
|
117560
|
-
|
|
117561
|
-
|
|
117562
|
-
|
|
117563
|
-
}
|
|
117564
|
-
|
|
117565
|
-
|
|
117566
|
-
|
|
117567
|
-
|
|
117568
|
-
|
|
117569
|
-
}
|
|
117570
|
-
const records = [];
|
|
117571
|
-
const seenKinds = /* @__PURE__ */ new Set();
|
|
117572
|
-
for (const [i, raw] of obj.records.entries()) {
|
|
117573
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
117574
|
-
return { ok: false, error: `stateDocs.records[${i}] must be an object {kind, scope}.` };
|
|
117575
|
-
}
|
|
117576
|
-
const r = raw;
|
|
117577
|
-
const extraRec = Object.keys(r).filter((k) => k !== "kind" && k !== "scope");
|
|
117578
|
-
if (extraRec.length > 0) {
|
|
117579
|
-
return { ok: false, error: `stateDocs.records[${i}] carries unknown propert${extraRec.length === 1 ? "y" : "ies"} ${extraRec.join(", ")} \u2014 a record is exactly {kind, scope}.` };
|
|
117580
|
-
}
|
|
117581
|
-
if (!isRecordKindToken(r.kind)) {
|
|
117582
|
-
return {
|
|
117583
|
-
ok: false,
|
|
117584
|
-
error: `stateDocs.records[${i}].kind must be a token matching [a-z0-9-]{1,64} \u2014 a reserved kind (${RESERVED_RECORD_KINDS.join(", ")}) or a free kind of the task's own (e.g. "pbi").`
|
|
117585
|
-
};
|
|
117586
|
-
}
|
|
117587
|
-
if (seenKinds.has(r.kind)) {
|
|
117588
|
-
return { ok: false, error: `stateDocs.records declares "${r.kind}" more than once.` };
|
|
117569
|
+
function startDashboardServer(args) {
|
|
117570
|
+
const html = args.loop.dashboardHtml;
|
|
117571
|
+
if (typeof html !== "string" || !html) return Promise.resolve(null);
|
|
117572
|
+
const fs = args.deps?.fs ?? realFs;
|
|
117573
|
+
const log = args.deps?.log ?? ((line) => console.error(line));
|
|
117574
|
+
const make = args.deps?.createServer ?? createServer2;
|
|
117575
|
+
const basePort = args.port ?? dashboardPort();
|
|
117576
|
+
const files = parseManifestFiles(args.loop.dashboardManifest);
|
|
117577
|
+
const handler = (req, res) => {
|
|
117578
|
+
try {
|
|
117579
|
+
const url2 = (req.url ?? "/").split("?")[0];
|
|
117580
|
+
if (req.method === "GET" && url2 === "/") {
|
|
117581
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
117582
|
+
res.end(html);
|
|
117583
|
+
return;
|
|
117584
|
+
}
|
|
117585
|
+
if (req.method === "GET" && url2 === "/data") {
|
|
117586
|
+
const data = {};
|
|
117587
|
+
for (const name of files) {
|
|
117588
|
+
try {
|
|
117589
|
+
const p = join9(args.loopDir, name);
|
|
117590
|
+
if (fs.existsSync(p)) data[name] = fs.readFileSync(p, "utf-8");
|
|
117591
|
+
} catch {
|
|
117592
|
+
}
|
|
117593
|
+
}
|
|
117594
|
+
const state = {};
|
|
117595
|
+
try {
|
|
117596
|
+
const p = join9(args.loopDir, ".state", "fires.jsonl");
|
|
117597
|
+
if (fs.existsSync(p)) state["fires.jsonl"] = fs.readFileSync(p, "utf-8");
|
|
117598
|
+
} catch {
|
|
117599
|
+
}
|
|
117600
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
117601
|
+
res.end(
|
|
117602
|
+
JSON.stringify({
|
|
117603
|
+
loop: args.loop.slug,
|
|
117604
|
+
files: data,
|
|
117605
|
+
state,
|
|
117606
|
+
mode: "live",
|
|
117607
|
+
asOf: (/* @__PURE__ */ new Date()).toISOString()
|
|
117608
|
+
})
|
|
117609
|
+
);
|
|
117610
|
+
return;
|
|
117611
|
+
}
|
|
117612
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
117613
|
+
res.end("not found");
|
|
117614
|
+
} catch {
|
|
117615
|
+
try {
|
|
117616
|
+
res.writeHead(500);
|
|
117617
|
+
res.end();
|
|
117618
|
+
} catch {
|
|
117619
|
+
}
|
|
117589
117620
|
}
|
|
117590
|
-
|
|
117591
|
-
|
|
117592
|
-
|
|
117621
|
+
};
|
|
117622
|
+
const bind = (port) => new Promise((resolve) => {
|
|
117623
|
+
const server2 = make(handler);
|
|
117624
|
+
server2.once("error", (err) => {
|
|
117625
|
+
try {
|
|
117626
|
+
server2.close();
|
|
117627
|
+
} catch {
|
|
117628
|
+
}
|
|
117629
|
+
resolve({ ok: false, err });
|
|
117630
|
+
});
|
|
117631
|
+
server2.listen(port, "127.0.0.1", () => {
|
|
117632
|
+
server2.unref();
|
|
117633
|
+
resolve({
|
|
117634
|
+
ok: true,
|
|
117635
|
+
handle: {
|
|
117636
|
+
server: server2,
|
|
117637
|
+
port,
|
|
117638
|
+
close() {
|
|
117639
|
+
try {
|
|
117640
|
+
server2.close();
|
|
117641
|
+
server2.closeAllConnections?.();
|
|
117642
|
+
} catch {
|
|
117643
|
+
}
|
|
117644
|
+
}
|
|
117645
|
+
}
|
|
117646
|
+
});
|
|
117647
|
+
});
|
|
117648
|
+
});
|
|
117649
|
+
return (async () => {
|
|
117650
|
+
const lastPort = Math.min(basePort + MAX_PORT_RETRIES, 65535);
|
|
117651
|
+
for (let port = basePort; port <= lastPort; port++) {
|
|
117652
|
+
const attempt = await bind(port);
|
|
117653
|
+
if (attempt.ok) {
|
|
117654
|
+
log(` loop dashboard: http://localhost:${attempt.handle.port}`);
|
|
117655
|
+
return attempt.handle;
|
|
117656
|
+
}
|
|
117657
|
+
if (attempt.err.code !== "EADDRINUSE") {
|
|
117658
|
+
log(
|
|
117659
|
+
` (loop dashboard not started on :${port} \u2014 ${attempt.err.code ?? attempt.err.message}; run continues)`
|
|
117660
|
+
);
|
|
117661
|
+
return null;
|
|
117662
|
+
}
|
|
117593
117663
|
}
|
|
117594
|
-
|
|
117595
|
-
|
|
117596
|
-
|
|
117664
|
+
log(` (loop dashboard not started \u2014 :${basePort}-${lastPort} all in use; run continues)`);
|
|
117665
|
+
return null;
|
|
117666
|
+
})();
|
|
117597
117667
|
}
|
|
117598
|
-
|
|
117599
|
-
|
|
117600
|
-
|
|
117601
|
-
|
|
117602
|
-
|
|
117603
|
-
|
|
117604
|
-
|
|
117605
|
-
|
|
117606
|
-
|
|
117607
|
-
|
|
117608
|
-
|
|
117609
|
-
if (!isRecordKindToken(r?.kind) || !scope) continue;
|
|
117610
|
-
records.push({ kind: r.kind, scope });
|
|
117611
|
-
}
|
|
117612
|
-
return { docs, records };
|
|
117668
|
+
|
|
117669
|
+
// src/mcp-server/task-run-mode.ts
|
|
117670
|
+
init_esm_shims();
|
|
117671
|
+
var ACCEPTED_RUN_MODES = ["in-chat", "headless"];
|
|
117672
|
+
var FRONTMATTER_SCAN_LIMIT = 8192;
|
|
117673
|
+
function normalizeRunMode(raw) {
|
|
117674
|
+
if (typeof raw !== "string") return void 0;
|
|
117675
|
+
const v = raw.trim().toLowerCase();
|
|
117676
|
+
if (v === "headless") return "headless";
|
|
117677
|
+
if (v === "in-chat" || v === "inchat" || v === "in_chat") return "in-chat";
|
|
117678
|
+
return void 0;
|
|
117613
117679
|
}
|
|
117614
|
-
|
|
117680
|
+
function describeProvided(raw) {
|
|
117681
|
+
if (typeof raw === "string") return raw.trim();
|
|
117615
117682
|
try {
|
|
117616
|
-
|
|
117617
|
-
if (got?.status !== "ok") return void 0;
|
|
117618
|
-
return normalizeMemoryManifest(got.task?.stateDocs ?? null);
|
|
117683
|
+
return JSON.stringify(raw) ?? String(raw);
|
|
117619
117684
|
} catch {
|
|
117620
|
-
return
|
|
117685
|
+
return String(raw);
|
|
117621
117686
|
}
|
|
117622
117687
|
}
|
|
117623
|
-
function
|
|
117624
|
-
|
|
117688
|
+
function classifyRunModeArgument(raw) {
|
|
117689
|
+
if (raw === void 0 || raw === null) return { kind: "omitted" };
|
|
117690
|
+
if (typeof raw === "string" && raw.trim() === "") return { kind: "omitted" };
|
|
117691
|
+
const mode2 = normalizeRunMode(raw);
|
|
117692
|
+
if (mode2) return { kind: "valid", mode: mode2 };
|
|
117693
|
+
return { kind: "invalid", provided: describeProvided(raw) };
|
|
117625
117694
|
}
|
|
117626
|
-
function
|
|
117627
|
-
|
|
117695
|
+
function unquoteScalar(raw) {
|
|
117696
|
+
let v = raw.trim();
|
|
117697
|
+
const comment = v.match(/(?:^|\s)#.*$/);
|
|
117698
|
+
if (comment) v = v.slice(0, comment.index === 0 ? 0 : comment.index).trim();
|
|
117699
|
+
if (v.length >= 2 && (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'"))) {
|
|
117700
|
+
v = v.slice(1, -1).trim();
|
|
117701
|
+
}
|
|
117702
|
+
return v;
|
|
117628
117703
|
}
|
|
117629
|
-
function
|
|
117630
|
-
if (
|
|
117631
|
-
|
|
117704
|
+
function parseDefaultRunMode(body) {
|
|
117705
|
+
if (typeof body !== "string" || !body) return void 0;
|
|
117706
|
+
const head = body.replace(/^\uFEFF/, "").slice(0, FRONTMATTER_SCAN_LIMIT);
|
|
117707
|
+
const opener = head.match(/^---[ \t]*\r?\n/);
|
|
117708
|
+
if (!opener) return void 0;
|
|
117709
|
+
const rest = head.slice(opener[0].length);
|
|
117710
|
+
const closer = rest.search(/^(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/m);
|
|
117711
|
+
if (closer < 0) return void 0;
|
|
117712
|
+
const block = rest.slice(0, closer);
|
|
117713
|
+
const hit = block.match(/^defaultRunMode[ \t]*:[ \t]*(.*)$/m);
|
|
117714
|
+
if (!hit) return void 0;
|
|
117715
|
+
return normalizeRunMode(unquoteScalar(hit[1] ?? ""));
|
|
117632
117716
|
}
|
|
117633
|
-
function
|
|
117634
|
-
const
|
|
117635
|
-
if (
|
|
117636
|
-
|
|
117637
|
-
|
|
117638
|
-
|
|
117639
|
-
|
|
117640
|
-
|
|
117717
|
+
function resolveRunMode(explicit, body) {
|
|
117718
|
+
const arg = classifyRunModeArgument(explicit);
|
|
117719
|
+
if (arg.kind === "invalid") {
|
|
117720
|
+
return { ok: false, error: "invalid_mode", provided: arg.provided, accepted: ACCEPTED_RUN_MODES };
|
|
117721
|
+
}
|
|
117722
|
+
if (arg.kind === "valid") return { ok: true, mode: arg.mode, source: "explicit" };
|
|
117723
|
+
const fromBody = parseDefaultRunMode(body);
|
|
117724
|
+
if (fromBody) return { ok: true, mode: fromBody, source: "frontmatter" };
|
|
117725
|
+
return { ok: true, mode: "in-chat", source: "default" };
|
|
117726
|
+
}
|
|
117727
|
+
|
|
117728
|
+
// src/loops/estimate.ts
|
|
117729
|
+
init_esm_shims();
|
|
117730
|
+
function estimateBlastRadius(loop2) {
|
|
117731
|
+
const g = loop2.graphJson ?? {};
|
|
117732
|
+
const nodes = Array.isArray(g.nodes) ? g.nodes : [];
|
|
117733
|
+
const steps = nodes.length;
|
|
117734
|
+
const paidSteps = nodes.filter(
|
|
117735
|
+
(n) => n?.type === "spend" || n?.data?.paid === true || n?.paid === true
|
|
117736
|
+
).length;
|
|
117737
|
+
const costHints = nodes.map((n) => Number(n?.data?.estCostEur ?? n?.estCostEur)).filter((x) => !Number.isNaN(x));
|
|
117738
|
+
const estCostEur = costHints.length ? costHints.reduce((a, b) => a + b, 0) : null;
|
|
117739
|
+
return { steps, paidSteps, estCostEur };
|
|
117641
117740
|
}
|
|
117642
117741
|
|
|
117643
|
-
// src/loops/
|
|
117644
|
-
|
|
117645
|
-
|
|
117742
|
+
// src/loops/dashboard-template.ts
|
|
117743
|
+
init_esm_shims();
|
|
117744
|
+
var DEFAULT_DASHBOARD_FILES = [
|
|
117646
117745
|
"VISION.md",
|
|
117647
117746
|
"CONSTRAINTS.md",
|
|
117648
|
-
"
|
|
117747
|
+
"QUEUE.md",
|
|
117649
117748
|
"STATUS.md",
|
|
117650
|
-
"
|
|
117651
|
-
"
|
|
117749
|
+
"README.md",
|
|
117750
|
+
"rounds.jsonl"
|
|
117652
117751
|
];
|
|
117653
|
-
|
|
117654
|
-
|
|
117655
|
-
|
|
117656
|
-
|
|
117657
|
-
|
|
117658
|
-
|
|
117659
|
-
|
|
117660
|
-
if (filename.includes("/") || filename.includes("\\")) return false;
|
|
117661
|
-
if (filename === ".." || filename.includes("\0")) return false;
|
|
117662
|
-
return true;
|
|
117663
|
-
}
|
|
117664
|
-
function isReservedName(filename) {
|
|
117665
|
-
return RESERVED_FIRE_FILENAMES.some((r) => r.toLowerCase() === filename.toLowerCase());
|
|
117666
|
-
}
|
|
117667
|
-
var DOC_LIST_LIMIT = 500;
|
|
117668
|
-
async function discoverMemoryDocs(sdk, apiKey, slug) {
|
|
117669
|
-
const out = { own: [], shared: [], failed: [], truncated: [] };
|
|
117670
|
-
for (const scope of ["member", "shared"]) {
|
|
117671
|
-
const res = await sdk.loops.memory.listDocs(apiKey, slug, { scope, limit: DOC_LIST_LIMIT });
|
|
117672
|
-
if (res.status !== "ok") {
|
|
117673
|
-
if (scope === "member" && isNotFound(res)) continue;
|
|
117674
|
-
out.failed.push(scope);
|
|
117675
|
-
continue;
|
|
117676
|
-
}
|
|
117677
|
-
if (res.items.length >= DOC_LIST_LIMIT) out.truncated.push(scope);
|
|
117678
|
-
if (scope === "member") out.own = res.items;
|
|
117679
|
-
else out.shared = res.items;
|
|
117680
|
-
}
|
|
117681
|
-
return out;
|
|
117682
|
-
}
|
|
117683
|
-
function planStateDocs(stateDocs, discovered) {
|
|
117684
|
-
const manifest = normalizeMemoryManifest(stateDocs);
|
|
117685
|
-
const candidates = [];
|
|
117686
|
-
const claimed = /* @__PURE__ */ new Set();
|
|
117687
|
-
for (const d of manifest?.docs ?? []) {
|
|
117688
|
-
claimed.add(d.key);
|
|
117689
|
-
candidates.push({ key: d.key, scope: d.scope, declared: true });
|
|
117690
|
-
}
|
|
117691
|
-
for (const [rows, scope] of [
|
|
117692
|
-
[discovered.own, "member"],
|
|
117693
|
-
[discovered.shared, "shared"]
|
|
117694
|
-
]) {
|
|
117695
|
-
for (const m of rows) {
|
|
117696
|
-
if (claimed.has(m.key)) continue;
|
|
117697
|
-
claimed.add(m.key);
|
|
117698
|
-
candidates.push({ key: m.key, scope, declared: false });
|
|
117699
|
-
}
|
|
117752
|
+
function parseProcessSpec(manifest) {
|
|
117753
|
+
if (typeof manifest !== "string" || !manifest.trim()) return null;
|
|
117754
|
+
let parsed;
|
|
117755
|
+
try {
|
|
117756
|
+
parsed = JSON.parse(manifest);
|
|
117757
|
+
} catch {
|
|
117758
|
+
return null;
|
|
117700
117759
|
}
|
|
117701
|
-
const
|
|
117702
|
-
|
|
117703
|
-
const
|
|
117704
|
-
|
|
117705
|
-
|
|
117706
|
-
if (!
|
|
117707
|
-
|
|
117708
|
-
|
|
117709
|
-
|
|
117710
|
-
|
|
117711
|
-
|
|
117712
|
-
|
|
117713
|
-
|
|
117714
|
-
}
|
|
117715
|
-
continue;
|
|
117716
|
-
}
|
|
117717
|
-
const owner = taken.get(filename.toLowerCase());
|
|
117718
|
-
if (owner !== void 0) {
|
|
117719
|
-
skipped.push({ key: c.key, reason: `'${filename}' is already taken by key '${owner}'` });
|
|
117720
|
-
continue;
|
|
117760
|
+
const raw = parsed?.processSpec;
|
|
117761
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
117762
|
+
const spec = raw;
|
|
117763
|
+
const stages = (Array.isArray(spec.stages) ? spec.stages : []).map((s) => {
|
|
117764
|
+
if (typeof s === "string" && s.trim()) return { label: s.trim() };
|
|
117765
|
+
if (s && typeof s === "object" && !Array.isArray(s)) {
|
|
117766
|
+
const o = s;
|
|
117767
|
+
if (typeof o.label === "string" && o.label.trim()) {
|
|
117768
|
+
return {
|
|
117769
|
+
label: o.label.trim(),
|
|
117770
|
+
...typeof o.id === "string" && o.id.trim() ? { id: o.id.trim() } : {},
|
|
117771
|
+
...typeof o.detail === "string" && o.detail.trim() ? { detail: o.detail.trim() } : {}
|
|
117772
|
+
};
|
|
117773
|
+
}
|
|
117721
117774
|
}
|
|
117722
|
-
|
|
117723
|
-
|
|
117724
|
-
|
|
117725
|
-
|
|
117775
|
+
return null;
|
|
117776
|
+
}).filter((s) => s !== null);
|
|
117777
|
+
const strings = (v) => (Array.isArray(v) ? v : []).filter((x) => typeof x === "string" && x.trim() !== "");
|
|
117778
|
+
const out = {
|
|
117779
|
+
stages,
|
|
117780
|
+
inputs: strings(spec.inputs),
|
|
117781
|
+
outputs: strings(spec.outputs),
|
|
117782
|
+
...typeof spec.title === "string" && spec.title.trim() ? { title: spec.title.trim() } : {}
|
|
117783
|
+
};
|
|
117784
|
+
return stages.length || out.inputs.length || out.outputs.length ? out : null;
|
|
117726
117785
|
}
|
|
117727
|
-
function
|
|
117728
|
-
|
|
117729
|
-
const notes = [];
|
|
117730
|
-
if (doc.created) notes.push(doc.seeded ? "created, seeded from shared" : "created");
|
|
117731
|
-
if (!doc.declared) notes.push("not in manifest");
|
|
117732
|
-
return `${doc.filename} \u2190 ${from14}${notes.length ? ` (${notes.join(", ")})` : ""}`;
|
|
117786
|
+
function defaultDashboardManifest() {
|
|
117787
|
+
return JSON.stringify({ files: [...DEFAULT_DASHBOARD_FILES] });
|
|
117733
117788
|
}
|
|
117734
|
-
|
|
117735
|
-
|
|
117736
|
-
if (res.status === "ok") return { status: "ok", body: res.doc.content ?? "" };
|
|
117737
|
-
if (isNotFound(res)) return { status: "absent" };
|
|
117738
|
-
return { status: "failed" };
|
|
117789
|
+
function embedJson(value2) {
|
|
117790
|
+
return JSON.stringify(value2).replace(/</g, "\\u003c");
|
|
117739
117791
|
}
|
|
117740
|
-
|
|
117741
|
-
|
|
117742
|
-
|
|
117743
|
-
|
|
117744
|
-
|
|
117745
|
-
|
|
117746
|
-
|
|
117747
|
-
|
|
117748
|
-
|
|
117749
|
-
|
|
117750
|
-
|
|
117751
|
-
|
|
117752
|
-
|
|
117753
|
-
|
|
117754
|
-
|
|
117755
|
-
|
|
117756
|
-
|
|
117757
|
-
|
|
117758
|
-
|
|
117759
|
-
|
|
117760
|
-
|
|
117761
|
-
|
|
117762
|
-
|
|
117763
|
-
|
|
117764
|
-
|
|
117765
|
-
|
|
117766
|
-
|
|
117767
|
-
|
|
117768
|
-
|
|
117769
|
-
|
|
117770
|
-
|
|
117771
|
-
|
|
117772
|
-
|
|
117773
|
-
|
|
117774
|
-
|
|
117775
|
-
|
|
117792
|
+
function escapeHtml(s) {
|
|
117793
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
117794
|
+
}
|
|
117795
|
+
function renderLoopDashboardTemplate(args) {
|
|
117796
|
+
const title = escapeHtml(args.processSpec?.title ?? args.slug);
|
|
117797
|
+
const seed = embedJson({
|
|
117798
|
+
slug: args.slug,
|
|
117799
|
+
descriptionShort: args.descriptionShort ?? "",
|
|
117800
|
+
processSpec: args.processSpec ?? null
|
|
117801
|
+
});
|
|
117802
|
+
return `<!doctype html>
|
|
117803
|
+
<html lang="en">
|
|
117804
|
+
<head>
|
|
117805
|
+
<meta charset="utf-8">
|
|
117806
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
117807
|
+
<title>${title} \u2014 loop dashboard</title>
|
|
117808
|
+
<style>
|
|
117809
|
+
:root { --bg:#f7f7fb; --panel:#fff; --border:#e3e3ee; --muted:#6b6b80; --accent:#5b5bd6; --ok:#1a7f37; --bad:#b42318; }
|
|
117810
|
+
* { box-sizing: border-box; }
|
|
117811
|
+
body { margin:0; font:14px/1.5 -apple-system, "Segoe UI", Roboto, sans-serif; background:var(--bg); color:#1d1d2b; padding:24px; }
|
|
117812
|
+
h1 { font-size:20px; margin:0 0 4px; }
|
|
117813
|
+
h2 { font-size:14px; margin:24px 0 8px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
|
|
117814
|
+
.sub { color:var(--muted); margin:0 0 16px; }
|
|
117815
|
+
.panel { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:16px; }
|
|
117816
|
+
.map { display:flex; flex-wrap:wrap; align-items:stretch; gap:8px; }
|
|
117817
|
+
.stage { background:var(--panel); border:1px solid var(--border); border-left:3px solid var(--accent); border-radius:10px; padding:10px 14px; min-width:140px; flex:1; }
|
|
117818
|
+
.stage .label { font-weight:600; }
|
|
117819
|
+
.stage .detail { color:var(--muted); font-size:12px; margin-top:2px; }
|
|
117820
|
+
.arrow { align-self:center; color:var(--muted); }
|
|
117821
|
+
.io { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
|
117822
|
+
ul { margin:6px 0 0; padding-left:18px; }
|
|
117823
|
+
table { width:100%; border-collapse:collapse; background:var(--panel); border:1px solid var(--border); border-radius:12px; overflow:hidden; }
|
|
117824
|
+
th, td { text-align:left; padding:8px 12px; border-top:1px solid var(--border); font-size:13px; vertical-align:top; }
|
|
117825
|
+
thead th { border-top:none; background:var(--bg); color:var(--muted); font-weight:600; }
|
|
117826
|
+
.ok { color:var(--ok); } .bad { color:var(--bad); }
|
|
117827
|
+
details { background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:10px 14px; margin-bottom:8px; }
|
|
117828
|
+
summary { cursor:pointer; font-weight:600; }
|
|
117829
|
+
pre { overflow-x:auto; font-size:12px; background:var(--bg); border-radius:8px; padding:10px; }
|
|
117830
|
+
.empty { color:var(--muted); font-style:italic; }
|
|
117831
|
+
#live { font-size:12px; color:var(--muted); float:right; }
|
|
117832
|
+
</style>
|
|
117833
|
+
</head>
|
|
117834
|
+
<body>
|
|
117835
|
+
<span id="live">loading\u2026</span>
|
|
117836
|
+
<h1 id="title"></h1>
|
|
117837
|
+
<p class="sub" id="desc"></p>
|
|
117838
|
+
|
|
117839
|
+
<h2>Process</h2>
|
|
117840
|
+
<div id="map" class="map panel"></div>
|
|
117841
|
+
|
|
117842
|
+
<h2>Inputs & outputs</h2>
|
|
117843
|
+
<div class="io">
|
|
117844
|
+
<div class="panel"><strong>Inputs</strong><ul id="inputs"></ul></div>
|
|
117845
|
+
<div class="panel"><strong>Outputs</strong><ul id="outputs"></ul></div>
|
|
117846
|
+
</div>
|
|
117847
|
+
|
|
117848
|
+
<h2>Rounds</h2>
|
|
117849
|
+
<div id="rounds"></div>
|
|
117850
|
+
|
|
117851
|
+
<h2>Files</h2>
|
|
117852
|
+
<div id="files"></div>
|
|
117853
|
+
|
|
117854
|
+
<script type="application/json" id="loop-seed">${seed}</script>
|
|
117855
|
+
<script>
|
|
117856
|
+
(function () {
|
|
117857
|
+
"use strict";
|
|
117858
|
+
var seed = JSON.parse(document.getElementById("loop-seed").textContent);
|
|
117859
|
+
document.getElementById("title").textContent = (seed.processSpec && seed.processSpec.title) || seed.slug;
|
|
117860
|
+
document.getElementById("desc").textContent = seed.descriptionShort || "";
|
|
117861
|
+
document.title = seed.slug + " \u2014 loop dashboard";
|
|
117862
|
+
|
|
117863
|
+
function el(tag, cls, text) {
|
|
117864
|
+
var e = document.createElement(tag);
|
|
117865
|
+
if (cls) e.className = cls;
|
|
117866
|
+
if (text !== undefined) e.textContent = text;
|
|
117867
|
+
return e;
|
|
117868
|
+
}
|
|
117869
|
+
|
|
117870
|
+
// \u2500\u2500 Process map + IO from the embedded spec (static \u2014 rendered once). \u2500\u2500
|
|
117871
|
+
var map = document.getElementById("map");
|
|
117872
|
+
var spec = seed.processSpec;
|
|
117873
|
+
if (spec && spec.stages && spec.stages.length) {
|
|
117874
|
+
spec.stages.forEach(function (s, i) {
|
|
117875
|
+
if (i > 0) map.appendChild(el("div", "arrow", "\\u2192"));
|
|
117876
|
+
var box = el("div", "stage");
|
|
117877
|
+
box.appendChild(el("div", "label", s.label));
|
|
117878
|
+
if (s.detail) box.appendChild(el("div", "detail", s.detail));
|
|
117879
|
+
map.appendChild(box);
|
|
117880
|
+
});
|
|
117881
|
+
} else {
|
|
117882
|
+
map.appendChild(el("span", "empty", "No process spec on this loop \\u2014 live files and rounds below."));
|
|
117776
117883
|
}
|
|
117777
|
-
|
|
117778
|
-
|
|
117779
|
-
|
|
117780
|
-
|
|
117781
|
-
|
|
117782
|
-
}
|
|
117783
|
-
function docMergeRefusal(boot, fresh, next, cleanAppend) {
|
|
117784
|
-
if (next.trim() === "" && fresh.trim() !== "") {
|
|
117785
|
-
return "would TRUNCATE the stored document to empty while the record still holds content \u2014 an emptied file is not an instruction to erase the record";
|
|
117884
|
+
function fillList(id, items) {
|
|
117885
|
+
var ul = document.getElementById(id);
|
|
117886
|
+
ul.textContent = "";
|
|
117887
|
+
if (!items || !items.length) { ul.appendChild(el("li", "empty", "\\u2014")); return; }
|
|
117888
|
+
items.forEach(function (x) { ul.appendChild(el("li", null, x)); });
|
|
117786
117889
|
}
|
|
117787
|
-
|
|
117788
|
-
|
|
117890
|
+
fillList("inputs", spec && spec.inputs);
|
|
117891
|
+
fillList("outputs", spec && spec.outputs);
|
|
117892
|
+
|
|
117893
|
+
// \u2500\u2500 Live data: rounds + files, refreshed from GET /data. \u2500\u2500
|
|
117894
|
+
function parseJsonl(text) {
|
|
117895
|
+
var rows = [];
|
|
117896
|
+
(text || "").split("\\n").forEach(function (line) {
|
|
117897
|
+
line = line.trim();
|
|
117898
|
+
if (!line) return;
|
|
117899
|
+
try { rows.push(JSON.parse(line)); } catch (e) { /* skip malformed line */ }
|
|
117900
|
+
});
|
|
117901
|
+
return rows;
|
|
117789
117902
|
}
|
|
117790
|
-
|
|
117791
|
-
|
|
117792
|
-
|
|
117793
|
-
|
|
117794
|
-
|
|
117795
|
-
|
|
117796
|
-
|
|
117797
|
-
|
|
117798
|
-
continue;
|
|
117799
|
-
}
|
|
117800
|
-
const materialized = readFileSync8(path2, "utf-8");
|
|
117801
|
-
const reread = await readScoped(sdk, apiKey, slug, doc.key, doc.scope);
|
|
117802
|
-
const fresh = reread.status === "ok" ? reread.body : reread.status === "absent" && doc.body === "" ? "" : void 0;
|
|
117803
|
-
if (fresh === void 0) {
|
|
117804
|
-
outcomes.push({
|
|
117805
|
-
key: doc.key,
|
|
117806
|
-
outcome: "failed",
|
|
117807
|
-
detail: `could not re-read the ${doc.scope} row; ${path2} kept for recovery`
|
|
117808
|
-
});
|
|
117809
|
-
continue;
|
|
117810
|
-
}
|
|
117811
|
-
const merged = mergeConstraints(doc.body, materialized, fresh);
|
|
117812
|
-
if (merged.refusal) {
|
|
117813
|
-
outcomes.push({ key: doc.key, outcome: "refused", detail: merged.refusal });
|
|
117814
|
-
continue;
|
|
117815
|
-
}
|
|
117816
|
-
if (merged.next === void 0) {
|
|
117817
|
-
outcomes.push({ key: doc.key, outcome: "unchanged" });
|
|
117818
|
-
continue;
|
|
117819
|
-
}
|
|
117820
|
-
const docRefusal = docMergeRefusal(doc.body, fresh, merged.next, merged.cleanAppend);
|
|
117821
|
-
if (docRefusal) {
|
|
117822
|
-
outcomes.push({
|
|
117823
|
-
key: doc.key,
|
|
117824
|
-
outcome: "refused",
|
|
117825
|
-
detail: `${docRefusal}; ${path2} kept for recovery`
|
|
117826
|
-
});
|
|
117827
|
-
continue;
|
|
117903
|
+
|
|
117904
|
+
function renderRounds(fires, rounds) {
|
|
117905
|
+
var host = document.getElementById("rounds");
|
|
117906
|
+
host.textContent = "";
|
|
117907
|
+
if (!fires.length && !rounds.length) {
|
|
117908
|
+
var p = el("div", "panel"); p.appendChild(el("span", "empty", "No rounds yet \\u2014 this fills in as the loop runs."));
|
|
117909
|
+
host.appendChild(p);
|
|
117910
|
+
return;
|
|
117828
117911
|
}
|
|
117829
|
-
|
|
117830
|
-
|
|
117831
|
-
|
|
117912
|
+
var table = document.createElement("table");
|
|
117913
|
+
var thead = document.createElement("thead");
|
|
117914
|
+
var hr = document.createElement("tr");
|
|
117915
|
+
["when", "duration", "turns", "tokens", "exit", "round detail (rounds.jsonl)"].forEach(function (h) {
|
|
117916
|
+
hr.appendChild(el("th", null, h));
|
|
117832
117917
|
});
|
|
117833
|
-
|
|
117834
|
-
|
|
117835
|
-
|
|
117836
|
-
|
|
117837
|
-
|
|
117838
|
-
|
|
117839
|
-
|
|
117918
|
+
thead.appendChild(hr); table.appendChild(thead);
|
|
117919
|
+
var tbody = document.createElement("tbody");
|
|
117920
|
+
var n = Math.max(fires.length, rounds.length);
|
|
117921
|
+
for (var i = n - 1; i >= 0; i--) { // newest first
|
|
117922
|
+
var f = fires[i] || {};
|
|
117923
|
+
var r = rounds[i];
|
|
117924
|
+
var tr = document.createElement("tr");
|
|
117925
|
+
tr.appendChild(el("td", null, f.ts || (r && r.ts) || "\\u2014"));
|
|
117926
|
+
tr.appendChild(el("td", null, f.duration_s != null ? f.duration_s + "s" : "\\u2014"));
|
|
117927
|
+
tr.appendChild(el("td", null, f.turns != null ? String(f.turns) : "\\u2014"));
|
|
117928
|
+
tr.appendChild(el("td", null, f.tokens_total != null ? Number(f.tokens_total).toLocaleString() : "\\u2014"));
|
|
117929
|
+
tr.appendChild(el("td", f.exit === 0 ? "ok" : f.exit != null ? "bad" : null, f.exit != null ? String(f.exit) : "\\u2014"));
|
|
117930
|
+
var detail = el("td");
|
|
117931
|
+
if (f.launch_failed) { detail.textContent = "launch failed \\u2014 " + (f.reason || "unknown reason"); }
|
|
117932
|
+
else if (r) { var pre = document.createElement("pre"); pre.textContent = JSON.stringify(r, null, 1); detail.appendChild(pre); }
|
|
117933
|
+
else detail.textContent = "\\u2014";
|
|
117934
|
+
tr.appendChild(detail);
|
|
117935
|
+
tbody.appendChild(tr);
|
|
117840
117936
|
}
|
|
117841
|
-
|
|
117842
|
-
|
|
117843
|
-
|
|
117844
|
-
|
|
117937
|
+
table.appendChild(tbody);
|
|
117938
|
+
host.appendChild(table);
|
|
117939
|
+
}
|
|
117940
|
+
|
|
117941
|
+
function renderFiles(files) {
|
|
117942
|
+
var host = document.getElementById("files");
|
|
117943
|
+
host.textContent = "";
|
|
117944
|
+
var names = Object.keys(files || {}).filter(function (n) { return n !== "rounds.jsonl"; });
|
|
117945
|
+
if (!names.length) { var p = el("div", "panel"); p.appendChild(el("span", "empty", "No files surfaced by the manifest.")); host.appendChild(p); return; }
|
|
117946
|
+
names.forEach(function (name) {
|
|
117947
|
+
var d = document.createElement("details");
|
|
117948
|
+
d.appendChild(el("summary", null, name));
|
|
117949
|
+
var pre = document.createElement("pre");
|
|
117950
|
+
pre.textContent = files[name];
|
|
117951
|
+
d.appendChild(pre);
|
|
117952
|
+
host.appendChild(d);
|
|
117953
|
+
});
|
|
117954
|
+
}
|
|
117955
|
+
|
|
117956
|
+
function refresh() {
|
|
117957
|
+
fetch("/data").then(function (res) { return res.json(); }).then(function (data) {
|
|
117958
|
+
var fires = parseJsonl(data.state && data.state["fires.jsonl"]);
|
|
117959
|
+
var rounds = parseJsonl(data.files && data.files["rounds.jsonl"]);
|
|
117960
|
+
renderRounds(fires, rounds);
|
|
117961
|
+
renderFiles(data.files);
|
|
117962
|
+
document.getElementById("live").textContent = "updated " + new Date().toLocaleTimeString();
|
|
117963
|
+
}).catch(function () {
|
|
117964
|
+
document.getElementById("live").textContent = "server stopped";
|
|
117965
|
+
});
|
|
117966
|
+
}
|
|
117967
|
+
refresh();
|
|
117968
|
+
setInterval(refresh, 5000);
|
|
117969
|
+
})();
|
|
117970
|
+
</script>
|
|
117971
|
+
</body>
|
|
117972
|
+
</html>
|
|
117973
|
+
`;
|
|
117974
|
+
}
|
|
117975
|
+
function injectDefaultDashboard(loop2) {
|
|
117976
|
+
const hasHtml = typeof loop2.dashboardHtml === "string" && loop2.dashboardHtml.trim() !== "";
|
|
117977
|
+
if (hasHtml) return;
|
|
117978
|
+
if (loop2.dashboardManifest && typeof loop2.dashboardManifest === "object") {
|
|
117979
|
+
try {
|
|
117980
|
+
loop2.dashboardManifest = JSON.stringify(loop2.dashboardManifest);
|
|
117981
|
+
} catch {
|
|
117845
117982
|
}
|
|
117846
|
-
const afterText = after.doc.content ?? "";
|
|
117847
|
-
const missing = merged.addedBlocks.length > 0 ? verifyLanded(afterText, merged.addedBlocks) : afterText === merged.next ? [] : ["<rewrite did not survive>"];
|
|
117848
|
-
outcomes.push(
|
|
117849
|
-
missing.length === 0 ? { key: doc.key, outcome: "shipped" } : {
|
|
117850
|
-
key: doc.key,
|
|
117851
|
-
outcome: "failed",
|
|
117852
|
-
detail: `${missing.length} section(s) did not survive a concurrent write; ${path2} kept`
|
|
117853
|
-
}
|
|
117854
|
-
);
|
|
117855
117983
|
}
|
|
117856
|
-
|
|
117984
|
+
const manifest = typeof loop2.dashboardManifest === "string" ? loop2.dashboardManifest : void 0;
|
|
117985
|
+
loop2.dashboardHtml = renderLoopDashboardTemplate({
|
|
117986
|
+
slug: typeof loop2.slug === "string" ? loop2.slug : "loop",
|
|
117987
|
+
descriptionShort: typeof loop2.descriptionShort === "string" ? loop2.descriptionShort : void 0,
|
|
117988
|
+
processSpec: parseProcessSpec(manifest)
|
|
117989
|
+
});
|
|
117990
|
+
if (!manifest || !manifest.trim()) loop2.dashboardManifest = defaultDashboardManifest();
|
|
117857
117991
|
}
|
|
117858
117992
|
|
|
117859
117993
|
// src/loops/memory-verbs.ts
|
|
@@ -118178,6 +118312,27 @@ async function taskMemoryArchiveVerb(rawSlug, opts) {
|
|
|
118178
118312
|
);
|
|
118179
118313
|
}
|
|
118180
118314
|
|
|
118315
|
+
// src/mcp-server/memory-scope.ts
|
|
118316
|
+
init_esm_shims();
|
|
118317
|
+
var MEMORY_SCOPE_EXPLANATION = "Global memory: everyone who uses this task (when it is shared) reads and writes the same copy \u2014 edits can conflict and build up over time. Per-member memory: each person who uses it gets their own copy. For a private task the difference doesn't matter. If unsure, choose per-member to avoid conflicts. Examples: a product PBI someone picks up and leaves half-done \u2192 per member; competitor signals the whole workspace should see \u2192 global.";
|
|
118318
|
+
function memorySummaryLine(manifest) {
|
|
118319
|
+
const normalized = normalizeMemoryManifest(manifest);
|
|
118320
|
+
const entries = normalized ? [...normalized.docs, ...normalized.records] : [];
|
|
118321
|
+
const global2 = entries.filter((e) => e.scope === "shared").length;
|
|
118322
|
+
const perMember = entries.filter((e) => e.scope === "member").length;
|
|
118323
|
+
if (global2 > 0 && perMember > 0) return `Memory: mixed \u2014 ${global2} global \xB7 ${perMember} per member`;
|
|
118324
|
+
if (global2 > 0) return "Memory: global";
|
|
118325
|
+
if (perMember > 0) return "Memory: per member";
|
|
118326
|
+
return "Memory: none";
|
|
118327
|
+
}
|
|
118328
|
+
function defaultMemoryManifest() {
|
|
118329
|
+
return { docs: [], records: [{ kind: "run", scope: "member" }] };
|
|
118330
|
+
}
|
|
118331
|
+
var MEMORY_DEFAULTED_SUFFIX = " (defaulted \u2014 no manifest declared)";
|
|
118332
|
+
function declaresNoMemory(manifest) {
|
|
118333
|
+
return memorySummaryLine(manifest ?? null) === "Memory: none";
|
|
118334
|
+
}
|
|
118335
|
+
|
|
118181
118336
|
// src/mcp-server/prompt-descriptors.ts
|
|
118182
118337
|
init_esm_shims();
|
|
118183
118338
|
|
|
@@ -118456,6 +118611,15 @@ function buildReceipt(input) {
|
|
|
118456
118611
|
if (typeof sent.draft === "boolean") {
|
|
118457
118612
|
out.push({ field: "draft", action: classify(sent.draft, prev ? prev.draft : void 0) });
|
|
118458
118613
|
}
|
|
118614
|
+
if (sent.stateDocs !== void 0) {
|
|
118615
|
+
const sentJson = JSON.stringify(sent.stateDocs ?? null);
|
|
118616
|
+
const prevHas = prev !== void 0 && prev.stateDocs !== void 0;
|
|
118617
|
+
const prevJson = prevHas ? JSON.stringify(prev.stateDocs ?? null) : void 0;
|
|
118618
|
+
const action = mode2 === "created" ? "set" : !previous || !previous.available ? "sent" : prevJson === void 0 ? "changed" : sentJson === prevJson ? "unchanged" : "changed";
|
|
118619
|
+
const rec = { field: "stateDocs", action, bytes: Buffer.byteLength(sentJson, "utf8") };
|
|
118620
|
+
if (prevJson !== void 0) rec.previousBytes = Buffer.byteLength(prevJson, "utf8");
|
|
118621
|
+
out.push(rec);
|
|
118622
|
+
}
|
|
118459
118623
|
return out;
|
|
118460
118624
|
}
|
|
118461
118625
|
function deriveChanges(input, receipt) {
|
|
@@ -118542,7 +118706,15 @@ function enforceResponseCap(payload) {
|
|
|
118542
118706
|
}
|
|
118543
118707
|
if (size6(out) > MAX_RESPONSE_CHARS && Array.isArray(out.receipt?.fields)) {
|
|
118544
118708
|
const r = out.receipt;
|
|
118545
|
-
out.receipt = {
|
|
118709
|
+
out.receipt = {
|
|
118710
|
+
fieldCount: r.fields.length,
|
|
118711
|
+
note: "receipt omitted \u2014 response cap",
|
|
118712
|
+
// WHERE IT LANDED SURVIVES THE CAP. It is two short strings, and it is the
|
|
118713
|
+
// one line on this receipt whose absence was an incident: shrinking the
|
|
118714
|
+
// response must not be how a caller stops being told which workspace it
|
|
118715
|
+
// just wrote to.
|
|
118716
|
+
...r.workspace !== void 0 ? { workspace: r.workspace } : {}
|
|
118717
|
+
};
|
|
118546
118718
|
}
|
|
118547
118719
|
if (size6(out) > MAX_RESPONSE_CHARS) {
|
|
118548
118720
|
out.summary = TRUNCATION_MARKER;
|
|
@@ -118553,7 +118725,7 @@ function enforceResponseCap(payload) {
|
|
|
118553
118725
|
|
|
118554
118726
|
// src/mcp-server/delegate-tools.ts
|
|
118555
118727
|
init_esm_shims();
|
|
118556
|
-
import { existsSync as
|
|
118728
|
+
import { existsSync as existsSync10, statSync as statSync2 } from "fs";
|
|
118557
118729
|
import { z as z3 } from "zod";
|
|
118558
118730
|
|
|
118559
118731
|
// src/delegate/jobs.ts
|
|
@@ -121047,7 +121219,7 @@ function registerDelegateTools(deps) {
|
|
|
121047
121219
|
const validated = validateStartInput(params, {
|
|
121048
121220
|
isDirectory: (path2) => {
|
|
121049
121221
|
try {
|
|
121050
|
-
return
|
|
121222
|
+
return existsSync10(path2) && statSync2(path2).isDirectory();
|
|
121051
121223
|
} catch {
|
|
121052
121224
|
return false;
|
|
121053
121225
|
}
|
|
@@ -121297,6 +121469,82 @@ function applyResolvedMethod(url2, method, inputs) {
|
|
|
121297
121469
|
|
|
121298
121470
|
// src/mcp-server/index.ts
|
|
121299
121471
|
init_plaintext_vault();
|
|
121472
|
+
init_paths();
|
|
121473
|
+
init_resolve();
|
|
121474
|
+
|
|
121475
|
+
// src/mcp-server/identity-reload.ts
|
|
121476
|
+
init_esm_shims();
|
|
121477
|
+
import { readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
121478
|
+
var NO_VAULT = "absent";
|
|
121479
|
+
var IDENTITY_CHECK_THROTTLE_MS = 1e3;
|
|
121480
|
+
function fingerprintVault(stat) {
|
|
121481
|
+
return stat ? `${stat.mtimeMs}:${stat.size}` : NO_VAULT;
|
|
121482
|
+
}
|
|
121483
|
+
function statVaultFile(path2) {
|
|
121484
|
+
try {
|
|
121485
|
+
const s = statSync3(path2);
|
|
121486
|
+
return { mtimeMs: s.mtimeMs, size: s.size };
|
|
121487
|
+
} catch {
|
|
121488
|
+
return null;
|
|
121489
|
+
}
|
|
121490
|
+
}
|
|
121491
|
+
var IdentityWatch = class {
|
|
121492
|
+
vaultPath;
|
|
121493
|
+
now;
|
|
121494
|
+
throttleMs;
|
|
121495
|
+
stat;
|
|
121496
|
+
/** False for an env-supplied key: `poll` is then a guaranteed no-op. */
|
|
121497
|
+
armed;
|
|
121498
|
+
fingerprint;
|
|
121499
|
+
lastCheckedAt = null;
|
|
121500
|
+
constructor(options) {
|
|
121501
|
+
this.vaultPath = options.vaultPath;
|
|
121502
|
+
this.now = options.now ?? Date.now;
|
|
121503
|
+
this.throttleMs = options.throttleMs ?? IDENTITY_CHECK_THROTTLE_MS;
|
|
121504
|
+
this.stat = options.stat ?? statVaultFile;
|
|
121505
|
+
this.armed = options.source !== "env";
|
|
121506
|
+
this.fingerprint = fingerprintVault(this.stat(this.vaultPath));
|
|
121507
|
+
}
|
|
121508
|
+
/** The fingerprint this watch currently considers current. */
|
|
121509
|
+
currentFingerprint() {
|
|
121510
|
+
return this.fingerprint;
|
|
121511
|
+
}
|
|
121512
|
+
/**
|
|
121513
|
+
* One throttled `stat`. Returns the change when the vault file differs from
|
|
121514
|
+
* the fingerprint on record — and ADOPTS it in the same step, so a reload the
|
|
121515
|
+
* caller then fails to apply is not retried on every subsequent tool call.
|
|
121516
|
+
* Returns `null` when nothing changed, when the throttle window is still open,
|
|
121517
|
+
* or when the watch is not armed.
|
|
121518
|
+
*/
|
|
121519
|
+
poll() {
|
|
121520
|
+
if (!this.armed) return null;
|
|
121521
|
+
const at = this.now();
|
|
121522
|
+
if (this.lastCheckedAt !== null && at - this.lastCheckedAt < this.throttleMs) return null;
|
|
121523
|
+
this.lastCheckedAt = at;
|
|
121524
|
+
const next = fingerprintVault(this.stat(this.vaultPath));
|
|
121525
|
+
if (next === this.fingerprint) return null;
|
|
121526
|
+
const from14 = this.fingerprint;
|
|
121527
|
+
this.fingerprint = next;
|
|
121528
|
+
return { from: from14, to: next };
|
|
121529
|
+
}
|
|
121530
|
+
/**
|
|
121531
|
+
* The vault bytes this watch is pointed at, or `null` when there is none.
|
|
121532
|
+
*
|
|
121533
|
+
* Reading THROUGH the watch rather than through `loadVault()` is deliberate:
|
|
121534
|
+
* the freshness check and the reload must address the same file, and having
|
|
121535
|
+
* one owner of the path is what makes that true by construction instead of by
|
|
121536
|
+
* two constants agreeing.
|
|
121537
|
+
*/
|
|
121538
|
+
readVault() {
|
|
121539
|
+
try {
|
|
121540
|
+
return readFileSync12(this.vaultPath, "utf-8");
|
|
121541
|
+
} catch {
|
|
121542
|
+
return null;
|
|
121543
|
+
}
|
|
121544
|
+
}
|
|
121545
|
+
};
|
|
121546
|
+
|
|
121547
|
+
// src/mcp-server/index.ts
|
|
121300
121548
|
function buildDeclaredConnectorsSentence(catalog, unreadable) {
|
|
121301
121549
|
const read = catalog === void 0 ? { connectors: cachedCatalogOrEmpty(), unreadable: mirrorGap() } : { connectors: catalog, unreadable: unreadable ?? [] };
|
|
121302
121550
|
const gap = describeCatalogGap(read.unreadable);
|
|
@@ -121428,6 +121676,8 @@ var loopIndexCache = null;
|
|
|
121428
121676
|
var taskIndexCache = null;
|
|
121429
121677
|
var cardCounts = null;
|
|
121430
121678
|
var lastNudgeAtCall = null;
|
|
121679
|
+
var WORKSPACE_IDENTITY_TTL_MS = 6e4;
|
|
121680
|
+
var workspaceIdentityCache = null;
|
|
121431
121681
|
var sessionId = randomUUID4();
|
|
121432
121682
|
var mcpEventLogger = null;
|
|
121433
121683
|
var PROCESSED_APPROVAL_IDS_CAP = 50;
|
|
@@ -121524,6 +121774,83 @@ function maybeAppendCardNudge(res, toolName, toolCallIndex) {
|
|
|
121524
121774
|
if (next !== res) lastNudgeAtCall = toolCallIndex;
|
|
121525
121775
|
return next;
|
|
121526
121776
|
}
|
|
121777
|
+
async function resolveWorkspaceIdentity() {
|
|
121778
|
+
const apiKey = currentCredentials.apiKey;
|
|
121779
|
+
if (!apiKey?.trim()) return { companyName: null, employeeName: null };
|
|
121780
|
+
const now = Date.now();
|
|
121781
|
+
if (workspaceIdentityCache && now - workspaceIdentityCache.at < WORKSPACE_IDENTITY_TTL_MS) {
|
|
121782
|
+
return workspaceIdentityCache.value;
|
|
121783
|
+
}
|
|
121784
|
+
let value2 = { companyName: null, employeeName: null };
|
|
121785
|
+
try {
|
|
121786
|
+
const sdk = await getSDK();
|
|
121787
|
+
const me = await sdk.virtualWalletsManagers.getManagerMe(apiKey);
|
|
121788
|
+
value2 = {
|
|
121789
|
+
companyName: typeof me?.companyName === "string" ? me.companyName : null,
|
|
121790
|
+
employeeName: typeof me?.employeeName === "string" ? me.employeeName : null
|
|
121791
|
+
};
|
|
121792
|
+
} catch (err) {
|
|
121793
|
+
console.error(
|
|
121794
|
+
`\u26A0\uFE0F workspace identity read failed \u2014 reporting nulls: ${err instanceof Error ? err.message : String(err)}`
|
|
121795
|
+
);
|
|
121796
|
+
}
|
|
121797
|
+
workspaceIdentityCache = { at: now, value: value2 };
|
|
121798
|
+
return value2;
|
|
121799
|
+
}
|
|
121800
|
+
var identityWatch = null;
|
|
121801
|
+
function dropIdentityScopedCaches() {
|
|
121802
|
+
walletsCache = null;
|
|
121803
|
+
policiesCache = null;
|
|
121804
|
+
transactionsCache = null;
|
|
121805
|
+
allowlistCache = null;
|
|
121806
|
+
capabilityIndexCache = null;
|
|
121807
|
+
compoundIndexCache = null;
|
|
121808
|
+
loopIndexCache = null;
|
|
121809
|
+
taskIndexCache = null;
|
|
121810
|
+
cardCounts = null;
|
|
121811
|
+
servicesDiscovered = null;
|
|
121812
|
+
workspaceIdentityCache = null;
|
|
121813
|
+
endpointSessionCache.clear();
|
|
121814
|
+
}
|
|
121815
|
+
async function applyIdentityReload() {
|
|
121816
|
+
const watch = identityWatch;
|
|
121817
|
+
if (!watch) return;
|
|
121818
|
+
const { readVaultAddress: readVaultAddress2 } = await Promise.resolve().then(() => (init_core(), core_exports));
|
|
121819
|
+
const vault = watch.readVault();
|
|
121820
|
+
const nextEoa = vault ? readVaultAddress2(vault) : "";
|
|
121821
|
+
const resolved = await resolveApiKey(cliConfig?.credentialStore ?? "keychain");
|
|
121822
|
+
const previousKey = currentCredentials.apiKey ?? "";
|
|
121823
|
+
const previousEoa = currentCredentials.eoaAddress ?? "";
|
|
121824
|
+
currentCredentials.walletKeystoreJson = vault ?? "";
|
|
121825
|
+
currentCredentials.eoaAddress = nextEoa;
|
|
121826
|
+
currentCredentials.signerAddress = nextEoa;
|
|
121827
|
+
if (resolved.apiKey) currentCredentials.apiKey = resolved.apiKey;
|
|
121828
|
+
refreshPlaintextVaultMode();
|
|
121829
|
+
clearWalletScopedCredentials(currentCredentials);
|
|
121830
|
+
delete currentCredentials.passphrase;
|
|
121831
|
+
delete currentCredentials.wrongPassphraseAttempts;
|
|
121832
|
+
delete currentCredentials.requiresNewPolicy;
|
|
121833
|
+
dropIdentityScopedCaches();
|
|
121834
|
+
console.error(
|
|
121835
|
+
`identity reloaded: ${maskApiKey(previousKey)} -> ${maskApiKey(currentCredentials.apiKey ?? "")}, eoa ${previousEoa || "(none)"} -> ${nextEoa || "(none)"}`
|
|
121836
|
+
);
|
|
121837
|
+
}
|
|
121838
|
+
async function ensureIdentityFresh() {
|
|
121839
|
+
let change = null;
|
|
121840
|
+
try {
|
|
121841
|
+
change = identityWatch?.poll() ?? null;
|
|
121842
|
+
} catch {
|
|
121843
|
+
return;
|
|
121844
|
+
}
|
|
121845
|
+
if (!change) return;
|
|
121846
|
+
try {
|
|
121847
|
+
await applyIdentityReload();
|
|
121848
|
+
} catch (err) {
|
|
121849
|
+
console.error(
|
|
121850
|
+
`\u26A0\uFE0F identity reload failed \u2014 continuing with the identity loaded at boot: ${err instanceof Error ? err.message : String(err)}`
|
|
121851
|
+
);
|
|
121852
|
+
}
|
|
121853
|
+
}
|
|
121527
121854
|
var __registerTool = server.tool.bind(server);
|
|
121528
121855
|
server.tool = (def, handler) => __registerTool(
|
|
121529
121856
|
def,
|
|
@@ -121534,6 +121861,7 @@ server.tool = (def, handler) => __registerTool(
|
|
|
121534
121861
|
// session, unconditionally. Only `severity: "block"` (an unsupported CLI)
|
|
121535
121862
|
// still refuses, and that refusal happens inside the gate.
|
|
121536
121863
|
runToolThroughInterstitialGate(def?.name, async () => {
|
|
121864
|
+
await ensureIdentityFresh();
|
|
121537
121865
|
const res = await handler(params);
|
|
121538
121866
|
maybeAutoShareOnToolCall();
|
|
121539
121867
|
const withNudge = maybeAppendCardNudge(res, def?.name, toolCallCount);
|
|
@@ -122248,6 +122576,7 @@ async function initializeCredentials(walletKeystoreJson, eoaAddress, config) {
|
|
|
122248
122576
|
currentCredentials.signerAddress = eoaAddress;
|
|
122249
122577
|
currentCredentials.eoaAddress = eoaAddress;
|
|
122250
122578
|
currentCredentials.apiKey = config.apiKey;
|
|
122579
|
+
identityWatch = new IdentityWatch({ vaultPath: VAULT_PATH, source: config.apiKeySource ?? null });
|
|
122251
122580
|
mcpEventLogger = new McpEventLogger(
|
|
122252
122581
|
() => getSDK(),
|
|
122253
122582
|
() => currentCredentials.apiKey,
|
|
@@ -122467,17 +122796,6 @@ var approvalWaitConfig = (() => {
|
|
|
122467
122796
|
delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
122468
122797
|
};
|
|
122469
122798
|
})();
|
|
122470
|
-
function findCurrentApprovedWallet(wallets, eoaAddress, pendingWalletId) {
|
|
122471
|
-
if (!Array.isArray(wallets)) return void 0;
|
|
122472
|
-
if (pendingWalletId) {
|
|
122473
|
-
return wallets.find(
|
|
122474
|
-
(w) => String(w?.id) === String(pendingWalletId) && w?.status === "approved"
|
|
122475
|
-
);
|
|
122476
|
-
}
|
|
122477
|
-
return wallets.find(
|
|
122478
|
-
(w) => w?.address?.toLowerCase() === eoaAddress?.toLowerCase() && w?.status === "approved"
|
|
122479
|
-
);
|
|
122480
|
-
}
|
|
122481
122799
|
async function tryResolvePendingApproval(probe) {
|
|
122482
122800
|
if (currentCredentials.authorizationStatus === "approved") return true;
|
|
122483
122801
|
if (currentCredentials.authorizationStatus !== "pending" || !currentCredentials.apiKey || !currentCredentials.eoaAddress) {
|
|
@@ -122615,11 +122933,20 @@ async function applyStartSessionUnlock(passphrase, resetRequested) {
|
|
|
122615
122933
|
persistVault: persistVault2,
|
|
122616
122934
|
wipeVault: wipeVault2,
|
|
122617
122935
|
sdk,
|
|
122618
|
-
|
|
122936
|
+
// FORCED REFRESH. `start_session` verifies the session's grant against the
|
|
122937
|
+
// backend (see `runStartSession`), and a verification run against the cache
|
|
122938
|
+
// this process loaded at boot verifies nothing: a grant approved since boot
|
|
122939
|
+
// would be invisible to exactly the check that exists to find it.
|
|
122940
|
+
fetchWallets: () => fetchVirtualWalletsFromBackend(currentCredentials.apiKey, true),
|
|
122619
122941
|
readPolicyActiveOnchain: buildPolicyActiveOnchainReader(sdk)
|
|
122620
122942
|
}
|
|
122621
122943
|
);
|
|
122622
|
-
const { walletChanged } = mergeStartSessionCredentials(
|
|
122944
|
+
const { walletChanged } = mergeStartSessionCredentials(
|
|
122945
|
+
currentCredentials,
|
|
122946
|
+
result.credentials,
|
|
122947
|
+
result.responseKey,
|
|
122948
|
+
{ grantChanged: result.grantChanged }
|
|
122949
|
+
);
|
|
122623
122950
|
refreshPlaintextVaultMode();
|
|
122624
122951
|
if (walletChanged) {
|
|
122625
122952
|
walletsCache = null;
|
|
@@ -123086,7 +123413,7 @@ server.tool(
|
|
|
123086
123413
|
server.tool(
|
|
123087
123414
|
{
|
|
123088
123415
|
name: "getWalletStatus",
|
|
123089
|
-
description: "Get wallet status: auth, balance, policy, allowlist, and the last 10 transactions (amount, merchant, timestamp, status). A transaction `amount` is a EUR display string like `\u20AC0.0126`, or the em dash `\u2014` when the row carries no usable amount (none recorded, or a value that is not a base-units integer) \u2014 `\u2014` means UNKNOWN, never zero. The unformatted base-units value is on `amountRaw`, which is null for exactly those rows.",
|
|
123416
|
+
description: "Get wallet status: auth, balance, policy, allowlist, WHICH WORKSPACE this server is acting in, and the last 10 transactions (amount, merchant, timestamp, status). `workspace` is `{companyName, employeeName}` \u2014 the Ametyst workspace every task, memory document and payment from this server lands in; both fields are null when the profile could not be read, which means UNKNOWN, never 'no workspace'. A transaction `amount` is a EUR display string like `\u20AC0.0126`, or the em dash `\u2014` when the row carries no usable amount (none recorded, or a value that is not a base-units integer) \u2014 `\u2014` means UNKNOWN, never zero. The unformatted base-units value is on `amountRaw`, which is null for exactly those rows.",
|
|
123090
123417
|
inputs: []
|
|
123091
123418
|
},
|
|
123092
123419
|
async () => {
|
|
@@ -123118,10 +123445,9 @@ server.tool(
|
|
|
123118
123445
|
fetchVirtualWalletsFromBackend(currentCredentials.apiKey, !refreshedFromBackend),
|
|
123119
123446
|
fetchPoliciesFromBackend(currentCredentials.apiKey, true)
|
|
123120
123447
|
]);
|
|
123121
|
-
const freshWallet = freshWallets.
|
|
123122
|
-
|
|
123123
|
-
)
|
|
123124
|
-
if (freshWallet && freshWallet.status === "approved") {
|
|
123448
|
+
const freshWallet = selectNewestApprovedWallet(freshWallets, currentCredentials.eoaAddress);
|
|
123449
|
+
const freshWalletIsActiveGrant = String(freshWallet?.id) === String(currentCredentials.virtualWalletId);
|
|
123450
|
+
if (freshWallet && freshWalletIsActiveGrant) {
|
|
123125
123451
|
if (freshWallet.policyAssociated != null) {
|
|
123126
123452
|
currentCredentials.policyId = String(freshWallet.policyAssociated);
|
|
123127
123453
|
}
|
|
@@ -123141,7 +123467,20 @@ server.tool(
|
|
|
123141
123467
|
status: currentCredentials.authorizationStatus || "none",
|
|
123142
123468
|
eoaAddress: currentCredentials.eoaAddress || null,
|
|
123143
123469
|
walletAddress: currentCredentials.walletAddress || null,
|
|
123144
|
-
kernelClientActive: !!currentCredentials.virtualWalletKernelAccountClient
|
|
123470
|
+
kernelClientActive: !!currentCredentials.virtualWalletKernelAccountClient,
|
|
123471
|
+
// WHICH GRANT THIS SESSION SIGNS WITH. The 2026-09-01 incident — three
|
|
123472
|
+
// approved rows for one EOA, every session silently signing with the oldest
|
|
123473
|
+
// — was invisible in every payload: diagnosing it took a nonce-key autopsy.
|
|
123474
|
+
// These two ids are the row `start_session` actually selected, so the next
|
|
123475
|
+
// stale grant is a field to read rather than a forensic exercise. Null until
|
|
123476
|
+
// a session has adopted one.
|
|
123477
|
+
activeVirtualWalletId: currentCredentials.virtualWalletId != null ? String(currentCredentials.virtualWalletId) : null,
|
|
123478
|
+
activePolicyId: currentCredentials.policyId != null ? String(currentCredentials.policyId) : null,
|
|
123479
|
+
// WHICH WORKSPACE THIS IS. The 2026-08-31 incident produced writes into
|
|
123480
|
+
// the wrong workspace with nothing in any payload naming one — an agent
|
|
123481
|
+
// reading wallet status could not have told, and neither could a human
|
|
123482
|
+
// reading the transcript afterwards. Nulls when `me` could not be read.
|
|
123483
|
+
workspace: await resolveWorkspaceIdentity()
|
|
123145
123484
|
};
|
|
123146
123485
|
if (policiesCache && currentCredentials.policyId) {
|
|
123147
123486
|
const policy = policiesCache.find((p) => p.id === parseInt(currentCredentials.policyId, 10));
|
|
@@ -123150,7 +123489,7 @@ server.tool(
|
|
|
123150
123489
|
}
|
|
123151
123490
|
}
|
|
123152
123491
|
if (walletsCache && currentCredentials.eoaAddress) {
|
|
123153
|
-
const wallet = walletsCache.find(
|
|
123492
|
+
const wallet = selectNewestApprovedWallet(walletsCache, currentCredentials.eoaAddress) ?? walletsCache.find(
|
|
123154
123493
|
(w) => w.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase()
|
|
123155
123494
|
);
|
|
123156
123495
|
if (wallet) {
|
|
@@ -123597,6 +123936,7 @@ async function resolveTasksCore(params, flavor) {
|
|
|
123597
123936
|
if (slug && flavor.fetchManifest) {
|
|
123598
123937
|
const manifest = await flavor.fetchManifest(sdk, currentCredentials.apiKey, slug);
|
|
123599
123938
|
if (manifest) payload.stateDocs = manifest;
|
|
123939
|
+
if (manifest !== void 0) payload.memory = memorySummaryLine(manifest);
|
|
123600
123940
|
}
|
|
123601
123941
|
}
|
|
123602
123942
|
}
|
|
@@ -123642,6 +123982,7 @@ function taskMemoryBlock(slug, docKey) {
|
|
|
123642
123982
|
};
|
|
123643
123983
|
}
|
|
123644
123984
|
var liveDashboards = /* @__PURE__ */ new Map();
|
|
123985
|
+
var LIVE_DASHBOARD_WATCH_MS = 15e3;
|
|
123645
123986
|
function closeAllLiveDashboards() {
|
|
123646
123987
|
for (const h of liveDashboards.values()) {
|
|
123647
123988
|
try {
|
|
@@ -123669,6 +124010,22 @@ async function startLiveDashboard(entity, dir) {
|
|
|
123669
124010
|
});
|
|
123670
124011
|
if (!handle) return null;
|
|
123671
124012
|
liveDashboards.set(slug, handle);
|
|
124013
|
+
const watcher = setInterval(() => {
|
|
124014
|
+
if (liveDashboards.get(slug) !== handle) {
|
|
124015
|
+
clearInterval(watcher);
|
|
124016
|
+
return;
|
|
124017
|
+
}
|
|
124018
|
+
if (!existsSync12(dir)) {
|
|
124019
|
+
clearInterval(watcher);
|
|
124020
|
+
liveDashboards.delete(slug);
|
|
124021
|
+
try {
|
|
124022
|
+
handle.close();
|
|
124023
|
+
} catch {
|
|
124024
|
+
}
|
|
124025
|
+
console.error(`(live dashboard for ${slug} closed \u2014 run folder ${dir} is gone)`);
|
|
124026
|
+
}
|
|
124027
|
+
}, LIVE_DASHBOARD_WATCH_MS);
|
|
124028
|
+
watcher.unref?.();
|
|
123672
124029
|
const url2 = `http://localhost:${handle.port}`;
|
|
123673
124030
|
openDashboardInBrowser(url2, { isTTY: true });
|
|
123674
124031
|
return url2;
|
|
@@ -123733,6 +124090,9 @@ async function runTaskCore(params, flavor) {
|
|
|
123733
124090
|
}
|
|
123734
124091
|
const memoryDocKey = defaultDocKey(normalizeMemoryManifest(entity?.stateDocs ?? null));
|
|
123735
124092
|
const materialized = flavor.materialize(entity, randomUUID4());
|
|
124093
|
+
const docBoot = await materializeMemoryDocs(sdk, apiKey, entity, materialized.dir);
|
|
124094
|
+
for (const note of docBoot.notes) console.error(`(${entity.slug} memory docs: ${note})`);
|
|
124095
|
+
const runFiles = { ...materialized.files, ...docBoot.files };
|
|
123736
124096
|
const est = estimateBlastRadius(entity);
|
|
123737
124097
|
const shipBack = flavor.buildShipBack({ dir: materialized.dir, entity });
|
|
123738
124098
|
const dashboardUrl = await startLiveDashboard(entity, materialized.dir);
|
|
@@ -123744,14 +124104,14 @@ async function runTaskCore(params, flavor) {
|
|
|
123744
124104
|
dir: materialized.dir,
|
|
123745
124105
|
...dashboardUrl ? { dashboard: dashboardUrl } : {},
|
|
123746
124106
|
blastRadius: est,
|
|
123747
|
-
files:
|
|
124107
|
+
files: runFiles,
|
|
123748
124108
|
...materialized.skipped ? { skippedEmptyFiles: materialized.skipped } : {},
|
|
123749
124109
|
// The run's durable memory (loop-memory-primitive). `dir` dies with the run, so
|
|
123750
124110
|
// anything the NEXT run needs has to be written here instead. Named explicitly with
|
|
123751
124111
|
// the slug because the run context carries no task identity — which is exactly why
|
|
123752
124112
|
// the taskMemory* tools take an explicit `taskSlug`.
|
|
123753
124113
|
memory: taskMemoryBlock(entity.slug, memoryDocKey),
|
|
123754
|
-
directive: flavor.buildDirective({ dir: materialized.dir, slug: entity.slug, files:
|
|
124114
|
+
directive: flavor.buildDirective({ dir: materialized.dir, slug: entity.slug, files: runFiles, docKey: memoryDocKey }),
|
|
123755
124115
|
...shipBack ? { shipBack } : {}
|
|
123756
124116
|
}) }, { type: "text", text: dashboardLine }] };
|
|
123757
124117
|
} catch (error) {
|
|
@@ -124281,7 +124641,7 @@ async function upsertTaskCore(params, flavor) {
|
|
|
124281
124641
|
return { available: false, reason: err instanceof Error ? err.message : String(err) };
|
|
124282
124642
|
}
|
|
124283
124643
|
};
|
|
124284
|
-
const upsert = async (entity, sourcePaths = {}) => {
|
|
124644
|
+
const upsert = async (entity, sourcePaths = {}, memoryDefaulted = false) => {
|
|
124285
124645
|
if (!id && flavor.injectDashboardOnCreate) {
|
|
124286
124646
|
try {
|
|
124287
124647
|
injectDefaultDashboard(entity);
|
|
@@ -124293,6 +124653,7 @@ async function upsertTaskCore(params, flavor) {
|
|
|
124293
124653
|
if (res.status === "ok") {
|
|
124294
124654
|
void refreshDynamicPrompts();
|
|
124295
124655
|
const mode2 = id ? "modified" : "created";
|
|
124656
|
+
const effectiveManifest = "stateDocs" in entity ? entity.stateDocs : !id ? null : previous && previous.available ? previous.content.stateDocs ?? null : void 0;
|
|
124296
124657
|
const summary = buildLoopUpsertSummary({
|
|
124297
124658
|
mode: mode2,
|
|
124298
124659
|
sent: entity,
|
|
@@ -124303,7 +124664,26 @@ async function upsertTaskCore(params, flavor) {
|
|
|
124303
124664
|
success: true,
|
|
124304
124665
|
mode: mode2,
|
|
124305
124666
|
[flavor.responseKey]: projectLoopIdentity(flavor.pick(res)),
|
|
124306
|
-
|
|
124667
|
+
// WHERE IT LANDED, on the receipt itself. The 2026-08-31 incident put a
|
|
124668
|
+
// task into the wrong (admin) workspace and the success payload named
|
|
124669
|
+
// no workspace at all, so neither the agent nor the human reading the
|
|
124670
|
+
// transcript afterwards could tell. Nulls when `me` could not be read —
|
|
124671
|
+
// unknown, never a guess.
|
|
124672
|
+
receipt: {
|
|
124673
|
+
fields: summary.receipt,
|
|
124674
|
+
workspace: await resolveWorkspaceIdentity(),
|
|
124675
|
+
// WHERE THE TASK'S MEMORY LANDED, said in the user's vocabulary rather than left
|
|
124676
|
+
// implicit in the manifest bytes. `effectiveManifest` is what the task HAS after
|
|
124677
|
+
// this call: what this call sent, or — on a MODIFY that did not send one — the
|
|
124678
|
+
// stored manifest the diff already read. Never a fourth read.
|
|
124679
|
+
//
|
|
124680
|
+
// ⛔ THE FLOOR IS NAMED, NOT HIDDEN. When the default fired the line still reports
|
|
124681
|
+
// where the memory landed (`Memory: per member` — it really is per member), with a
|
|
124682
|
+
// suffix saying the author declared nothing. A receipt printing the bare line would
|
|
124683
|
+
// present the floor as an authored choice, which is the one thing this injection
|
|
124684
|
+
// must never be allowed to look like.
|
|
124685
|
+
...effectiveManifest !== void 0 ? { memory: memorySummaryLine(effectiveManifest) + (memoryDefaulted ? MEMORY_DEFAULTED_SUFFIX : "") } : {}
|
|
124686
|
+
},
|
|
124307
124687
|
summary: summary.text,
|
|
124308
124688
|
summaryTruncated: summary.truncated,
|
|
124309
124689
|
// ON CREATE ONLY. A task lands PRIVATE to the author, and the one step
|
|
@@ -124394,8 +124774,13 @@ async function upsertTaskCore(params, flavor) {
|
|
|
124394
124774
|
}
|
|
124395
124775
|
stateDocs = checked.manifest;
|
|
124396
124776
|
}
|
|
124777
|
+
let memoryDefaulted = false;
|
|
124778
|
+
if (!id && declaresNoMemory(stateDocs)) {
|
|
124779
|
+
stateDocs = defaultMemoryManifest();
|
|
124780
|
+
memoryDefaulted = true;
|
|
124781
|
+
}
|
|
124397
124782
|
const body = { ...resolvedMd };
|
|
124398
|
-
if (stateDocsProvided) body.stateDocs = stateDocs;
|
|
124783
|
+
if (stateDocsProvided || memoryDefaulted) body.stateDocs = stateDocs;
|
|
124399
124784
|
if (slug) body.slug = slug;
|
|
124400
124785
|
if (descriptionShort) body.descriptionShort = descriptionShort;
|
|
124401
124786
|
if (markdownBody) body.markdownBody = markdownBody;
|
|
@@ -124410,7 +124795,7 @@ async function upsertTaskCore(params, flavor) {
|
|
|
124410
124795
|
sourcePaths[mdKey] = params[fileKey].trim();
|
|
124411
124796
|
}
|
|
124412
124797
|
}
|
|
124413
|
-
return await upsert(body, sourcePaths);
|
|
124798
|
+
return await upsert(body, sourcePaths, memoryDefaulted);
|
|
124414
124799
|
}
|
|
124415
124800
|
const hasMessages = typeof params.messages === "string" && params.messages.trim();
|
|
124416
124801
|
const hasBrief = typeof params.brief === "string" && params.brief.trim();
|
|
@@ -124435,7 +124820,7 @@ async function upsertTaskCore(params, flavor) {
|
|
|
124435
124820
|
server.tool(
|
|
124436
124821
|
{
|
|
124437
124822
|
name: "createTask",
|
|
124438
|
-
description: "Create OR modify a workspace TASK (upsert). A task is the unified card \u2014 what used to be published as either a 'compound skill' or a 'loop' is ONE row now, so this ONE tool authors both: a task with only a `markdownBody` is what a compound was, a task that also carries visionMd/constraintsMd/readmeMd is what a loop was. Pass `id` to MODIFY, omit it to CREATE. MODIFY IS A NO-CLOBBER PATCH: send `id` plus ONLY the field(s) you want to change \u2014 any of markdownBody/visionMd/constraintsMd/readmeMd/dashboardHtml/dashboardManifest/stateDocs/descriptionShort/category/draft/graphJson \u2014 fields you don't send are preserved, so a constraints-only ship-back never has to resend the body. VERBATIM BODIES: for any LONG markdown field, WRITE it to a local file first and pass the matching *FilePath (`filePath` for the body, `visionFilePath`/`constraintsFilePath`/`readmeFilePath`/`dashboardHtmlFilePath`/`dashboardManifestFilePath`) INSTEAD of inlining it \u2014 the local MCP server reads the bytes from disk and pushes them verbatim (no arg-size limit, no drift). RESPONSE: a closing `summary` derived strictly from the published content (never invented) plus a compact `receipt` of per-field byte counts and source paths; the full bodies are NOT echoed back. NOT AN AUTHORING TOOL: it publishes what you give it. To create, publish or audit a task, run `task-architect` first \u2014 this tool is only the final upsert it performs. NOTE: if the user's intent is to publish/upload/import local skills to Ametyst, call getTask FIRST to fetch the `task-architect` task \u2014 it contains the publish procedure to follow before using this tool. MEMORY: `stateDocs` is the task's memory manifest \u2014 which memory docs / record kinds it owns and whether each is `shared` by the workspace or per-`member`; the runner creates the declared docs at boot and ships each back to its declared namespace. DASHBOARD: createTask does NOT inject a default monitoring page on create \u2014 pass `dashboardHtml` if the task wants one.
|
|
124823
|
+
description: "Create OR modify a workspace TASK (upsert). A task is the unified card \u2014 what used to be published as either a 'compound skill' or a 'loop' is ONE row now, so this ONE tool authors both: a task with only a `markdownBody` is what a compound was, a task that also carries visionMd/constraintsMd/readmeMd is what a loop was. Pass `id` to MODIFY, omit it to CREATE. MODIFY IS A NO-CLOBBER PATCH: send `id` plus ONLY the field(s) you want to change \u2014 any of markdownBody/visionMd/constraintsMd/readmeMd/dashboardHtml/dashboardManifest/stateDocs/descriptionShort/category/draft/graphJson \u2014 fields you don't send are preserved, so a constraints-only ship-back never has to resend the body. VERBATIM BODIES: for any LONG markdown field, WRITE it to a local file first and pass the matching *FilePath (`filePath` for the body, `visionFilePath`/`constraintsFilePath`/`readmeFilePath`/`dashboardHtmlFilePath`/`dashboardManifestFilePath`) INSTEAD of inlining it \u2014 the local MCP server reads the bytes from disk and pushes them verbatim (no arg-size limit, no drift). RESPONSE: a closing `summary` derived strictly from the published content (never invented) plus a compact `receipt` of per-field byte counts and source paths; the full bodies are NOT echoed back. NOT AN AUTHORING TOOL: it publishes what you give it. To create, publish or audit a task, run `task-architect` first \u2014 this tool is only the final upsert it performs. NOTE: if the user's intent is to publish/upload/import local skills to Ametyst, call getTask FIRST to fetch the `task-architect` task \u2014 it contains the publish procedure to follow before using this tool. MEMORY: `stateDocs` is the task's memory manifest \u2014 which memory docs / record kinds it owns and whether each is `shared` by the workspace or per-`member`; the runner creates the declared docs at boot and ships each back to its declared namespace. \u26D4 THE SCOPE CHOICE IS THE USER'S, NOT YOURS: BEFORE you create or modify a task that carries a memory manifest, EXPLAIN THE CHOICE TO THEM in your own words \u2014 never as `stateDocs` or `scope` \u2014 covering all of the following, and ASK them when their intent is ambiguous instead of deciding for them. Quote it verbatim if that is clearer; never contradict it: \"" + MEMORY_SCOPE_EXPLANATION + '" The response says which one the task ended up with, as a `memory` line on the receipt (`Memory: global` / `Memory: per member` / `Memory: mixed \u2014 N global \xB7 M per member` / `Memory: none`); getTask carries the same line. \u26D4 DECLARING THE MANIFEST IS EXPECTED ON EVERY TASK: a CREATE that declares none \u2014 or a declared-empty one \u2014 is given a floor rather than being born with no memory (the run diary, per member: `{"docs":[],"records":[{"kind":"run","scope":"member"}]}`), and the receipt says so with `(defaulted \u2014 no manifest declared)`, but that floor is a backstop and NOT a substitute for deriving the manifest this task actually needs and explaining the scope choice to the user first. DASHBOARD: createTask does NOT inject a default monitoring page on create \u2014 pass `dashboardHtml` if the task wants one.',
|
|
124439
124824
|
inputs: [
|
|
124440
124825
|
{ name: "slug", type: "string", required: false, description: "URL-safe unique slug for the task within the workspace. Required on CREATE." },
|
|
124441
124826
|
{ name: "descriptionShort", type: "string", required: false, description: "One-line description of what the task does. Required on CREATE." },
|
|
@@ -124451,7 +124836,7 @@ server.tool(
|
|
|
124451
124836
|
{ name: "dashboardHtmlFilePath", type: "string", required: false, description: "Path to a local file read verbatim as the dashboard HTML. Takes precedence over `dashboardHtml`." },
|
|
124452
124837
|
{ name: "dashboardManifest", type: "string", required: false, description: `JSON (as text) naming which materialized files the dashboard's /data endpoint surfaces (e.g. {"files":["STATUS.md"]}).` },
|
|
124453
124838
|
{ name: "dashboardManifestFilePath", type: "string", required: false, description: "Path to a local file read verbatim as the dashboard manifest. Takes precedence over `dashboardManifest`." },
|
|
124454
|
-
{ name: "stateDocs", type: "string", required: false, description: 'THE MEMORY MANIFEST \u2014 JSON TEXT: `{"docs":[{"key":"board","scope":"shared"}],"records":[{"kind":"run","scope":"member"}]}`. It declares WHICH memory documents and record kinds this task owns and WHERE each one lives: `scope` is `shared` (ONE row for the whole workspace \u2014 every member reads and writes the same document) or `member` (one row PER SEAT \u2014 each member has their own). `records[].kind` is a RESERVED diary kind (run | decision | error | import \u2014 always accepted by the server, declared or not; declaring one only fixes its scope) or a FREE item kind the task invents (a token [a-z0-9-]{1,64}, e.g. `pbi`, `test`, `needs-patrick` \u2014 keyed, versioned, closed with taskMemoryArchive; see the TASK MEMORY MODEL in your instructions). Both arrays are REQUIRED and may be empty (`{"docs":[],"records":[]}` = this task keeps no memory). \u26D4 A STRING, ALWAYS: send the JSON object as text, `"null"` to clear an existing manifest, and omit the field to leave it untouched; a bare object or a bare null is REJECTED by this tool\'s input schema before the call runs, and an EMPTY STRING is treated exactly like omitting the field. WHAT THE RUNNER DOES WITH IT: at boot every declared doc is read from its declared namespace and CREATED there (empty) if it does not exist yet \u2014 a `member` doc is seeded from the `shared` row of the same key when one exists \u2014 and materialized as `<key>.md`; at exit each changed doc is shipped back to the SAME namespace. A write through taskMemoryAppend to a declared key/kind needs no scope (the server resolves it and refuses a disagreeing one). RULES, enforced here AND by the server (the call fails and the task is neither created nor modified): `key` is a safe basename of 1..128 chars (no `/`, `\\`, no leading dot), unique case-insensitively, and NOT one of the seven files the runner writes itself (SKILL.md, VISION.md, CONSTRAINTS.md, README.md, STATUS.md, dashboard.html, dashboard.manifest.json \u2014 with or without `.md`); at most 64 docs and 16 records, one entry per kind. The manifest has NO archive section \u2014 an archived item is the same kind in the same namespace with archived: true. The retired array shape (`[{key, filename, \u2026}]`, including `"[]"`) is INVALID. \u26D4 DERIVE the manifest from what the task actually needs to remember between runs and who needs to see it \u2014 a board the whole team works is `shared`, a personal cursor is `member` \u2014 and explain it to the author IN THEIR WORDS ("everyone on the team sees the same to-do list"), never as `stateDocs` or `scope`.' },
|
|
124839
|
+
{ name: "stateDocs", type: "string", required: false, description: 'THE MEMORY MANIFEST \u2014 JSON TEXT: `{"docs":[{"key":"board","scope":"shared"}],"records":[{"kind":"run","scope":"member"}]}`. It declares WHICH memory documents and record kinds this task owns and WHERE each one lives: `scope` is `shared` (ONE row for the whole workspace \u2014 every member reads and writes the same document) or `member` (one row PER SEAT \u2014 each member has their own). `records[].kind` is a RESERVED diary kind (run | decision | error | import \u2014 always accepted by the server, declared or not; declaring one only fixes its scope) or a FREE item kind the task invents (a token [a-z0-9-]{1,64}, e.g. `pbi`, `test`, `needs-patrick` \u2014 keyed, versioned, closed with taskMemoryArchive; see the TASK MEMORY MODEL in your instructions). Both arrays are REQUIRED and may be empty (`{"docs":[],"records":[]}` = this task keeps no memory). \u26D4 A STRING, ALWAYS: send the JSON object as text, `"null"` to clear an existing manifest, and omit the field to leave it untouched; a bare object or a bare null is REJECTED by this tool\'s input schema before the call runs, and an EMPTY STRING is treated exactly like omitting the field. WHAT THE RUNNER DOES WITH IT: at boot every declared doc is read from its declared namespace and CREATED there (empty) if it does not exist yet \u2014 a `member` doc is seeded from the `shared` row of the same key when one exists \u2014 and materialized as `<key>.md`; at exit each changed doc is shipped back to the SAME namespace. A write through taskMemoryAppend to a declared key/kind needs no scope (the server resolves it and refuses a disagreeing one). RULES, enforced here AND by the server (the call fails and the task is neither created nor modified): `key` is a safe basename of 1..128 chars (no `/`, `\\`, no leading dot), unique case-insensitively, and NOT one of the seven files the runner writes itself (SKILL.md, VISION.md, CONSTRAINTS.md, README.md, STATUS.md, dashboard.html, dashboard.manifest.json \u2014 with or without `.md`); at most 64 docs and 16 records, one entry per kind. The manifest has NO archive section \u2014 an archived item is the same kind in the same namespace with archived: true. The retired array shape (`[{key, filename, \u2026}]`, including `"[]"`) is INVALID. \u26D4 DERIVE the manifest from what the task actually needs to remember between runs and who needs to see it \u2014 a board the whole team works is `shared`, a personal cursor is `member` \u2014 and explain it to the author IN THEIR WORDS ("everyone on the team sees the same to-do list"), never as `stateDocs` or `scope`. \u26D4 AND YOU DO THAT BEFORE THIS CALL, NOT AFTER \u2014 the choice is the user\'s: cover all of this with them, and ASK when their intent is ambiguous instead of deciding for them. "' + MEMORY_SCOPE_EXPLANATION + '"' },
|
|
124455
124840
|
{ name: "graphJson", type: "string", required: false, description: "JSON string of the canvas node graph (for re-edit). Defaults to {} on create." },
|
|
124456
124841
|
{ name: "draft", type: "boolean", required: false, description: "Whether the task is a draft (excluded from discovery). Defaults to true on create." },
|
|
124457
124842
|
{ name: "category", type: "string", required: false, description: "Free-text category for browsing/filtering (e.g. 'research'). Must be explicit \u2014 the tool asks rather than guessing." },
|
|
@@ -125469,9 +125854,7 @@ async function reconcileApprovalStateOnConnect() {
|
|
|
125469
125854
|
}
|
|
125470
125855
|
try {
|
|
125471
125856
|
const wallets = await fetchVirtualWalletsFromBackend(currentCredentials.apiKey, true);
|
|
125472
|
-
const approved = wallets.
|
|
125473
|
-
(w) => w.address?.toLowerCase() === currentCredentials.eoaAddress?.toLowerCase() && w.status === "approved"
|
|
125474
|
-
);
|
|
125857
|
+
const approved = selectNewestApprovedWallet(wallets, currentCredentials.eoaAddress);
|
|
125475
125858
|
if (!approved) return;
|
|
125476
125859
|
if (typeof approved.id === "number" && hasProcessedApprovalEventId(approved.id)) return;
|
|
125477
125860
|
console.error(
|
|
@@ -125809,14 +126192,14 @@ function killPreviousServeInstances(deps) {
|
|
|
125809
126192
|
|
|
125810
126193
|
// src/commands/autosync-skills.ts
|
|
125811
126194
|
init_esm_shims();
|
|
125812
|
-
import { existsSync as
|
|
126195
|
+
import { existsSync as existsSync14 } from "fs";
|
|
125813
126196
|
import { homedir as homedir12 } from "os";
|
|
125814
126197
|
import { join as join18 } from "path";
|
|
125815
126198
|
|
|
125816
126199
|
// src/compounds/sync-skills.ts
|
|
125817
126200
|
init_esm_shims();
|
|
125818
126201
|
init_paths();
|
|
125819
|
-
import { existsSync as
|
|
126202
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync13, rmSync, writeFileSync as writeFileSync10 } from "fs";
|
|
125820
126203
|
import { join as join17 } from "path";
|
|
125821
126204
|
var MANAGED_MARKER = "<!-- ametyst-managed: sync-skills -->";
|
|
125822
126205
|
var GITIGNORE_HEADER = "# ametyst-managed: sync-skills \u2014 pointer skills generated for this account; never commit them.";
|
|
@@ -125861,7 +126244,7 @@ ${taskRunSection(slug, kind)}`;
|
|
|
125861
126244
|
}
|
|
125862
126245
|
function isManaged(file) {
|
|
125863
126246
|
try {
|
|
125864
|
-
return
|
|
126247
|
+
return readFileSync13(file, "utf8").includes(MANAGED_MARKER);
|
|
125865
126248
|
} catch {
|
|
125866
126249
|
return false;
|
|
125867
126250
|
}
|
|
@@ -125875,8 +126258,8 @@ function buildGitignoreContent(managedSlugs) {
|
|
|
125875
126258
|
function maintainGitignore(root2) {
|
|
125876
126259
|
const file = join17(root2, ".gitignore");
|
|
125877
126260
|
const managed = listManagedDirs(root2);
|
|
125878
|
-
if (
|
|
125879
|
-
const current =
|
|
126261
|
+
if (existsSync13(file)) {
|
|
126262
|
+
const current = readFileSync13(file, "utf8");
|
|
125880
126263
|
const firstLine = current.split(/\r?\n/, 1)[0];
|
|
125881
126264
|
if (firstLine !== GITIGNORE_HEADER) {
|
|
125882
126265
|
console.error(
|
|
@@ -125939,7 +126322,7 @@ async function syncSkills(opts = {}) {
|
|
|
125939
126322
|
const skipped = [];
|
|
125940
126323
|
for (const [dir, item] of desired) {
|
|
125941
126324
|
const file = join17(dir, "SKILL.md");
|
|
125942
|
-
if (
|
|
126325
|
+
if (existsSync13(file) && !isManaged(file)) {
|
|
125943
126326
|
skipped.push(item.slug);
|
|
125944
126327
|
console.warn(`\u26A0\uFE0F sync-skills: skipping "${item.slug}" \u2014 an unmanaged skill already exists at ${file}`);
|
|
125945
126328
|
continue;
|
|
@@ -125967,7 +126350,7 @@ function isSkillsAutosyncEnabled(env = process.env) {
|
|
|
125967
126350
|
return !(raw === "0" || raw === "false" || raw === "off" || raw === "no");
|
|
125968
126351
|
}
|
|
125969
126352
|
function detectPresentTargets(deps = {}) {
|
|
125970
|
-
const exists = deps.existsSync ??
|
|
126353
|
+
const exists = deps.existsSync ?? existsSync14;
|
|
125971
126354
|
const cwd = deps.cwd ?? (() => process.cwd());
|
|
125972
126355
|
const home = deps.homedir ?? homedir12;
|
|
125973
126356
|
const targets = [];
|
|
@@ -126047,7 +126430,7 @@ async function serveCommand() {
|
|
|
126047
126430
|
console.error(
|
|
126048
126431
|
persisted ? isPlaintextVault(persisted) ? "Starting MCP server... (persisted UNENCRYPTED wallet found \u2014 always unlocked, no start_session needed)" : "Starting MCP server... (persisted wallet found \u2014 unlock with start_session)" : "Starting MCP server... (no saved wallet \u2014 one will be created on start_session)"
|
|
126049
126432
|
);
|
|
126050
|
-
await startMCPServer(keystore, eoa, { ...config, apiKey }, versionNotice, {
|
|
126433
|
+
await startMCPServer(keystore, eoa, { ...config, apiKey, apiKeySource: resolved.source ?? void 0 }, versionNotice, {
|
|
126051
126434
|
// Best-effort: materialize local `/`-command pointer skills for every published
|
|
126052
126435
|
// compound + loop so they're available without a manual `compound sync-skills`.
|
|
126053
126436
|
// Deferred from boot to the MCP initialize handshake so the sync is CLIENT-AWARE:
|
|
@@ -126986,7 +127369,7 @@ function recordFailedLaunch(args) {
|
|
|
126986
127369
|
init_esm_shims();
|
|
126987
127370
|
import { spawn as spawn2 } from "child_process";
|
|
126988
127371
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
126989
|
-
import { existsSync as
|
|
127372
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync as rmSync2, writeFileSync as writeFileSync12 } from "fs";
|
|
126990
127373
|
import { dirname as dirname8, join as join23 } from "path";
|
|
126991
127374
|
|
|
126992
127375
|
// src/loops/claude-binary.ts
|
|
@@ -127178,7 +127561,7 @@ async function runLoop(loopId, opts = {}) {
|
|
|
127178
127561
|
`Loop ${loop2.slug}: ${est.steps} steps (${est.paidSteps} paid), est \u20AC${est.estCostEur ?? "?"} \u2014 ${capLabel}`
|
|
127179
127562
|
);
|
|
127180
127563
|
const statusPath = join23(dir, "STATUS.md");
|
|
127181
|
-
const statusBefore =
|
|
127564
|
+
const statusBefore = existsSync15(statusPath) ? readFileSync14(statusPath, "utf-8") : "";
|
|
127182
127565
|
writeFileSync12(
|
|
127183
127566
|
statusPath,
|
|
127184
127567
|
statusBefore.replace(
|
|
@@ -127238,7 +127621,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
|
|
|
127238
127621
|
async function shipConstraints() {
|
|
127239
127622
|
try {
|
|
127240
127623
|
const constraintsPath = join23(dir, "CONSTRAINTS.md");
|
|
127241
|
-
const materializedConstraints =
|
|
127624
|
+
const materializedConstraints = existsSync15(constraintsPath) ? readFileSync14(constraintsPath, "utf-8") : void 0;
|
|
127242
127625
|
if (materializedConstraints === void 0) return;
|
|
127243
127626
|
const boot = loop2.constraintsMd ?? "";
|
|
127244
127627
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
@@ -127349,7 +127732,7 @@ blast_radius: steps=${est.steps} paid=${est.paidSteps} estEur=${est.estCostEur ?
|
|
|
127349
127732
|
startedAtEpochMs
|
|
127350
127733
|
});
|
|
127351
127734
|
}
|
|
127352
|
-
const statusAfter =
|
|
127735
|
+
const statusAfter = existsSync15(statusPath) ? readFileSync14(statusPath, "utf-8") : "";
|
|
127353
127736
|
const clean2 = exitCode === 0 && /(vision\s*done|queue\s*drained|status:\s*done)/i.test(statusAfter) && !/(brake|crash|crashed|errored)/i.test(statusAfter);
|
|
127354
127737
|
await shipBackConstraints();
|
|
127355
127738
|
process.off("SIGINT", onSignal);
|
|
@@ -127387,7 +127770,7 @@ async function showLoop(loopId) {
|
|
|
127387
127770
|
// src/loops/schedule.ts
|
|
127388
127771
|
init_esm_shims();
|
|
127389
127772
|
init_paths();
|
|
127390
|
-
import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync11, rmSync as rmSync3, existsSync as
|
|
127773
|
+
import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync11, rmSync as rmSync3, existsSync as existsSync16, readdirSync as readdirSync5, readFileSync as readFileSync15, accessSync as accessSync2, constants as constants2 } from "fs";
|
|
127391
127774
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
127392
127775
|
import { join as join24, dirname as dirname9 } from "path";
|
|
127393
127776
|
import { homedir as homedir14 } from "os";
|
|
@@ -127467,7 +127850,7 @@ function isLoaded(lbl) {
|
|
|
127467
127850
|
}
|
|
127468
127851
|
function readInteractiveDefaultModel(home) {
|
|
127469
127852
|
try {
|
|
127470
|
-
const raw =
|
|
127853
|
+
const raw = readFileSync15(join24(home, ".claude", "settings.json"), "utf-8");
|
|
127471
127854
|
const model = JSON.parse(String(raw)).model;
|
|
127472
127855
|
return typeof model === "string" && model.trim() ? model.trim() : void 0;
|
|
127473
127856
|
} catch {
|
|
@@ -127695,7 +128078,7 @@ function list(kind, opts = {}) {
|
|
|
127695
128078
|
const prefix = kind.labelPrefix;
|
|
127696
128079
|
if (platform === "darwin") {
|
|
127697
128080
|
const dir = join24(home, "Library", "LaunchAgents");
|
|
127698
|
-
if (!
|
|
128081
|
+
if (!existsSync16(dir)) return [];
|
|
127699
128082
|
return readdirSync5(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => f.slice(prefix.length, -".plist".length));
|
|
127700
128083
|
}
|
|
127701
128084
|
return readCrontab().split("\n").filter((l) => l.includes(`# ${prefix}`)).map((l) => l.trimEnd().slice(l.trimEnd().lastIndexOf(prefix) + prefix.length));
|
|
@@ -127757,12 +128140,12 @@ function listEntries(kind, opts = {}) {
|
|
|
127757
128140
|
const prefix = kind.labelPrefix;
|
|
127758
128141
|
if (platform === "darwin") {
|
|
127759
128142
|
const dir = join24(home, "Library", "LaunchAgents");
|
|
127760
|
-
if (!
|
|
128143
|
+
if (!existsSync16(dir)) return [];
|
|
127761
128144
|
return readdirSync5(dir).filter((f) => f.startsWith(prefix) && f.endsWith(".plist")).map((f) => {
|
|
127762
128145
|
const slug = f.slice(prefix.length, -".plist".length);
|
|
127763
128146
|
let envKeys = [];
|
|
127764
128147
|
try {
|
|
127765
|
-
envKeys = plistEnvKeys(String(
|
|
128148
|
+
envKeys = plistEnvKeys(String(readFileSync15(join24(dir, f), "utf-8")));
|
|
127766
128149
|
} catch {
|
|
127767
128150
|
envKeys = [];
|
|
127768
128151
|
}
|