@elisym/cli 0.23.2 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -12
- package/dist/index.js +1884 -247
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --no-deprecation
|
|
2
2
|
import { ReadableStream } from 'node:stream/web';
|
|
3
|
-
import { readFileSync, existsSync, readdirSync, statSync, renameSync, chmodSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { readFileSync, existsSync, readdirSync, statSync, renameSync, chmodSync, mkdirSync, writeFileSync, appendFileSync, unlinkSync, rmdirSync, realpathSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, resolve, basename, extname, relative, sep } from 'node:path';
|
|
5
|
-
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, signerFromSecretKeyBase58, ElisymClient, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, createBlossomTransport, generateSolanaWallet, getProtocolConfig, getProtocolProgramId, calculateProtocolFee, LIMITS, makeCensor, DEFAULT_REDACT_PATHS, POLICY_T_TAG, createSlidingWindowLimiter, utf8ByteLength, readAcceptedTransports, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, encodeSecretKeyBase58, NATIVE_SOL } from '@elisym/sdk';
|
|
6
|
-
import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, ensureGitignoreHasIrohEntry, ensureGitignoreHasX402Entries, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
|
|
5
|
+
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, signerFromSecretKeyBase58, ElisymClient, KIND_EXTERNAL_IDENTITIES, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, createBlossomTransport, verifyAgentIdentities, generateSolanaWallet, getProtocolConfig, getProtocolProgramId, calculateProtocolFee, LIMITS, makeCensor, DEFAULT_REDACT_PATHS, POLICY_T_TAG, SESSION_ID_REGEX, createSlidingWindowLimiter, utf8ByteLength, readAcceptedTransports, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, GITHUB_USERNAME_REGEX, X_USERNAME_REGEX, normalizeNip05Identifier, splitNip05Identifier, encodeSecretKeyBase58, NATIVE_SOL, GIST_ID_REGEX, TWEET_ID_REGEX } from '@elisym/sdk';
|
|
6
|
+
import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, ensureGitignoreHasIrohEntry, ensureGitignoreHasX402Entries, ensureGitignoreHasSessionsEntry, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
|
|
7
7
|
import { isAddress, createSolanaRpc, address } from '@solana/kit';
|
|
8
8
|
import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools';
|
|
9
9
|
import YAML from 'yaml';
|
|
@@ -17,7 +17,7 @@ import pino from 'pino';
|
|
|
17
17
|
import { mkdir, writeFile, readFile, mkdtemp, rm, rename, stat } from 'node:fs/promises';
|
|
18
18
|
import { tmpdir } from 'node:os';
|
|
19
19
|
import pLimit from 'p-limit';
|
|
20
|
-
import { parseSkillMd, validateSkillFrontmatter, DEFAULT_X402_MAX_INPUT_BYTES,
|
|
20
|
+
import { parseSkillMd, validateSkillFrontmatter, DEFAULT_X402_MAX_INPUT_BYTES, resolveInsidePathReal, DEFAULT_SCRIPT_TIMEOUT_MS, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
|
|
21
21
|
import { x402Client, wrapFetchWithPayment } from '@x402/fetch';
|
|
22
22
|
import { ExactSvmScheme } from '@x402/svm';
|
|
23
23
|
import { decodePaymentRequiredHeader } from '@x402/core/http';
|
|
@@ -52,9 +52,9 @@ function sleepWithSignal(ms, signal) {
|
|
|
52
52
|
return Promise.reject(createAbortError());
|
|
53
53
|
}
|
|
54
54
|
if (!signal) {
|
|
55
|
-
return new Promise((
|
|
55
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
56
56
|
}
|
|
57
|
-
return new Promise((
|
|
57
|
+
return new Promise((resolve4, reject) => {
|
|
58
58
|
const cleanup = () => {
|
|
59
59
|
clearTimeout(timer);
|
|
60
60
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -65,7 +65,7 @@ function sleepWithSignal(ms, signal) {
|
|
|
65
65
|
};
|
|
66
66
|
const timer = setTimeout(() => {
|
|
67
67
|
cleanup();
|
|
68
|
-
|
|
68
|
+
resolve4();
|
|
69
69
|
}, ms);
|
|
70
70
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
71
71
|
});
|
|
@@ -126,6 +126,16 @@ async function fetchWithTimeout(url, init, signal) {
|
|
|
126
126
|
headers: response.headers
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
|
+
function sanitizeErrorBody(body, maxLength = 500) {
|
|
130
|
+
let scrubbed = body;
|
|
131
|
+
for (const pattern of SECRET_BODY_PATTERNS) {
|
|
132
|
+
scrubbed = scrubbed.replace(
|
|
133
|
+
pattern,
|
|
134
|
+
(_match, prefix) => typeof prefix === "string" ? `${prefix}[REDACTED]` : "[REDACTED]"
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return scrubbed.slice(0, maxLength);
|
|
138
|
+
}
|
|
129
139
|
async function fetchWithRetry(url, init, signal) {
|
|
130
140
|
for (let attempt = 0; ; attempt++) {
|
|
131
141
|
let response;
|
|
@@ -148,13 +158,20 @@ async function fetchWithRetry(url, init, signal) {
|
|
|
148
158
|
await sleepWithSignal(delay, signal);
|
|
149
159
|
}
|
|
150
160
|
}
|
|
151
|
-
var DEFAULT_LLM_TIMEOUT_MS, LLM_TIMEOUT_MS, MAX_RETRIES, RETRYABLE_STATUSES;
|
|
161
|
+
var DEFAULT_LLM_TIMEOUT_MS, LLM_TIMEOUT_MS, MAX_RETRIES, RETRYABLE_STATUSES, SECRET_BODY_PATTERNS;
|
|
152
162
|
var init_http = __esm({
|
|
153
163
|
"src/llm/providers/http.ts"() {
|
|
154
164
|
DEFAULT_LLM_TIMEOUT_MS = 6e5;
|
|
155
165
|
LLM_TIMEOUT_MS = resolveLlmTimeoutMs();
|
|
156
166
|
MAX_RETRIES = 2;
|
|
157
167
|
RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
168
|
+
SECRET_BODY_PATTERNS = [
|
|
169
|
+
/sk-[A-Za-z0-9_-]{8,}/g,
|
|
170
|
+
/xai-[A-Za-z0-9_-]{8,}/g,
|
|
171
|
+
/AIza[A-Za-z0-9_-]{16,}/g,
|
|
172
|
+
/(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi,
|
|
173
|
+
/(x-api-key["':\s=]+)[A-Za-z0-9._~+/=-]{8,}/gi
|
|
174
|
+
];
|
|
158
175
|
}
|
|
159
176
|
});
|
|
160
177
|
|
|
@@ -193,7 +210,7 @@ async function verifyKey(apiKey, signal) {
|
|
|
193
210
|
await response.body?.cancel().catch(() => void 0);
|
|
194
211
|
return { ok: true };
|
|
195
212
|
}
|
|
196
|
-
const body = (await response.text().catch(() => ""))
|
|
213
|
+
const body = sanitizeErrorBody(await response.text().catch(() => ""));
|
|
197
214
|
if (response.status === 401 || response.status === 403) {
|
|
198
215
|
return { ok: false, reason: "invalid", status: response.status, body };
|
|
199
216
|
}
|
|
@@ -234,7 +251,7 @@ async function verifyKeyDeep(apiKey, model, signal) {
|
|
|
234
251
|
await response.body?.cancel().catch(() => void 0);
|
|
235
252
|
return { ok: true };
|
|
236
253
|
}
|
|
237
|
-
const body = (await response.text().catch(() => ""))
|
|
254
|
+
const body = sanitizeErrorBody(await response.text().catch(() => ""));
|
|
238
255
|
if (response.status === 401 || response.status === 403) {
|
|
239
256
|
return { ok: false, reason: "invalid", status: response.status, body };
|
|
240
257
|
}
|
|
@@ -280,7 +297,7 @@ var init_anthropic = __esm({
|
|
|
280
297
|
` [LLM] ${this.config.model} tokens: in=${inputTokens} out=${outputTokens} (total: in=${this.totalIn} out=${this.totalOut})`
|
|
281
298
|
);
|
|
282
299
|
}
|
|
283
|
-
async complete(systemPrompt, userInput, signal) {
|
|
300
|
+
async complete(systemPrompt, userInput, signal, history) {
|
|
284
301
|
const response = await fetchWithRetry(
|
|
285
302
|
"https://api.anthropic.com/v1/messages",
|
|
286
303
|
{
|
|
@@ -294,13 +311,15 @@ var init_anthropic = __esm({
|
|
|
294
311
|
model: this.config.model,
|
|
295
312
|
max_tokens: this.config.maxTokens,
|
|
296
313
|
system: systemPrompt,
|
|
297
|
-
messages: [{ role: "user", content: userInput }]
|
|
314
|
+
messages: [...history ?? [], { role: "user", content: userInput }]
|
|
298
315
|
})
|
|
299
316
|
},
|
|
300
317
|
signal
|
|
301
318
|
);
|
|
302
319
|
if (!response.ok) {
|
|
303
|
-
throw new Error(
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Anthropic API error: ${response.status} ${sanitizeErrorBody(await response.text(), 200)}`
|
|
322
|
+
);
|
|
304
323
|
}
|
|
305
324
|
const data = await response.json();
|
|
306
325
|
this.logTokens(data.usage);
|
|
@@ -342,7 +361,9 @@ var init_anthropic = __esm({
|
|
|
342
361
|
signal
|
|
343
362
|
);
|
|
344
363
|
if (!response.ok) {
|
|
345
|
-
throw new Error(
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Anthropic API error: ${response.status} ${sanitizeErrorBody(await response.text(), 200)}`
|
|
366
|
+
);
|
|
346
367
|
}
|
|
347
368
|
const data = await response.json();
|
|
348
369
|
this.logTokens(data.usage);
|
|
@@ -438,7 +459,7 @@ function createOpenAICompatibleProvider(config) {
|
|
|
438
459
|
await response.body?.cancel().catch(() => void 0);
|
|
439
460
|
return { ok: true };
|
|
440
461
|
}
|
|
441
|
-
const body = (await response.text().catch(() => ""))
|
|
462
|
+
const body = sanitizeErrorBody(await response.text().catch(() => ""));
|
|
442
463
|
if (response.status === 401 || response.status === 403) {
|
|
443
464
|
return { ok: false, reason: "invalid", status: response.status, body };
|
|
444
465
|
}
|
|
@@ -483,7 +504,7 @@ function createOpenAICompatibleProvider(config) {
|
|
|
483
504
|
await response.body?.cancel().catch(() => void 0);
|
|
484
505
|
return { ok: true };
|
|
485
506
|
}
|
|
486
|
-
const body = (await response.text().catch(() => ""))
|
|
507
|
+
const body = sanitizeErrorBody(await response.text().catch(() => ""));
|
|
487
508
|
if (response.status === 401 || response.status === 403) {
|
|
488
509
|
return { ok: false, reason: "invalid", status: response.status, body };
|
|
489
510
|
}
|
|
@@ -550,7 +571,7 @@ var init_openai_compatible = __esm({
|
|
|
550
571
|
` [LLM] ${this.config.model} tokens: in=${inputTokens} out=${outputTokens} (total: in=${this.totalIn} out=${this.totalOut})`
|
|
551
572
|
);
|
|
552
573
|
}
|
|
553
|
-
async complete(systemPrompt, userInput, signal) {
|
|
574
|
+
async complete(systemPrompt, userInput, signal, history) {
|
|
554
575
|
const response = await fetchWithRetry(
|
|
555
576
|
`${this.config.baseUrl}/chat/completions`,
|
|
556
577
|
{
|
|
@@ -564,6 +585,7 @@ var init_openai_compatible = __esm({
|
|
|
564
585
|
max_tokens: this.config.maxTokens,
|
|
565
586
|
messages: [
|
|
566
587
|
{ role: "system", content: systemPrompt },
|
|
588
|
+
...history ?? [],
|
|
567
589
|
{ role: "user", content: userInput }
|
|
568
590
|
]
|
|
569
591
|
})
|
|
@@ -572,7 +594,7 @@ var init_openai_compatible = __esm({
|
|
|
572
594
|
);
|
|
573
595
|
if (!response.ok) {
|
|
574
596
|
throw new Error(
|
|
575
|
-
`${this.config.providerLabel} API error: ${response.status} ${await response.text()}`
|
|
597
|
+
`${this.config.providerLabel} API error: ${response.status} ${sanitizeErrorBody(await response.text(), 200)}`
|
|
576
598
|
);
|
|
577
599
|
}
|
|
578
600
|
const data = await response.json();
|
|
@@ -616,7 +638,7 @@ var init_openai_compatible = __esm({
|
|
|
616
638
|
);
|
|
617
639
|
if (!response.ok) {
|
|
618
640
|
throw new Error(
|
|
619
|
-
`${this.config.providerLabel} API error: ${response.status} ${await response.text()}`
|
|
641
|
+
`${this.config.providerLabel} API error: ${response.status} ${sanitizeErrorBody(await response.text(), 200)}`
|
|
620
642
|
);
|
|
621
643
|
}
|
|
622
644
|
const data = await response.json();
|
|
@@ -730,7 +752,7 @@ async function verifyKey2(apiKey, signal) {
|
|
|
730
752
|
await response.body?.cancel().catch(() => void 0);
|
|
731
753
|
return { ok: true };
|
|
732
754
|
}
|
|
733
|
-
const body = (await response.text().catch(() => ""))
|
|
755
|
+
const body = sanitizeErrorBody(await response.text().catch(() => ""));
|
|
734
756
|
if (response.status === 401 || response.status === 403) {
|
|
735
757
|
return { ok: false, reason: "invalid", status: response.status, body };
|
|
736
758
|
}
|
|
@@ -781,7 +803,7 @@ async function verifyKeyDeep2(apiKey, model, signal) {
|
|
|
781
803
|
await response.body?.cancel().catch(() => void 0);
|
|
782
804
|
return { ok: true };
|
|
783
805
|
}
|
|
784
|
-
const body = (await response.text().catch(() => ""))
|
|
806
|
+
const body = sanitizeErrorBody(await response.text().catch(() => ""));
|
|
785
807
|
if (response.status === 401 || response.status === 403) {
|
|
786
808
|
return { ok: false, reason: "invalid", status: response.status, body };
|
|
787
809
|
}
|
|
@@ -833,7 +855,7 @@ var init_openai = __esm({
|
|
|
833
855
|
` [LLM] ${this.config.model} tokens: in=${inputTokens} out=${outputTokens} (total: in=${this.totalIn} out=${this.totalOut})`
|
|
834
856
|
);
|
|
835
857
|
}
|
|
836
|
-
async complete(systemPrompt, userInput, signal) {
|
|
858
|
+
async complete(systemPrompt, userInput, signal, history) {
|
|
837
859
|
const reasoning = this.isReasoningModel();
|
|
838
860
|
const response = await fetchWithRetry(
|
|
839
861
|
"https://api.openai.com/v1/chat/completions",
|
|
@@ -848,6 +870,7 @@ var init_openai = __esm({
|
|
|
848
870
|
...reasoning ? { max_completion_tokens: this.config.maxTokens } : { max_tokens: this.config.maxTokens },
|
|
849
871
|
messages: [
|
|
850
872
|
{ role: reasoning ? "developer" : "system", content: systemPrompt },
|
|
873
|
+
...history ?? [],
|
|
851
874
|
{ role: "user", content: userInput }
|
|
852
875
|
]
|
|
853
876
|
})
|
|
@@ -855,7 +878,9 @@ var init_openai = __esm({
|
|
|
855
878
|
signal
|
|
856
879
|
);
|
|
857
880
|
if (!response.ok) {
|
|
858
|
-
throw new Error(
|
|
881
|
+
throw new Error(
|
|
882
|
+
`OpenAI API error: ${response.status} ${sanitizeErrorBody(await response.text(), 200)}`
|
|
883
|
+
);
|
|
859
884
|
}
|
|
860
885
|
const data = await response.json();
|
|
861
886
|
this.logTokens(data.usage);
|
|
@@ -901,7 +926,9 @@ var init_openai = __esm({
|
|
|
901
926
|
signal
|
|
902
927
|
);
|
|
903
928
|
if (!response.ok) {
|
|
904
|
-
throw new Error(
|
|
929
|
+
throw new Error(
|
|
930
|
+
`OpenAI API error: ${response.status} ${sanitizeErrorBody(await response.text(), 200)}`
|
|
931
|
+
);
|
|
905
932
|
}
|
|
906
933
|
const data = await response.json();
|
|
907
934
|
this.logTokens(data.usage);
|
|
@@ -1415,6 +1442,538 @@ var init_init = __esm({
|
|
|
1415
1442
|
init_llm();
|
|
1416
1443
|
}
|
|
1417
1444
|
});
|
|
1445
|
+
var IDENTITY_PLATFORMS = ["github", "x", "website"];
|
|
1446
|
+
var X_PROOF_URL_HOSTS = /* @__PURE__ */ new Set([
|
|
1447
|
+
"x.com",
|
|
1448
|
+
"www.x.com",
|
|
1449
|
+
"mobile.x.com",
|
|
1450
|
+
"twitter.com",
|
|
1451
|
+
"www.twitter.com",
|
|
1452
|
+
"mobile.twitter.com"
|
|
1453
|
+
]);
|
|
1454
|
+
function githubProofText(npub) {
|
|
1455
|
+
return `Verifying that I control the following Nostr public key: ${npub}`;
|
|
1456
|
+
}
|
|
1457
|
+
function xProofText(npub) {
|
|
1458
|
+
return `Verifying my account on nostr My Public Key: "${npub}"`;
|
|
1459
|
+
}
|
|
1460
|
+
function websiteNostrJson(local, pubkeyHex) {
|
|
1461
|
+
return JSON.stringify({ names: { [local]: pubkeyHex } }, null, 2);
|
|
1462
|
+
}
|
|
1463
|
+
function parseIdentityPlatform(raw) {
|
|
1464
|
+
const lowered = raw.trim().toLowerCase();
|
|
1465
|
+
const match = IDENTITY_PLATFORMS.find((platform) => platform === lowered);
|
|
1466
|
+
if (match === void 0) {
|
|
1467
|
+
throw new Error(`Unknown platform "${raw}". Expected github, x, or website.`);
|
|
1468
|
+
}
|
|
1469
|
+
return match;
|
|
1470
|
+
}
|
|
1471
|
+
function parseGithubProofInput(raw) {
|
|
1472
|
+
const trimmed = raw.trim();
|
|
1473
|
+
const bareId = trimmed.toLowerCase();
|
|
1474
|
+
if (GIST_ID_REGEX.test(bareId)) {
|
|
1475
|
+
return { proofId: bareId };
|
|
1476
|
+
}
|
|
1477
|
+
let url;
|
|
1478
|
+
try {
|
|
1479
|
+
url = new URL(trimmed);
|
|
1480
|
+
} catch {
|
|
1481
|
+
return null;
|
|
1482
|
+
}
|
|
1483
|
+
if (url.protocol !== "https:" || url.hostname.toLowerCase() !== "gist.github.com") {
|
|
1484
|
+
return null;
|
|
1485
|
+
}
|
|
1486
|
+
const segments = url.pathname.split("/").filter((segment) => segment.length > 0);
|
|
1487
|
+
const [userSegment, gistSegment] = segments;
|
|
1488
|
+
if (userSegment === void 0 || gistSegment === void 0) {
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
const username = userSegment.toLowerCase();
|
|
1492
|
+
const gistId = gistSegment.toLowerCase();
|
|
1493
|
+
if (!GITHUB_USERNAME_REGEX.test(username) || !GIST_ID_REGEX.test(gistId)) {
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
return { proofId: gistId, username };
|
|
1497
|
+
}
|
|
1498
|
+
function parseXProofInput(raw) {
|
|
1499
|
+
const trimmed = raw.trim();
|
|
1500
|
+
if (TWEET_ID_REGEX.test(trimmed)) {
|
|
1501
|
+
return { proofId: trimmed };
|
|
1502
|
+
}
|
|
1503
|
+
let url;
|
|
1504
|
+
try {
|
|
1505
|
+
url = new URL(trimmed);
|
|
1506
|
+
} catch {
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
if (url.protocol !== "https:" || !X_PROOF_URL_HOSTS.has(url.hostname.toLowerCase())) {
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
const segments = url.pathname.split("/").filter((segment) => segment.length > 0);
|
|
1513
|
+
const [userSegment, statusLiteral, idSegment] = segments;
|
|
1514
|
+
if (userSegment === void 0 || statusLiteral !== "status" || idSegment === void 0) {
|
|
1515
|
+
return null;
|
|
1516
|
+
}
|
|
1517
|
+
const username = userSegment.toLowerCase();
|
|
1518
|
+
if (!X_USERNAME_REGEX.test(username) || !TWEET_ID_REGEX.test(idSegment)) {
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
return { proofId: idSegment, username };
|
|
1522
|
+
}
|
|
1523
|
+
function buildIdentityClaimInputs(identities) {
|
|
1524
|
+
const claims = [];
|
|
1525
|
+
if (identities?.github !== void 0) {
|
|
1526
|
+
claims.push({
|
|
1527
|
+
platform: "github",
|
|
1528
|
+
handle: identities.github.username,
|
|
1529
|
+
proofId: identities.github.gist
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
if (identities?.x !== void 0) {
|
|
1533
|
+
claims.push({ platform: "x", handle: identities.x.username, proofId: identities.x.tweet });
|
|
1534
|
+
}
|
|
1535
|
+
return claims;
|
|
1536
|
+
}
|
|
1537
|
+
function identitiesFromYaml(identities) {
|
|
1538
|
+
const claims = [];
|
|
1539
|
+
if (identities === void 0) {
|
|
1540
|
+
return claims;
|
|
1541
|
+
}
|
|
1542
|
+
if (identities.github !== void 0) {
|
|
1543
|
+
claims.push({
|
|
1544
|
+
platform: "github",
|
|
1545
|
+
handle: identities.github.username,
|
|
1546
|
+
proofUrl: `https://gist.github.com/${encodeURIComponent(identities.github.username)}/${encodeURIComponent(identities.github.gist)}`
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
if (identities.x !== void 0) {
|
|
1550
|
+
claims.push({
|
|
1551
|
+
platform: "x",
|
|
1552
|
+
handle: identities.x.username,
|
|
1553
|
+
proofUrl: `https://x.com/${encodeURIComponent(identities.x.username)}/status/${encodeURIComponent(identities.x.tweet)}`
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
if (identities.website !== void 0) {
|
|
1557
|
+
const identifier = normalizeNip05Identifier(identities.website);
|
|
1558
|
+
if (identifier !== null) {
|
|
1559
|
+
const { domain } = splitNip05Identifier(identifier);
|
|
1560
|
+
claims.push({ platform: "website", handle: identifier, proofUrl: `https://${domain}` });
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return claims;
|
|
1564
|
+
}
|
|
1565
|
+
async function createInquirerPrompter() {
|
|
1566
|
+
const { default: inquirer } = await import('inquirer');
|
|
1567
|
+
return {
|
|
1568
|
+
async input(message) {
|
|
1569
|
+
const { value } = await inquirer.prompt([{ type: "input", name: "value", message }]);
|
|
1570
|
+
return String(value);
|
|
1571
|
+
},
|
|
1572
|
+
async confirm(message, defaultValue) {
|
|
1573
|
+
const { value } = await inquirer.prompt([
|
|
1574
|
+
{ type: "confirm", name: "value", message, default: defaultValue }
|
|
1575
|
+
]);
|
|
1576
|
+
return Boolean(value);
|
|
1577
|
+
},
|
|
1578
|
+
async select(message, choices) {
|
|
1579
|
+
const { value } = await inquirer.prompt([{ type: "list", name: "value", message, choices }]);
|
|
1580
|
+
return String(value);
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
function createDefaultPublisher(relays) {
|
|
1585
|
+
const client = new ElisymClient({ relays });
|
|
1586
|
+
return {
|
|
1587
|
+
fetchExternalIdentityClaims: (pubkey) => client.discovery.fetchExternalIdentityClaims(pubkey),
|
|
1588
|
+
publishExternalIdentities: (identity2, claims) => client.discovery.publishExternalIdentities(identity2, claims),
|
|
1589
|
+
publishProfile: (identity2, name, about, picture, banner, nip05) => client.discovery.publishProfile(identity2, name, about, picture, banner, nip05),
|
|
1590
|
+
close: () => client.close()
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
async function resolveAndLoadAgent(nameArg, cwd, prompter) {
|
|
1594
|
+
let name = nameArg;
|
|
1595
|
+
if (!name) {
|
|
1596
|
+
const agents = await listAgents(cwd);
|
|
1597
|
+
if (agents.length === 0) {
|
|
1598
|
+
throw new Error("No agents found. Run `npx @elisym/cli init` first.");
|
|
1599
|
+
}
|
|
1600
|
+
name = await prompter.select(
|
|
1601
|
+
"Select agent:",
|
|
1602
|
+
agents.map((agent) => ({ name: `${agent.name} (${agent.source})`, value: agent.name }))
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
const passphrase = process.env.ELISYM_PASSPHRASE;
|
|
1606
|
+
const loaded = await loadAgent(name, cwd, passphrase);
|
|
1607
|
+
return { name, loaded };
|
|
1608
|
+
}
|
|
1609
|
+
function relaysFor(yaml) {
|
|
1610
|
+
return yaml.relays.length > 0 ? yaml.relays : [...RELAYS];
|
|
1611
|
+
}
|
|
1612
|
+
async function resolvePreservedMediaUrl(configured, agentDir, cache, publishedUrl) {
|
|
1613
|
+
if (!configured) {
|
|
1614
|
+
return void 0;
|
|
1615
|
+
}
|
|
1616
|
+
if (/^https?:\/\//i.test(configured)) {
|
|
1617
|
+
return configured;
|
|
1618
|
+
}
|
|
1619
|
+
const agentRoot = resolve(agentDir);
|
|
1620
|
+
const candidate = resolve(agentRoot, configured);
|
|
1621
|
+
const rel = relative(agentRoot, candidate);
|
|
1622
|
+
if (rel === "" || rel.startsWith("..") || rel.includes(`..${sep}`)) {
|
|
1623
|
+
return publishedUrl;
|
|
1624
|
+
}
|
|
1625
|
+
const cached = await lookupCachedUrl(cache, configured, candidate);
|
|
1626
|
+
return cached ?? publishedUrl;
|
|
1627
|
+
}
|
|
1628
|
+
async function republishAgentProfile(publisher, identity2, agentName, agentDir, yaml) {
|
|
1629
|
+
let publishedProfile;
|
|
1630
|
+
try {
|
|
1631
|
+
const published = await publisher.fetchExternalIdentityClaims(identity2.publicKey);
|
|
1632
|
+
publishedProfile = published.profile;
|
|
1633
|
+
} catch {
|
|
1634
|
+
}
|
|
1635
|
+
const mediaCache = await readMediaCache(agentDir);
|
|
1636
|
+
const picture = await resolvePreservedMediaUrl(
|
|
1637
|
+
yaml.picture,
|
|
1638
|
+
agentDir,
|
|
1639
|
+
mediaCache,
|
|
1640
|
+
publishedProfile?.picture
|
|
1641
|
+
);
|
|
1642
|
+
const banner = await resolvePreservedMediaUrl(
|
|
1643
|
+
yaml.banner,
|
|
1644
|
+
agentDir,
|
|
1645
|
+
mediaCache,
|
|
1646
|
+
publishedProfile?.banner
|
|
1647
|
+
);
|
|
1648
|
+
const unresolved = [];
|
|
1649
|
+
if (yaml.picture && picture === void 0) {
|
|
1650
|
+
unresolved.push("picture");
|
|
1651
|
+
}
|
|
1652
|
+
if (yaml.banner && banner === void 0) {
|
|
1653
|
+
unresolved.push("banner");
|
|
1654
|
+
}
|
|
1655
|
+
if (unresolved.length > 0) {
|
|
1656
|
+
throw new Error(
|
|
1657
|
+
`${unresolved.join(" and ")} carry-over unavailable (no media cache entry and no currently-published URL - the relay read may have failed), so republishing kind 0 would wipe it from the profile`
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
await publisher.publishProfile(
|
|
1661
|
+
identity2,
|
|
1662
|
+
yaml.display_name ?? agentName,
|
|
1663
|
+
yaml.description ?? "",
|
|
1664
|
+
picture,
|
|
1665
|
+
banner,
|
|
1666
|
+
yaml.identities?.website
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
async function sweepStaleIdentityClaims(discovery, identity2) {
|
|
1670
|
+
try {
|
|
1671
|
+
const published = await discovery.fetchExternalIdentityClaims(identity2.publicKey);
|
|
1672
|
+
const staleClaims = published.identities.filter((claim) => claim.platform !== "website");
|
|
1673
|
+
if (staleClaims.length === 0) {
|
|
1674
|
+
return false;
|
|
1675
|
+
}
|
|
1676
|
+
await discovery.publishExternalIdentities(identity2, []);
|
|
1677
|
+
return true;
|
|
1678
|
+
} catch {
|
|
1679
|
+
return false;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
async function promptGithubLink(loaded, npub, prompter) {
|
|
1683
|
+
console.log(
|
|
1684
|
+
"\n 1. Create a PUBLIC gist (https://gist.github.com/new) containing ONLY this\n single line (the proof statement - a standard NIP-39 text that any\n Nostr client can verify; do not paste these instructions):"
|
|
1685
|
+
);
|
|
1686
|
+
console.log(`
|
|
1687
|
+
${githubProofText(npub)}
|
|
1688
|
+
`);
|
|
1689
|
+
console.log(" 2. Paste the gist URL (or its id) below.\n");
|
|
1690
|
+
const rawProof = await prompter.input("Gist URL or id:");
|
|
1691
|
+
const parsed = parseGithubProofInput(rawProof);
|
|
1692
|
+
if (parsed === null) {
|
|
1693
|
+
throw new Error("Expected a gist URL (https://gist.github.com/<user>/<id>) or a hex gist id.");
|
|
1694
|
+
}
|
|
1695
|
+
let username = parsed.username;
|
|
1696
|
+
if (username === void 0) {
|
|
1697
|
+
const rawUsername = await prompter.input("GitHub username:");
|
|
1698
|
+
username = rawUsername.trim().toLowerCase();
|
|
1699
|
+
}
|
|
1700
|
+
if (!GITHUB_USERNAME_REGEX.test(username)) {
|
|
1701
|
+
throw new Error(`Invalid GitHub username: "${username}" (alphanumeric or hyphen, 1-39 chars).`);
|
|
1702
|
+
}
|
|
1703
|
+
return {
|
|
1704
|
+
nextIdentities: {
|
|
1705
|
+
...loaded.yaml.identities,
|
|
1706
|
+
github: { username, gist: parsed.proofId }
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
async function promptXLink(loaded, npub, prompter) {
|
|
1711
|
+
console.log(
|
|
1712
|
+
"\n 1. Post a tweet whose text is ONLY this line, including the double quotes\n (the proof statement - a standard NIP-39 text; the account you post\n from becomes the linked identity):"
|
|
1713
|
+
);
|
|
1714
|
+
console.log(`
|
|
1715
|
+
${xProofText(npub)}
|
|
1716
|
+
`);
|
|
1717
|
+
console.log(" 2. Paste the tweet URL (or its status id) below.\n");
|
|
1718
|
+
const rawProof = await prompter.input("Tweet URL or id:");
|
|
1719
|
+
const parsed = parseXProofInput(rawProof);
|
|
1720
|
+
if (parsed === null) {
|
|
1721
|
+
throw new Error(
|
|
1722
|
+
"Expected a tweet URL (https://x.com/<user>/status/<id>) or a numeric status id."
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
let username = parsed.username;
|
|
1726
|
+
if (username === void 0) {
|
|
1727
|
+
const rawUsername = await prompter.input("X username (without @):");
|
|
1728
|
+
username = rawUsername.trim().replace(/^@/, "").toLowerCase();
|
|
1729
|
+
}
|
|
1730
|
+
if (!X_USERNAME_REGEX.test(username)) {
|
|
1731
|
+
throw new Error(`Invalid X username: "${username}" (alphanumeric or underscore, 1-15 chars).`);
|
|
1732
|
+
}
|
|
1733
|
+
return {
|
|
1734
|
+
nextIdentities: {
|
|
1735
|
+
...loaded.yaml.identities,
|
|
1736
|
+
x: { username, tweet: parsed.proofId }
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
async function promptWebsiteLink(loaded, pubkeyHex, prompter) {
|
|
1741
|
+
const rawIdentifier = await prompter.input("NIP-05 identifier (name@domain or bare domain):");
|
|
1742
|
+
const identifier = normalizeNip05Identifier(rawIdentifier.trim());
|
|
1743
|
+
if (identifier === null) {
|
|
1744
|
+
throw new Error(
|
|
1745
|
+
`Invalid NIP-05 identifier: "${rawIdentifier.trim()}". Expected name@domain or a bare domain (ASCII hostname labels, no IP literals).`
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
const { local, domain } = splitNip05Identifier(identifier);
|
|
1749
|
+
console.log(`
|
|
1750
|
+
Serve this JSON at https://${domain}/.well-known/nostr.json :
|
|
1751
|
+
`);
|
|
1752
|
+
for (const line of websiteNostrJson(local, pubkeyHex).split("\n")) {
|
|
1753
|
+
console.log(` ${line}`);
|
|
1754
|
+
}
|
|
1755
|
+
console.log(
|
|
1756
|
+
`
|
|
1757
|
+
It must answer https://${domain}/.well-known/nostr.json?name=${local} over https,`
|
|
1758
|
+
);
|
|
1759
|
+
console.log(" without redirects - NIP-05 forbids following them. The verifier consults only");
|
|
1760
|
+
console.log(` this exact host (no www/apex twin fallback), so the file must live at ${domain}.`);
|
|
1761
|
+
console.log(" Sending `Access-Control-Allow-Origin: *` is recommended (NIP-05), so browser");
|
|
1762
|
+
console.log(" clients can verify it too.\n");
|
|
1763
|
+
const live = await prompter.confirm("Is the file deployed and reachable? Verify now", true);
|
|
1764
|
+
if (!live) {
|
|
1765
|
+
throw new Error(
|
|
1766
|
+
"aborted - deploy the file, then re-run `npx @elisym/cli identity link website`. Nothing was written."
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
return {
|
|
1770
|
+
nextIdentities: {
|
|
1771
|
+
...loaded.yaml.identities,
|
|
1772
|
+
website: identifier
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
function describeBrokenProof(platform, claim) {
|
|
1777
|
+
switch (platform) {
|
|
1778
|
+
case "github":
|
|
1779
|
+
return `the gist at ${claim.proofUrl} is missing, belongs to another user, or does not contain the exact proof text with this agent's npub`;
|
|
1780
|
+
case "x":
|
|
1781
|
+
return `the tweet at ${claim.proofUrl} is deleted, belongs to another account, or does not contain the exact proof text with this agent's npub`;
|
|
1782
|
+
case "website":
|
|
1783
|
+
return `${claim.proofUrl}/.well-known/nostr.json does not map "${claim.handle}" to this agent's pubkey`;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
async function cmdIdentityLink(platformArg, agentName, deps = {}) {
|
|
1787
|
+
const platform = parseIdentityPlatform(platformArg);
|
|
1788
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
1789
|
+
const prompter = deps.prompter ?? await createInquirerPrompter();
|
|
1790
|
+
const verify = deps.verify ?? verifyAgentIdentities;
|
|
1791
|
+
const { name, loaded } = await resolveAndLoadAgent(agentName, cwd, prompter);
|
|
1792
|
+
const identity2 = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
|
|
1793
|
+
console.log(`
|
|
1794
|
+
Linking ${platform} to agent "${name}" (${identity2.npub}).`);
|
|
1795
|
+
let prompted;
|
|
1796
|
+
if (platform === "github") {
|
|
1797
|
+
prompted = await promptGithubLink(loaded, identity2.npub, prompter);
|
|
1798
|
+
} else if (platform === "x") {
|
|
1799
|
+
prompted = await promptXLink(loaded, identity2.npub, prompter);
|
|
1800
|
+
} else {
|
|
1801
|
+
prompted = await promptWebsiteLink(loaded, identity2.publicKey, prompter);
|
|
1802
|
+
}
|
|
1803
|
+
const claim = identitiesFromYaml(prompted.nextIdentities).find(
|
|
1804
|
+
(candidate) => candidate.platform === platform
|
|
1805
|
+
);
|
|
1806
|
+
if (claim === void 0) {
|
|
1807
|
+
throw new Error("Internal: failed to build the identity claim from the prompt input.");
|
|
1808
|
+
}
|
|
1809
|
+
console.log("\n Verifying the proof (live, cache bypassed)...");
|
|
1810
|
+
const results = await verify(identity2.publicKey, [claim], { bypassCache: true });
|
|
1811
|
+
const result = results[0];
|
|
1812
|
+
if (result === void 0) {
|
|
1813
|
+
throw new Error("Internal: the verifier returned no result.");
|
|
1814
|
+
}
|
|
1815
|
+
if (result.status === "broken") {
|
|
1816
|
+
throw new Error(
|
|
1817
|
+
`proof check failed: ${describeBrokenProof(platform, claim)}. Nothing was written.`
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
if (result.status === "unverifiable") {
|
|
1821
|
+
console.warn(
|
|
1822
|
+
" ! Could not check the proof (network error, rate limit, or the proof is not reachable yet)."
|
|
1823
|
+
);
|
|
1824
|
+
console.warn(" ! This is neutral - the proof may be fine - but it was NOT verified.");
|
|
1825
|
+
const proceed = await prompter.confirm(
|
|
1826
|
+
"Link anyway (write elisym.yaml and publish the unverified claim)?",
|
|
1827
|
+
false
|
|
1828
|
+
);
|
|
1829
|
+
if (!proceed) {
|
|
1830
|
+
throw new Error("aborted - nothing was written.");
|
|
1831
|
+
}
|
|
1832
|
+
} else {
|
|
1833
|
+
console.log(" Proof verified.");
|
|
1834
|
+
}
|
|
1835
|
+
const nextYaml = { ...loaded.yaml, identities: prompted.nextIdentities };
|
|
1836
|
+
await writeYaml(loaded.dir, nextYaml);
|
|
1837
|
+
console.log(` Wrote identities.${platform} to elisym.yaml.`);
|
|
1838
|
+
const createPublisher = deps.createPublisher ?? createDefaultPublisher;
|
|
1839
|
+
const publisher = createPublisher(relaysFor(loaded.yaml));
|
|
1840
|
+
try {
|
|
1841
|
+
const claims = buildIdentityClaimInputs(prompted.nextIdentities);
|
|
1842
|
+
try {
|
|
1843
|
+
await publisher.publishExternalIdentities(identity2, claims);
|
|
1844
|
+
console.log(` Published identity claims (kind 10011, ${claims.length} tag(s)).`);
|
|
1845
|
+
if (platform === "website") {
|
|
1846
|
+
await republishAgentProfile(publisher, identity2, name, loaded.dir, nextYaml);
|
|
1847
|
+
console.log(" Republished profile (kind 0) with nip05.");
|
|
1848
|
+
}
|
|
1849
|
+
} catch (error) {
|
|
1850
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1851
|
+
throw new Error(
|
|
1852
|
+
`linked locally, but publishing failed: ${message}. Run \`npx @elisym/cli start ${name}\` to publish, or re-run this command.`
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
} finally {
|
|
1856
|
+
publisher.close();
|
|
1857
|
+
}
|
|
1858
|
+
console.log(`
|
|
1859
|
+
Linked ${platform} to agent "${name}".
|
|
1860
|
+
`);
|
|
1861
|
+
}
|
|
1862
|
+
function removeIdentityEntry(identities, platform) {
|
|
1863
|
+
const next = { ...identities };
|
|
1864
|
+
delete next[platform];
|
|
1865
|
+
const hasEntries = next.github !== void 0 || next.x !== void 0 || next.website !== void 0;
|
|
1866
|
+
return hasEntries ? next : void 0;
|
|
1867
|
+
}
|
|
1868
|
+
async function cmdIdentityUnlink(platformArg, agentName, deps = {}) {
|
|
1869
|
+
const platform = parseIdentityPlatform(platformArg);
|
|
1870
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
1871
|
+
const prompter = deps.prompter ?? await createInquirerPrompter();
|
|
1872
|
+
const { name, loaded } = await resolveAndLoadAgent(agentName, cwd, prompter);
|
|
1873
|
+
const identity2 = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
|
|
1874
|
+
const current = loaded.yaml.identities;
|
|
1875
|
+
if (current === void 0 || current[platform] === void 0) {
|
|
1876
|
+
console.log(`
|
|
1877
|
+
${platform} is not linked for agent "${name}" - nothing to unlink.
|
|
1878
|
+
`);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
const nextIdentities = removeIdentityEntry(current, platform);
|
|
1882
|
+
const nextYaml = { ...loaded.yaml };
|
|
1883
|
+
if (nextIdentities === void 0) {
|
|
1884
|
+
delete nextYaml.identities;
|
|
1885
|
+
} else {
|
|
1886
|
+
nextYaml.identities = nextIdentities;
|
|
1887
|
+
}
|
|
1888
|
+
await writeYaml(loaded.dir, nextYaml);
|
|
1889
|
+
console.log(`
|
|
1890
|
+
Removed identities.${platform} from elisym.yaml.`);
|
|
1891
|
+
const createPublisher = deps.createPublisher ?? createDefaultPublisher;
|
|
1892
|
+
const publisher = createPublisher(relaysFor(loaded.yaml));
|
|
1893
|
+
try {
|
|
1894
|
+
const remaining = buildIdentityClaimInputs(nextIdentities);
|
|
1895
|
+
try {
|
|
1896
|
+
await publisher.publishExternalIdentities(identity2, remaining);
|
|
1897
|
+
console.log(
|
|
1898
|
+
` Republished identity claims (kind 10011, ${remaining.length} tag(s) remaining).`
|
|
1899
|
+
);
|
|
1900
|
+
if (platform === "website") {
|
|
1901
|
+
await republishAgentProfile(publisher, identity2, name, loaded.dir, nextYaml);
|
|
1902
|
+
console.log(" Republished profile (kind 0) without nip05.");
|
|
1903
|
+
}
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1906
|
+
throw new Error(
|
|
1907
|
+
`unlinked locally, but publishing the retraction failed: ${message}. Run \`npx @elisym/cli start ${name}\` to propagate it.`
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
} finally {
|
|
1911
|
+
publisher.close();
|
|
1912
|
+
}
|
|
1913
|
+
console.log(`
|
|
1914
|
+
Unlinked ${platform} from agent "${name}".
|
|
1915
|
+
`);
|
|
1916
|
+
}
|
|
1917
|
+
function sameClaim(left, right) {
|
|
1918
|
+
return left.platform === right.platform && left.handle.toLowerCase() === right.handle.toLowerCase() && left.proofUrl.toLowerCase() === right.proofUrl.toLowerCase();
|
|
1919
|
+
}
|
|
1920
|
+
function buildStatusRows(results, publishedClaims) {
|
|
1921
|
+
return results.map((result) => ({
|
|
1922
|
+
platform: result.identity.platform,
|
|
1923
|
+
handle: result.identity.handle,
|
|
1924
|
+
proofUrl: result.identity.proofUrl,
|
|
1925
|
+
status: result.status,
|
|
1926
|
+
published: publishedClaims === void 0 ? void 0 : publishedClaims.some((claim) => sameClaim(claim, result.identity))
|
|
1927
|
+
}));
|
|
1928
|
+
}
|
|
1929
|
+
async function cmdIdentityStatus(agentName, deps = {}) {
|
|
1930
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
1931
|
+
const prompter = deps.prompter ?? await createInquirerPrompter();
|
|
1932
|
+
const verify = deps.verify ?? verifyAgentIdentities;
|
|
1933
|
+
const { name, loaded } = await resolveAndLoadAgent(agentName, cwd, prompter);
|
|
1934
|
+
const identity2 = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
|
|
1935
|
+
const localClaims = identitiesFromYaml(loaded.yaml.identities);
|
|
1936
|
+
const createPublisher = deps.createPublisher ?? createDefaultPublisher;
|
|
1937
|
+
const publisher = createPublisher(relaysFor(loaded.yaml));
|
|
1938
|
+
let published;
|
|
1939
|
+
try {
|
|
1940
|
+
published = await publisher.fetchExternalIdentityClaims(identity2.publicKey);
|
|
1941
|
+
} catch {
|
|
1942
|
+
console.warn(" ! Could not query the relays for published claims - drift check skipped.");
|
|
1943
|
+
} finally {
|
|
1944
|
+
publisher.close();
|
|
1945
|
+
}
|
|
1946
|
+
if (localClaims.length === 0) {
|
|
1947
|
+
console.log(`
|
|
1948
|
+
No identities linked in elisym.yaml for agent "${name}".`);
|
|
1949
|
+
console.log(" Link one: npx @elisym/cli identity link <github|x|website>\n");
|
|
1950
|
+
if (published !== void 0 && published.identities.length > 0) {
|
|
1951
|
+
console.log(
|
|
1952
|
+
` ! The relays still hold published identity claims - run \`npx @elisym/cli start ${name}\` to retract them.
|
|
1953
|
+
`
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
console.log(`
|
|
1959
|
+
Verifying ${localClaims.length} proof(s) (live, cache bypassed)...
|
|
1960
|
+
`);
|
|
1961
|
+
const results = await verify(identity2.publicKey, localClaims, { bypassCache: true });
|
|
1962
|
+
const rows = buildStatusRows(results, published?.identities);
|
|
1963
|
+
console.log(` Identities for agent "${name}" (${identity2.npub}):
|
|
1964
|
+
`);
|
|
1965
|
+
for (const row of rows) {
|
|
1966
|
+
const displayHandle = row.handle.startsWith("_@") ? row.handle.slice(2) : row.handle;
|
|
1967
|
+
console.log(` ${row.platform.padEnd(8)} ${row.status.padEnd(13)} ${displayHandle}`);
|
|
1968
|
+
console.log(` proof: ${row.proofUrl}`);
|
|
1969
|
+
if (row.published === false) {
|
|
1970
|
+
console.log(
|
|
1971
|
+
` ! linked locally but not published - run \`npx @elisym/cli start ${name}\` to publish`
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
console.log();
|
|
1976
|
+
}
|
|
1418
1977
|
|
|
1419
1978
|
// src/index.ts
|
|
1420
1979
|
init_init();
|
|
@@ -1669,7 +2228,7 @@ function parseRelayUrl(url) {
|
|
|
1669
2228
|
}
|
|
1670
2229
|
}
|
|
1671
2230
|
async function tcpProbe(host, port, timeoutMs) {
|
|
1672
|
-
return new Promise((
|
|
2231
|
+
return new Promise((resolve4) => {
|
|
1673
2232
|
const started = Date.now();
|
|
1674
2233
|
const socket = new Socket();
|
|
1675
2234
|
let settled = false;
|
|
@@ -1679,7 +2238,7 @@ async function tcpProbe(host, port, timeoutMs) {
|
|
|
1679
2238
|
}
|
|
1680
2239
|
settled = true;
|
|
1681
2240
|
socket.destroy();
|
|
1682
|
-
|
|
2241
|
+
resolve4(result);
|
|
1683
2242
|
};
|
|
1684
2243
|
const timer = setTimeout(() => finish({ error: "timeout" }), timeoutMs);
|
|
1685
2244
|
socket.once("connect", () => {
|
|
@@ -1996,80 +2555,809 @@ function resolveProviderApiKey(input) {
|
|
|
1996
2555
|
error: `Provider "${provider}" is not registered (required by skill(s): ${skillList2}).`
|
|
1997
2556
|
};
|
|
1998
2557
|
}
|
|
1999
|
-
const perProviderValue = secrets.llm_api_keys?.[provider];
|
|
2000
|
-
if (typeof perProviderValue === "string" && perProviderValue.length > 0) {
|
|
2001
|
-
return { apiKey: perProviderValue, origin: "secrets-per-provider" };
|
|
2558
|
+
const perProviderValue = secrets.llm_api_keys?.[provider];
|
|
2559
|
+
if (typeof perProviderValue === "string" && perProviderValue.length > 0) {
|
|
2560
|
+
return { apiKey: perProviderValue, origin: "secrets-per-provider" };
|
|
2561
|
+
}
|
|
2562
|
+
const envValue = process.env[descriptor.envVar];
|
|
2563
|
+
if (envValue) {
|
|
2564
|
+
return { apiKey: envValue, origin: "env" };
|
|
2565
|
+
}
|
|
2566
|
+
const skillList = dependentSkills.length > 0 ? dependentSkills.join(", ") : "<none>";
|
|
2567
|
+
return {
|
|
2568
|
+
error: `Provider "${provider}" needs an API key (required by skill(s): ${skillList}). Set secrets.llm_api_keys.${provider} via 'npx @elisym/cli profile <agent>' or export ${descriptor.envVar}.`
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
function sanitizeForTerminal(value) {
|
|
2572
|
+
return value.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
|
|
2573
|
+
}
|
|
2574
|
+
function resolveLevel(options) {
|
|
2575
|
+
if (options.level) {
|
|
2576
|
+
return options.level;
|
|
2577
|
+
}
|
|
2578
|
+
if (options.verbose) {
|
|
2579
|
+
return "debug";
|
|
2580
|
+
}
|
|
2581
|
+
const envLevel = process.env.LOG_LEVEL;
|
|
2582
|
+
if (envLevel) {
|
|
2583
|
+
return envLevel;
|
|
2584
|
+
}
|
|
2585
|
+
return "info";
|
|
2586
|
+
}
|
|
2587
|
+
function createLogger(options = {}) {
|
|
2588
|
+
const level = resolveLevel(options);
|
|
2589
|
+
const baseOptions = {
|
|
2590
|
+
name: "elisym-cli",
|
|
2591
|
+
level,
|
|
2592
|
+
redact: {
|
|
2593
|
+
paths: DEFAULT_REDACT_PATHS,
|
|
2594
|
+
censor: makeCensor()
|
|
2595
|
+
}
|
|
2596
|
+
};
|
|
2597
|
+
let logger;
|
|
2598
|
+
if (options.destination) {
|
|
2599
|
+
logger = pino(baseOptions, options.destination);
|
|
2600
|
+
} else if (options.tty === true) {
|
|
2601
|
+
logger = pino({
|
|
2602
|
+
...baseOptions,
|
|
2603
|
+
transport: {
|
|
2604
|
+
target: "pino-pretty",
|
|
2605
|
+
options: { destination: 2, colorize: true, translateTime: "HH:MM:ss" }
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
} else {
|
|
2609
|
+
logger = pino(baseOptions, pino.destination(2));
|
|
2610
|
+
}
|
|
2611
|
+
function logWithIndent(line) {
|
|
2612
|
+
process.stdout.write(` ${sanitizeForTerminal(line)}
|
|
2613
|
+
`);
|
|
2614
|
+
}
|
|
2615
|
+
return {
|
|
2616
|
+
logger,
|
|
2617
|
+
logWithIndent,
|
|
2618
|
+
bannerLog: logWithIndent
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
var MIME_BY_EXT = {
|
|
2622
|
+
".png": "image/png",
|
|
2623
|
+
".jpg": "image/jpeg",
|
|
2624
|
+
".jpeg": "image/jpeg",
|
|
2625
|
+
".gif": "image/gif",
|
|
2626
|
+
".webp": "image/webp",
|
|
2627
|
+
".svg": "image/svg+xml",
|
|
2628
|
+
".avif": "image/avif"
|
|
2629
|
+
};
|
|
2630
|
+
function mimeFromPath(path) {
|
|
2631
|
+
return MIME_BY_EXT[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
2632
|
+
}
|
|
2633
|
+
var SESSIONS_DIR_NAME = ".sessions";
|
|
2634
|
+
var SESSION_MAX_CONCURRENT_JOBS = 2;
|
|
2635
|
+
var SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2636
|
+
var SESSION_MAX_PER_CUSTOMER = 64;
|
|
2637
|
+
var SESSION_MAX_TURN_BYTES = 256 * 1024;
|
|
2638
|
+
var SESSION_MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
2639
|
+
var SESSION_GLOBAL_MAX_BYTES = 512 * 1024 * 1024;
|
|
2640
|
+
var SESSION_GLOBAL_EVICT_TO_BYTES = Math.floor(SESSION_GLOBAL_MAX_BYTES * 0.9);
|
|
2641
|
+
var SESSION_COMPACTION_TRIGGER_CHARS = 12e4;
|
|
2642
|
+
var SESSION_COMPACTION_KEEP_CHARS = Math.floor(SESSION_COMPACTION_TRIGGER_CHARS / 2);
|
|
2643
|
+
var SESSION_COMPACTION_MAX_STRIKES = 2;
|
|
2644
|
+
var SESSION_SUMMARY_PREFIX = "[Conversation summary]";
|
|
2645
|
+
var SESSION_CONTEXT_UNAVAILABLE = "[earlier context unavailable]";
|
|
2646
|
+
var TRUNCATION_MARKER = "\n[truncated]";
|
|
2647
|
+
var CUSTOMER_PUBKEY_REGEX = /^[0-9a-f]{64}$/;
|
|
2648
|
+
var DIR_MODE = 448;
|
|
2649
|
+
var FILE_MODE = 384;
|
|
2650
|
+
var SessionStore = class {
|
|
2651
|
+
root;
|
|
2652
|
+
log;
|
|
2653
|
+
ttlMs;
|
|
2654
|
+
maxPerCustomer;
|
|
2655
|
+
maxTurnBytes;
|
|
2656
|
+
maxFileBytes;
|
|
2657
|
+
globalMaxBytes;
|
|
2658
|
+
globalEvictToBytes;
|
|
2659
|
+
compactionTriggerChars;
|
|
2660
|
+
compactionKeepChars;
|
|
2661
|
+
/** Session-key -> release promise. Entry present = mutex held. */
|
|
2662
|
+
locks = /* @__PURE__ */ new Map();
|
|
2663
|
+
/** Session-key -> admitted-job counts (live intake + jobId-keyed recovery). */
|
|
2664
|
+
admitted = /* @__PURE__ */ new Map();
|
|
2665
|
+
/** Recovery registration index: jobId -> session key (for the release sweep). */
|
|
2666
|
+
recoveryByJob = /* @__PURE__ */ new Map();
|
|
2667
|
+
/** Session-key -> consecutive compaction failures (in-process, reset on restart). */
|
|
2668
|
+
compactionStrikes = /* @__PURE__ */ new Map();
|
|
2669
|
+
/** Running total of session-file bytes (established by `init()`'s scan). */
|
|
2670
|
+
globalBytes = 0;
|
|
2671
|
+
constructor(agentDir, logger, tuning) {
|
|
2672
|
+
this.root = join(agentDir, SESSIONS_DIR_NAME);
|
|
2673
|
+
this.log = logger ?? (() => {
|
|
2674
|
+
});
|
|
2675
|
+
this.ttlMs = tuning?.ttlMs ?? SESSION_TTL_MS;
|
|
2676
|
+
this.maxPerCustomer = tuning?.maxPerCustomer ?? SESSION_MAX_PER_CUSTOMER;
|
|
2677
|
+
this.maxTurnBytes = tuning?.maxTurnBytes ?? SESSION_MAX_TURN_BYTES;
|
|
2678
|
+
this.maxFileBytes = tuning?.maxFileBytes ?? SESSION_MAX_FILE_BYTES;
|
|
2679
|
+
this.globalMaxBytes = tuning?.globalMaxBytes ?? SESSION_GLOBAL_MAX_BYTES;
|
|
2680
|
+
this.globalEvictToBytes = tuning?.globalEvictToBytes ?? (tuning?.globalMaxBytes !== void 0 ? Math.floor(tuning.globalMaxBytes * 0.9) : SESSION_GLOBAL_EVICT_TO_BYTES);
|
|
2681
|
+
this.compactionTriggerChars = tuning?.compactionTriggerChars ?? SESSION_COMPACTION_TRIGGER_CHARS;
|
|
2682
|
+
this.compactionKeepChars = tuning?.compactionKeepChars ?? (tuning?.compactionTriggerChars !== void 0 ? Math.floor(tuning.compactionTriggerChars / 2) : SESSION_COMPACTION_KEEP_CHARS);
|
|
2683
|
+
}
|
|
2684
|
+
/**
|
|
2685
|
+
* Startup: establish the global byte counter (excluding `.corrupt.*`
|
|
2686
|
+
* sidecars) and run a GC sweep. Call BEFORE `recoverPendingJobs` (mirrors
|
|
2687
|
+
* the ledger prune-before-recovery ordering).
|
|
2688
|
+
*/
|
|
2689
|
+
init() {
|
|
2690
|
+
this.globalBytes = 0;
|
|
2691
|
+
for (const file of this.listSessionFiles()) {
|
|
2692
|
+
this.globalBytes += file.size;
|
|
2693
|
+
}
|
|
2694
|
+
this.gc();
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* TTL sweep: delete sessions idle past the TTL and `.corrupt.*` sidecars
|
|
2698
|
+
* older than the TTL, then rmdir emptied customer dirs. A victim is deleted
|
|
2699
|
+
* only while holding its try-acquired mutex with a zero admitted count -
|
|
2700
|
+
* check and unlink are synchronous, so nothing can interleave.
|
|
2701
|
+
*/
|
|
2702
|
+
gc() {
|
|
2703
|
+
const cutoff = Date.now() - this.ttlMs;
|
|
2704
|
+
for (const file of this.listSessionFiles()) {
|
|
2705
|
+
if (file.mtimeMs >= cutoff) {
|
|
2706
|
+
continue;
|
|
2707
|
+
}
|
|
2708
|
+
this.deleteIfIdle(file.customerId, file.sessionId);
|
|
2709
|
+
}
|
|
2710
|
+
this.pruneSidecars(cutoff);
|
|
2711
|
+
this.pruneEmptyCustomerDirs();
|
|
2712
|
+
}
|
|
2713
|
+
// ── Mutex ──────────────────────────────────────────────────────────────
|
|
2714
|
+
/**
|
|
2715
|
+
* Acquire the session mutex. Lock order invariant (runtime-enforced): the
|
|
2716
|
+
* caller already holds its `pLimit` slot - a mutex holder never waits for a
|
|
2717
|
+
* slot, so no lock-order inversion is possible.
|
|
2718
|
+
*/
|
|
2719
|
+
async acquire(customerId, sessionId) {
|
|
2720
|
+
const key = sessionKey(customerId, sessionId);
|
|
2721
|
+
for (; ; ) {
|
|
2722
|
+
const current = this.locks.get(key);
|
|
2723
|
+
if (current === void 0) {
|
|
2724
|
+
return this.installLock(key);
|
|
2725
|
+
}
|
|
2726
|
+
await current.promise;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
/** Non-blocking acquire for GC/eviction. Returns a release fn or null. */
|
|
2730
|
+
tryAcquireSync(key) {
|
|
2731
|
+
if (this.locks.has(key)) {
|
|
2732
|
+
return null;
|
|
2733
|
+
}
|
|
2734
|
+
return this.installLock(key);
|
|
2735
|
+
}
|
|
2736
|
+
installLock(key) {
|
|
2737
|
+
let release;
|
|
2738
|
+
const promise = new Promise((resolve4) => {
|
|
2739
|
+
release = resolve4;
|
|
2740
|
+
});
|
|
2741
|
+
const entry = { promise, release };
|
|
2742
|
+
this.locks.set(key, entry);
|
|
2743
|
+
return () => {
|
|
2744
|
+
if (this.locks.get(key) === entry) {
|
|
2745
|
+
this.locks.delete(key);
|
|
2746
|
+
}
|
|
2747
|
+
entry.release();
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
// ── Admitted-job counter ───────────────────────────────────────────────
|
|
2751
|
+
admittedCount(customerId, sessionId) {
|
|
2752
|
+
const entry = this.admitted.get(sessionKey(customerId, sessionId));
|
|
2753
|
+
return entry === void 0 ? 0 : entry.live + entry.recovery.size;
|
|
2754
|
+
}
|
|
2755
|
+
/** Live-intake registration. Pair with `releaseLive` in the job's finally. */
|
|
2756
|
+
admitLive(customerId, sessionId) {
|
|
2757
|
+
const entry = this.admittedEntry(sessionKey(customerId, sessionId));
|
|
2758
|
+
entry.live++;
|
|
2759
|
+
}
|
|
2760
|
+
releaseLive(customerId, sessionId) {
|
|
2761
|
+
const key = sessionKey(customerId, sessionId);
|
|
2762
|
+
const entry = this.admitted.get(key);
|
|
2763
|
+
if (entry === void 0) {
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
entry.live = Math.max(0, entry.live - 1);
|
|
2767
|
+
this.dropAdmittedIfEmpty(key, entry);
|
|
2768
|
+
}
|
|
2769
|
+
/**
|
|
2770
|
+
* Recovery pre-pass: register the sessions of pending `paid` entries (jobId-
|
|
2771
|
+
* keyed, idempotent across ticks) and release registrations whose ledger
|
|
2772
|
+
* entry has left `paid` (status predicate, re-checked every tick). Runs at
|
|
2773
|
+
* the top of every recovery tick, BEFORE its empty-pending early return -
|
|
2774
|
+
* releases are needed precisely when entries have left the pending set.
|
|
2775
|
+
*/
|
|
2776
|
+
syncRecoveryRegistrations(refs, isStillPaid) {
|
|
2777
|
+
for (const ref of refs) {
|
|
2778
|
+
if (this.recoveryByJob.has(ref.jobId)) {
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
const key = sessionKey(ref.customerId, ref.sessionId);
|
|
2782
|
+
this.recoveryByJob.set(ref.jobId, key);
|
|
2783
|
+
this.admittedEntry(key).recovery.add(ref.jobId);
|
|
2784
|
+
}
|
|
2785
|
+
for (const [jobId, key] of this.recoveryByJob) {
|
|
2786
|
+
if (isStillPaid(jobId)) {
|
|
2787
|
+
continue;
|
|
2788
|
+
}
|
|
2789
|
+
this.recoveryByJob.delete(jobId);
|
|
2790
|
+
const entry = this.admitted.get(key);
|
|
2791
|
+
if (entry !== void 0) {
|
|
2792
|
+
entry.recovery.delete(jobId);
|
|
2793
|
+
this.dropAdmittedIfEmpty(key, entry);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
// ── Open / append / compaction (call with the session mutex held) ──────
|
|
2798
|
+
/**
|
|
2799
|
+
* Load the session for a job. Applies the load-time TTL check (expired file
|
|
2800
|
+
* is deleted BEFORE recording - expiry is a hard reset, never a resurrection),
|
|
2801
|
+
* the global byte cap (at-cap global LRU eviction, excluding this session),
|
|
2802
|
+
* and the per-customer LRU cap. `excludeJobId` (crash recovery) removes the
|
|
2803
|
+
* recovering job's own turns from the replayed history and reports which of
|
|
2804
|
+
* its roles are already recorded.
|
|
2805
|
+
*/
|
|
2806
|
+
open(customerId, sessionId, excludeJobId) {
|
|
2807
|
+
validateKeys(customerId, sessionId);
|
|
2808
|
+
const path = this.sessionPath(customerId, sessionId);
|
|
2809
|
+
const stateless = {
|
|
2810
|
+
stateless: true,
|
|
2811
|
+
known: false,
|
|
2812
|
+
messages: [],
|
|
2813
|
+
compactionNeeded: false,
|
|
2814
|
+
recordedRoles: /* @__PURE__ */ new Set()
|
|
2815
|
+
};
|
|
2816
|
+
let exists = existsSync(path);
|
|
2817
|
+
if (exists) {
|
|
2818
|
+
const stat2 = statSync(path);
|
|
2819
|
+
if (Date.now() - stat2.mtimeMs > this.ttlMs) {
|
|
2820
|
+
this.deleteSessionFile(customerId, sessionId);
|
|
2821
|
+
exists = false;
|
|
2822
|
+
this.log(`[sessions] ${sessionId.slice(0, 8)} expired; starting fresh`);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
if (this.globalBytes >= this.globalMaxBytes) {
|
|
2826
|
+
this.evictGlobal(sessionKey(customerId, sessionId));
|
|
2827
|
+
if (this.globalBytes >= this.globalMaxBytes) {
|
|
2828
|
+
this.log(
|
|
2829
|
+
"[sessions] global byte cap reached and no evictable victim; processing stateless"
|
|
2830
|
+
);
|
|
2831
|
+
return stateless;
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
if (!exists && !this.ensureCustomerCapacity(customerId, sessionId)) {
|
|
2835
|
+
this.log(
|
|
2836
|
+
`[sessions] customer session cap reached with no evictable victim; processing stateless`
|
|
2837
|
+
);
|
|
2838
|
+
return stateless;
|
|
2839
|
+
}
|
|
2840
|
+
if (!exists) {
|
|
2841
|
+
return {
|
|
2842
|
+
stateless: false,
|
|
2843
|
+
known: false,
|
|
2844
|
+
messages: [],
|
|
2845
|
+
compactionNeeded: false,
|
|
2846
|
+
recordedRoles: /* @__PURE__ */ new Set()
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2849
|
+
const lines = this.readSessionLines(customerId, sessionId);
|
|
2850
|
+
if (lines === null) {
|
|
2851
|
+
return {
|
|
2852
|
+
stateless: false,
|
|
2853
|
+
known: false,
|
|
2854
|
+
messages: [],
|
|
2855
|
+
compactionNeeded: false,
|
|
2856
|
+
recordedRoles: /* @__PURE__ */ new Set()
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
const recordedRoles = /* @__PURE__ */ new Set();
|
|
2860
|
+
if (excludeJobId !== void 0) {
|
|
2861
|
+
for (const line of lines) {
|
|
2862
|
+
if (line.type === "turn" && line.jobId === excludeJobId) {
|
|
2863
|
+
recordedRoles.add(line.role);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
const messages = assembleReplay(lines, excludeJobId);
|
|
2868
|
+
const replayedChars = messages.reduce((sum, message) => sum + message.content.length, 0);
|
|
2869
|
+
const compactionNeeded = replayedChars > this.compactionTriggerChars;
|
|
2870
|
+
return {
|
|
2871
|
+
stateless: false,
|
|
2872
|
+
known: true,
|
|
2873
|
+
messages,
|
|
2874
|
+
compactionNeeded,
|
|
2875
|
+
summarizeText: compactionNeeded ? buildSummarizeText(lines, this.keptTailStart(lines)) : void 0,
|
|
2876
|
+
recordedRoles
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
/**
|
|
2880
|
+
* Record a completed exchange. Called after execution success, BEFORE the
|
|
2881
|
+
* ledger's `markExecuted` flush and before result seeding (§2 append timing).
|
|
2882
|
+
* Both turns go in one write; dedupe is `(jobId, role)` via `skipRoles`.
|
|
2883
|
+
*/
|
|
2884
|
+
appendExchange(customerId, sessionId, exchange) {
|
|
2885
|
+
validateKeys(customerId, sessionId);
|
|
2886
|
+
const ts = Math.floor(Date.now() / 1e3);
|
|
2887
|
+
const lines = [];
|
|
2888
|
+
if (!exchange.skipRoles?.has("user")) {
|
|
2889
|
+
lines.push({
|
|
2890
|
+
type: "turn",
|
|
2891
|
+
role: "user",
|
|
2892
|
+
content: truncateTurn(exchange.userContent, this.maxTurnBytes),
|
|
2893
|
+
jobId: exchange.jobId,
|
|
2894
|
+
capability: exchange.capability,
|
|
2895
|
+
ts
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
if (!exchange.skipRoles?.has("assistant")) {
|
|
2899
|
+
lines.push({
|
|
2900
|
+
type: "turn",
|
|
2901
|
+
role: "assistant",
|
|
2902
|
+
content: truncateTurn(exchange.assistantContent, this.maxTurnBytes),
|
|
2903
|
+
jobId: exchange.jobId,
|
|
2904
|
+
ts
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
if (lines.length === 0) {
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
const payload = lines.map((line) => JSON.stringify(line)).join("\n") + "\n";
|
|
2911
|
+
const path = this.sessionPath(customerId, sessionId);
|
|
2912
|
+
try {
|
|
2913
|
+
appendFileSync(path, payload, { mode: FILE_MODE });
|
|
2914
|
+
} catch (error) {
|
|
2915
|
+
if (error.code !== "ENOENT") {
|
|
2916
|
+
throw error;
|
|
2917
|
+
}
|
|
2918
|
+
mkdirSync(join(this.root, customerId), { recursive: true, mode: DIR_MODE });
|
|
2919
|
+
appendFileSync(path, payload, { mode: FILE_MODE });
|
|
2920
|
+
}
|
|
2921
|
+
this.globalBytes += Buffer.byteLength(payload);
|
|
2922
|
+
}
|
|
2923
|
+
/**
|
|
2924
|
+
* Compaction rewrite: `[summary line, ...kept tail]`, atomic (temp + rename).
|
|
2925
|
+
* Returns the fresh replayed history. Throws on an empty summary - the
|
|
2926
|
+
* caller must treat that as a compaction failure, not rewrite on it (an
|
|
2927
|
+
* empty summary line is skipped at replay, silently vaporizing everything
|
|
2928
|
+
* it summarized).
|
|
2929
|
+
*/
|
|
2930
|
+
compact(customerId, sessionId, summary) {
|
|
2931
|
+
if (summary.trim().length === 0) {
|
|
2932
|
+
throw new Error("empty summary");
|
|
2933
|
+
}
|
|
2934
|
+
const rewritten = this.rewriteWithSummary(customerId, sessionId, summary);
|
|
2935
|
+
this.compactionStrikes.delete(sessionKey(customerId, sessionId));
|
|
2936
|
+
return rewritten;
|
|
2937
|
+
}
|
|
2938
|
+
/**
|
|
2939
|
+
* Record a compaction failure (summarize error or empty summary). After
|
|
2940
|
+
* `SESSION_COMPACTION_MAX_STRIKES` consecutive failures, force-truncate
|
|
2941
|
+
* without a summarize call - same canonical `[summary, ...kept tail]` shape
|
|
2942
|
+
* with the fixed `[earlier context unavailable]` content - so a small-window
|
|
2943
|
+
* model can never permanently brick a session into paid-job failures.
|
|
2944
|
+
*/
|
|
2945
|
+
compactionFailed(customerId, sessionId) {
|
|
2946
|
+
const key = sessionKey(customerId, sessionId);
|
|
2947
|
+
const strikes = (this.compactionStrikes.get(key) ?? 0) + 1;
|
|
2948
|
+
if (strikes < SESSION_COMPACTION_MAX_STRIKES) {
|
|
2949
|
+
this.compactionStrikes.set(key, strikes);
|
|
2950
|
+
return { forced: false };
|
|
2951
|
+
}
|
|
2952
|
+
this.compactionStrikes.delete(key);
|
|
2953
|
+
const messages = this.rewriteWithSummary(customerId, sessionId, SESSION_CONTEXT_UNAVAILABLE);
|
|
2954
|
+
this.log(
|
|
2955
|
+
`[sessions] ${sessionId.slice(0, 8)} force-truncated after repeated compaction failures`
|
|
2956
|
+
);
|
|
2957
|
+
return { forced: true, messages };
|
|
2958
|
+
}
|
|
2959
|
+
// ── Internals ──────────────────────────────────────────────────────────
|
|
2960
|
+
admittedEntry(key) {
|
|
2961
|
+
let entry = this.admitted.get(key);
|
|
2962
|
+
if (entry === void 0) {
|
|
2963
|
+
entry = { live: 0, recovery: /* @__PURE__ */ new Set() };
|
|
2964
|
+
this.admitted.set(key, entry);
|
|
2965
|
+
}
|
|
2966
|
+
return entry;
|
|
2967
|
+
}
|
|
2968
|
+
dropAdmittedIfEmpty(key, entry) {
|
|
2969
|
+
if (entry.live === 0 && entry.recovery.size === 0) {
|
|
2970
|
+
this.admitted.delete(key);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
sessionPath(customerId, sessionId) {
|
|
2974
|
+
return join(this.root, customerId, `${sessionId}.jsonl`);
|
|
2975
|
+
}
|
|
2976
|
+
/**
|
|
2977
|
+
* Parse the transcript. A torn LAST line (crash mid-append) is dropped AND
|
|
2978
|
+
* repaired away: the file is atomically rewritten without it, because a
|
|
2979
|
+
* later `appendFileSync` would otherwise glue its first line onto the torn
|
|
2980
|
+
* fragment (the fragment has no trailing newline), corrupting a mid-file
|
|
2981
|
+
* line. Any other malformed line - or a file over the backstop - is treated
|
|
2982
|
+
* as corruption: the file is renamed to a `.corrupt.<ts>` sidecar (outside
|
|
2983
|
+
* the byte counter, pruned by the TTL sweep) and the session starts fresh.
|
|
2984
|
+
* Callers hold the session mutex, so the repair cannot race an append.
|
|
2985
|
+
*/
|
|
2986
|
+
readSessionLines(customerId, sessionId) {
|
|
2987
|
+
const path = this.sessionPath(customerId, sessionId);
|
|
2988
|
+
let raw;
|
|
2989
|
+
let size;
|
|
2990
|
+
try {
|
|
2991
|
+
size = statSync(path).size;
|
|
2992
|
+
raw = readFileSync(path, "utf-8");
|
|
2993
|
+
} catch {
|
|
2994
|
+
return null;
|
|
2995
|
+
}
|
|
2996
|
+
if (size > this.maxFileBytes) {
|
|
2997
|
+
this.quarantine(customerId, sessionId, size);
|
|
2998
|
+
return null;
|
|
2999
|
+
}
|
|
3000
|
+
const rawLines = raw.split("\n");
|
|
3001
|
+
const lines = [];
|
|
3002
|
+
let torn = false;
|
|
3003
|
+
for (let index = 0; index < rawLines.length; index++) {
|
|
3004
|
+
const text = rawLines[index].trim();
|
|
3005
|
+
if (text.length === 0) {
|
|
3006
|
+
continue;
|
|
3007
|
+
}
|
|
3008
|
+
const parsed = parseSessionLine(text);
|
|
3009
|
+
if (parsed === null) {
|
|
3010
|
+
const isLastNonEmpty = rawLines.slice(index + 1).every((rest) => rest.trim().length === 0);
|
|
3011
|
+
if (isLastNonEmpty) {
|
|
3012
|
+
torn = true;
|
|
3013
|
+
break;
|
|
3014
|
+
}
|
|
3015
|
+
this.quarantine(customerId, sessionId, size);
|
|
3016
|
+
return null;
|
|
3017
|
+
}
|
|
3018
|
+
lines.push(parsed);
|
|
3019
|
+
}
|
|
3020
|
+
if (torn) {
|
|
3021
|
+
const payload = lines.map((line) => JSON.stringify(line)).join("\n") + (lines.length > 0 ? "\n" : "");
|
|
3022
|
+
const tempPath = `${path}.tmp`;
|
|
3023
|
+
writeFileSync(tempPath, payload, { mode: FILE_MODE });
|
|
3024
|
+
renameSync(tempPath, path);
|
|
3025
|
+
this.globalBytes = Math.max(0, this.globalBytes - size + Buffer.byteLength(payload));
|
|
3026
|
+
}
|
|
3027
|
+
return lines;
|
|
3028
|
+
}
|
|
3029
|
+
quarantine(customerId, sessionId, size) {
|
|
3030
|
+
const path = this.sessionPath(customerId, sessionId);
|
|
3031
|
+
try {
|
|
3032
|
+
renameSync(path, `${path}.corrupt.${Date.now()}`);
|
|
3033
|
+
this.globalBytes = Math.max(0, this.globalBytes - size);
|
|
3034
|
+
this.compactionStrikes.delete(sessionKey(customerId, sessionId));
|
|
3035
|
+
this.log(`[sessions] ${sessionId.slice(0, 8)} corrupt; quarantined and reset`);
|
|
3036
|
+
} catch {
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
/**
|
|
3040
|
+
* Kept-tail start index: the longest suffix of TURN lines whose combined
|
|
3041
|
+
* stored size stays within the keep budget, floored at the most recent turn.
|
|
3042
|
+
*/
|
|
3043
|
+
keptTailStart(lines) {
|
|
3044
|
+
let budget = this.compactionKeepChars;
|
|
3045
|
+
let start = lines.length;
|
|
3046
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
3047
|
+
const line = lines[index];
|
|
3048
|
+
if (line.type !== "turn") {
|
|
3049
|
+
break;
|
|
3050
|
+
}
|
|
3051
|
+
const cost = line.content.length;
|
|
3052
|
+
if (cost > budget && start < lines.length) {
|
|
3053
|
+
break;
|
|
3054
|
+
}
|
|
3055
|
+
budget -= cost;
|
|
3056
|
+
start = index;
|
|
3057
|
+
if (budget <= 0) {
|
|
3058
|
+
break;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
if (start === lines.length && lines.length > 0) {
|
|
3062
|
+
start = lines.length - 1;
|
|
3063
|
+
}
|
|
3064
|
+
return start;
|
|
3065
|
+
}
|
|
3066
|
+
rewriteWithSummary(customerId, sessionId, summary) {
|
|
3067
|
+
validateKeys(customerId, sessionId);
|
|
3068
|
+
const lines = this.readSessionLines(customerId, sessionId) ?? [];
|
|
3069
|
+
const tailStart = this.keptTailStart(lines);
|
|
3070
|
+
const tail = lines.slice(tailStart).flatMap((line) => {
|
|
3071
|
+
if (line.type !== "turn") {
|
|
3072
|
+
return [];
|
|
3073
|
+
}
|
|
3074
|
+
if (line.content.length > this.compactionKeepChars) {
|
|
3075
|
+
return [
|
|
3076
|
+
{
|
|
3077
|
+
...line,
|
|
3078
|
+
content: line.content.slice(0, this.compactionKeepChars) + TRUNCATION_MARKER
|
|
3079
|
+
}
|
|
3080
|
+
];
|
|
3081
|
+
}
|
|
3082
|
+
return [line];
|
|
3083
|
+
});
|
|
3084
|
+
const summaryLine = {
|
|
3085
|
+
type: "summary",
|
|
3086
|
+
content: summary,
|
|
3087
|
+
ts: Math.floor(Date.now() / 1e3)
|
|
3088
|
+
};
|
|
3089
|
+
const rewritten = [summaryLine, ...tail];
|
|
3090
|
+
const payload = rewritten.map((line) => JSON.stringify(line)).join("\n") + "\n";
|
|
3091
|
+
const path = this.sessionPath(customerId, sessionId);
|
|
3092
|
+
const oldSize = existsSync(path) ? statSync(path).size : 0;
|
|
3093
|
+
const tempPath = `${path}.tmp`;
|
|
3094
|
+
mkdirSync(join(this.root, customerId), { recursive: true, mode: DIR_MODE });
|
|
3095
|
+
writeFileSync(tempPath, payload, { mode: FILE_MODE });
|
|
3096
|
+
renameSync(tempPath, path);
|
|
3097
|
+
this.globalBytes = Math.max(0, this.globalBytes - oldSize + Buffer.byteLength(payload));
|
|
3098
|
+
return assembleReplay(rewritten, void 0);
|
|
3099
|
+
}
|
|
3100
|
+
/** New-session admission under the per-customer cap: LRU-evict an idle victim. */
|
|
3101
|
+
ensureCustomerCapacity(customerId, sessionId) {
|
|
3102
|
+
const dir = join(this.root, customerId);
|
|
3103
|
+
let entries;
|
|
3104
|
+
try {
|
|
3105
|
+
entries = readdirSync(dir).filter((name) => name.endsWith(".jsonl"));
|
|
3106
|
+
} catch {
|
|
3107
|
+
return true;
|
|
3108
|
+
}
|
|
3109
|
+
if (entries.length < this.maxPerCustomer) {
|
|
3110
|
+
return true;
|
|
3111
|
+
}
|
|
3112
|
+
const candidates = entries.map((name) => {
|
|
3113
|
+
const candidateSessionId = name.slice(0, -".jsonl".length);
|
|
3114
|
+
const path = join(dir, name);
|
|
3115
|
+
try {
|
|
3116
|
+
return { sessionId: candidateSessionId, mtimeMs: statSync(path).mtimeMs };
|
|
3117
|
+
} catch {
|
|
3118
|
+
return null;
|
|
3119
|
+
}
|
|
3120
|
+
}).filter(
|
|
3121
|
+
(candidate) => candidate !== null
|
|
3122
|
+
).sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
3123
|
+
for (const candidate of candidates) {
|
|
3124
|
+
if (candidate.sessionId === sessionId) {
|
|
3125
|
+
continue;
|
|
3126
|
+
}
|
|
3127
|
+
if (this.deleteIfIdle(customerId, candidate.sessionId)) {
|
|
3128
|
+
return true;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
return false;
|
|
3132
|
+
}
|
|
3133
|
+
/**
|
|
3134
|
+
* Global LRU eviction down to the high-water mark. Excludes `selfKey` (the
|
|
3135
|
+
* arriving job's own session must not be its own victim).
|
|
3136
|
+
*/
|
|
3137
|
+
evictGlobal(selfKey) {
|
|
3138
|
+
const files = this.listSessionFiles().sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
3139
|
+
for (const file of files) {
|
|
3140
|
+
if (this.globalBytes <= this.globalEvictToBytes) {
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
if (sessionKey(file.customerId, file.sessionId) === selfKey) {
|
|
3144
|
+
continue;
|
|
3145
|
+
}
|
|
3146
|
+
this.deleteIfIdle(file.customerId, file.sessionId);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
/**
|
|
3150
|
+
* Delete a session iff idle: try-acquired mutex AND zero admitted count.
|
|
3151
|
+
* The mutex alone is not an in-flight guard - an admitted job spends its
|
|
3152
|
+
* whole payment phase before the mutex. Check and unlink are synchronous.
|
|
3153
|
+
*/
|
|
3154
|
+
deleteIfIdle(customerId, sessionId) {
|
|
3155
|
+
const key = sessionKey(customerId, sessionId);
|
|
3156
|
+
const release = this.tryAcquireSync(key);
|
|
3157
|
+
if (release === null) {
|
|
3158
|
+
return false;
|
|
3159
|
+
}
|
|
3160
|
+
try {
|
|
3161
|
+
if (this.admittedCount(customerId, sessionId) > 0) {
|
|
3162
|
+
return false;
|
|
3163
|
+
}
|
|
3164
|
+
this.deleteSessionFile(customerId, sessionId);
|
|
3165
|
+
return true;
|
|
3166
|
+
} finally {
|
|
3167
|
+
release();
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
deleteSessionFile(customerId, sessionId) {
|
|
3171
|
+
const path = this.sessionPath(customerId, sessionId);
|
|
3172
|
+
let size = 0;
|
|
3173
|
+
try {
|
|
3174
|
+
size = statSync(path).size;
|
|
3175
|
+
unlinkSync(path);
|
|
3176
|
+
} catch {
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
this.globalBytes = Math.max(0, this.globalBytes - size);
|
|
3180
|
+
this.compactionStrikes.delete(sessionKey(customerId, sessionId));
|
|
3181
|
+
}
|
|
3182
|
+
listSessionFiles() {
|
|
3183
|
+
const out = [];
|
|
3184
|
+
let customers;
|
|
3185
|
+
try {
|
|
3186
|
+
customers = readdirSync(this.root);
|
|
3187
|
+
} catch {
|
|
3188
|
+
return out;
|
|
3189
|
+
}
|
|
3190
|
+
for (const customerId of customers) {
|
|
3191
|
+
if (!CUSTOMER_PUBKEY_REGEX.test(customerId)) {
|
|
3192
|
+
continue;
|
|
3193
|
+
}
|
|
3194
|
+
let names;
|
|
3195
|
+
try {
|
|
3196
|
+
names = readdirSync(join(this.root, customerId));
|
|
3197
|
+
} catch {
|
|
3198
|
+
continue;
|
|
3199
|
+
}
|
|
3200
|
+
for (const name of names) {
|
|
3201
|
+
if (!name.endsWith(".jsonl")) {
|
|
3202
|
+
continue;
|
|
3203
|
+
}
|
|
3204
|
+
const sessionId = name.slice(0, -".jsonl".length);
|
|
3205
|
+
if (!SESSION_ID_REGEX.test(sessionId)) {
|
|
3206
|
+
continue;
|
|
3207
|
+
}
|
|
3208
|
+
try {
|
|
3209
|
+
const stat2 = statSync(join(this.root, customerId, name));
|
|
3210
|
+
out.push({ customerId, sessionId, size: stat2.size, mtimeMs: stat2.mtimeMs });
|
|
3211
|
+
} catch {
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
return out;
|
|
3216
|
+
}
|
|
3217
|
+
pruneSidecars(cutoffMs) {
|
|
3218
|
+
let customers;
|
|
3219
|
+
try {
|
|
3220
|
+
customers = readdirSync(this.root);
|
|
3221
|
+
} catch {
|
|
3222
|
+
return;
|
|
3223
|
+
}
|
|
3224
|
+
for (const customerId of customers) {
|
|
3225
|
+
const dir = join(this.root, customerId);
|
|
3226
|
+
let names;
|
|
3227
|
+
try {
|
|
3228
|
+
names = readdirSync(dir);
|
|
3229
|
+
} catch {
|
|
3230
|
+
continue;
|
|
3231
|
+
}
|
|
3232
|
+
for (const name of names) {
|
|
3233
|
+
if (!name.includes(".corrupt.")) {
|
|
3234
|
+
continue;
|
|
3235
|
+
}
|
|
3236
|
+
const path = join(dir, name);
|
|
3237
|
+
try {
|
|
3238
|
+
if (statSync(path).mtimeMs < cutoffMs) {
|
|
3239
|
+
unlinkSync(path);
|
|
3240
|
+
}
|
|
3241
|
+
} catch {
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
pruneEmptyCustomerDirs() {
|
|
3247
|
+
let customers;
|
|
3248
|
+
try {
|
|
3249
|
+
customers = readdirSync(this.root);
|
|
3250
|
+
} catch {
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
for (const customerId of customers) {
|
|
3254
|
+
const dir = join(this.root, customerId);
|
|
3255
|
+
try {
|
|
3256
|
+
if (readdirSync(dir).length === 0) {
|
|
3257
|
+
rmdirSync(dir);
|
|
3258
|
+
}
|
|
3259
|
+
} catch {
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
};
|
|
3264
|
+
function sessionKey(customerId, sessionId) {
|
|
3265
|
+
return `${customerId}/${sessionId}`;
|
|
3266
|
+
}
|
|
3267
|
+
function validateKeys(customerId, sessionId) {
|
|
3268
|
+
if (!CUSTOMER_PUBKEY_REGEX.test(customerId)) {
|
|
3269
|
+
throw new Error("SessionStore: invalid customer pubkey");
|
|
2002
3270
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
return { apiKey: envValue, origin: "env" };
|
|
3271
|
+
if (!SESSION_ID_REGEX.test(sessionId)) {
|
|
3272
|
+
throw new Error("SessionStore: invalid session id");
|
|
2006
3273
|
}
|
|
2007
|
-
const skillList = dependentSkills.length > 0 ? dependentSkills.join(", ") : "<none>";
|
|
2008
|
-
return {
|
|
2009
|
-
error: `Provider "${provider}" needs an API key (required by skill(s): ${skillList}). Set secrets.llm_api_keys.${provider} via 'npx @elisym/cli profile <agent>' or export ${descriptor.envVar}.`
|
|
2010
|
-
};
|
|
2011
3274
|
}
|
|
2012
|
-
function
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
return
|
|
3275
|
+
function parseSessionLine(text) {
|
|
3276
|
+
let value;
|
|
3277
|
+
try {
|
|
3278
|
+
value = JSON.parse(text);
|
|
3279
|
+
} catch {
|
|
3280
|
+
return null;
|
|
2018
3281
|
}
|
|
2019
|
-
if (
|
|
2020
|
-
return
|
|
3282
|
+
if (typeof value !== "object" || value === null) {
|
|
3283
|
+
return null;
|
|
2021
3284
|
}
|
|
2022
|
-
const
|
|
2023
|
-
if (
|
|
2024
|
-
|
|
3285
|
+
const record = value;
|
|
3286
|
+
if (record.type === "summary") {
|
|
3287
|
+
if (typeof record.content !== "string" || typeof record.ts !== "number") {
|
|
3288
|
+
return null;
|
|
3289
|
+
}
|
|
3290
|
+
return { type: "summary", content: record.content, ts: record.ts };
|
|
2025
3291
|
}
|
|
2026
|
-
|
|
3292
|
+
if (record.type === "turn") {
|
|
3293
|
+
if (record.role !== "user" && record.role !== "assistant" || typeof record.content !== "string" || typeof record.jobId !== "string" || typeof record.ts !== "number") {
|
|
3294
|
+
return null;
|
|
3295
|
+
}
|
|
3296
|
+
return {
|
|
3297
|
+
type: "turn",
|
|
3298
|
+
role: record.role,
|
|
3299
|
+
content: record.content,
|
|
3300
|
+
jobId: record.jobId,
|
|
3301
|
+
capability: typeof record.capability === "string" ? record.capability : void 0,
|
|
3302
|
+
ts: record.ts
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
return null;
|
|
2027
3306
|
}
|
|
2028
|
-
function
|
|
2029
|
-
const
|
|
2030
|
-
const
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
3307
|
+
function assembleReplay(lines, excludeJobId) {
|
|
3308
|
+
const raw = [];
|
|
3309
|
+
for (const line of lines) {
|
|
3310
|
+
if (line.type === "summary") {
|
|
3311
|
+
raw.push({ role: "user", content: `${SESSION_SUMMARY_PREFIX}
|
|
3312
|
+
${line.content}` });
|
|
3313
|
+
} else if (line.jobId !== excludeJobId) {
|
|
3314
|
+
raw.push({ role: line.role, content: line.content });
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
const nonEmpty = raw.filter((message) => message.content.trim().length > 0);
|
|
3318
|
+
let start = 0;
|
|
3319
|
+
while (start < nonEmpty.length && nonEmpty[start].role === "assistant") {
|
|
3320
|
+
start++;
|
|
3321
|
+
}
|
|
3322
|
+
let end = nonEmpty.length;
|
|
3323
|
+
while (end > start && nonEmpty[end - 1].role === "user") {
|
|
3324
|
+
end--;
|
|
3325
|
+
}
|
|
3326
|
+
const bounded = nonEmpty.slice(start, end);
|
|
3327
|
+
const merged = [];
|
|
3328
|
+
for (const message of bounded) {
|
|
3329
|
+
const previous = merged[merged.length - 1];
|
|
3330
|
+
if (previous !== void 0 && previous.role === message.role) {
|
|
3331
|
+
previous.content = `${previous.content}
|
|
3332
|
+
|
|
3333
|
+
${message.content}`;
|
|
3334
|
+
} else {
|
|
3335
|
+
merged.push({ ...message });
|
|
2036
3336
|
}
|
|
2037
|
-
};
|
|
2038
|
-
let logger;
|
|
2039
|
-
if (options.destination) {
|
|
2040
|
-
logger = pino(baseOptions, options.destination);
|
|
2041
|
-
} else if (options.tty === true) {
|
|
2042
|
-
logger = pino({
|
|
2043
|
-
...baseOptions,
|
|
2044
|
-
transport: {
|
|
2045
|
-
target: "pino-pretty",
|
|
2046
|
-
options: { destination: 2, colorize: true, translateTime: "HH:MM:ss" }
|
|
2047
|
-
}
|
|
2048
|
-
});
|
|
2049
|
-
} else {
|
|
2050
|
-
logger = pino(baseOptions, pino.destination(2));
|
|
2051
3337
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
3338
|
+
return merged;
|
|
3339
|
+
}
|
|
3340
|
+
function buildSummarizeText(lines, tailStart) {
|
|
3341
|
+
const parts = [];
|
|
3342
|
+
for (const line of lines.slice(0, tailStart)) {
|
|
3343
|
+
if (line.type === "summary") {
|
|
3344
|
+
parts.push(`Summary of earlier conversation:
|
|
3345
|
+
${line.content}`);
|
|
3346
|
+
} else {
|
|
3347
|
+
parts.push(`${line.role === "user" ? "User" : "Assistant"}: ${line.content}`);
|
|
3348
|
+
}
|
|
2055
3349
|
}
|
|
2056
|
-
return
|
|
2057
|
-
logger,
|
|
2058
|
-
logWithIndent,
|
|
2059
|
-
bannerLog: logWithIndent
|
|
2060
|
-
};
|
|
3350
|
+
return parts.join("\n\n");
|
|
2061
3351
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
function mimeFromPath(path) {
|
|
2072
|
-
return MIME_BY_EXT[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
3352
|
+
function truncateTurn(content, maxBytes) {
|
|
3353
|
+
if (Buffer.byteLength(content) <= maxBytes) {
|
|
3354
|
+
return content;
|
|
3355
|
+
}
|
|
3356
|
+
let sliced = content.slice(0, maxBytes - TRUNCATION_MARKER.length);
|
|
3357
|
+
while (Buffer.byteLength(sliced) > maxBytes - TRUNCATION_MARKER.length) {
|
|
3358
|
+
sliced = sliced.slice(0, Math.floor(sliced.length * 0.9));
|
|
3359
|
+
}
|
|
3360
|
+
return sliced + TRUNCATION_MARKER;
|
|
2073
3361
|
}
|
|
2074
3362
|
|
|
2075
3363
|
// src/x402/errors.ts
|
|
@@ -2113,6 +3401,29 @@ var LEDGER_GC_INTERVAL_MS = 60 * 60 * 1e3;
|
|
|
2113
3401
|
var LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2114
3402
|
var MAX_PAID_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
2115
3403
|
var SIG_PATH_TIMEOUT_MS = 60 * 1e3;
|
|
3404
|
+
var SESSION_SUMMARIZE_SYSTEM = "Summarize the following conversation between a user and an assistant so the conversation can continue with your summary standing in for the older turns. Preserve facts, decisions, names, numbers, constraints, and open questions. Reply with the summary only.";
|
|
3405
|
+
function buildUserRecord(data, fileName) {
|
|
3406
|
+
if (fileName === void 0) {
|
|
3407
|
+
return data;
|
|
3408
|
+
}
|
|
3409
|
+
const placeholder = `[file input: ${fileName}]`;
|
|
3410
|
+
return data.length > 0 ? `${data}
|
|
3411
|
+
${placeholder}` : placeholder;
|
|
3412
|
+
}
|
|
3413
|
+
function buildAssistantRecord(output) {
|
|
3414
|
+
let filePaths = [];
|
|
3415
|
+
if (output.filePaths !== void 0 && output.filePaths.length > 0) {
|
|
3416
|
+
filePaths = output.filePaths;
|
|
3417
|
+
} else if (output.filePath !== void 0) {
|
|
3418
|
+
filePaths = [output.filePath];
|
|
3419
|
+
}
|
|
3420
|
+
if (filePaths.length === 0) {
|
|
3421
|
+
return output.data;
|
|
3422
|
+
}
|
|
3423
|
+
const placeholders = filePaths.map((path) => `[file result: ${basename(path)}]`).join("\n");
|
|
3424
|
+
return output.data.length > 0 ? `${output.data}
|
|
3425
|
+
${placeholders}` : placeholders;
|
|
3426
|
+
}
|
|
2116
3427
|
function resolveJobPrice(tags, skills) {
|
|
2117
3428
|
const skill = skills.route(tags);
|
|
2118
3429
|
return skill?.priceSubunits ?? 0;
|
|
@@ -2229,7 +3540,7 @@ var PAID_GLOBAL_MAX_JOBS_PER_WINDOW = 2e3;
|
|
|
2229
3540
|
var MAX_TRACKED_CUSTOMERS = 1e3;
|
|
2230
3541
|
var GLOBAL_LIMITER_KEY = "__global__";
|
|
2231
3542
|
var AgentRuntime = class {
|
|
2232
|
-
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor, irohTransport,
|
|
3543
|
+
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor, irohTransport, identity2, blossomTransport, sessionStore) {
|
|
2233
3544
|
this.transport = transport;
|
|
2234
3545
|
this.skills = skills;
|
|
2235
3546
|
this.skillCtx = skillCtx;
|
|
@@ -2238,8 +3549,9 @@ var AgentRuntime = class {
|
|
|
2238
3549
|
this.callbacks = callbacks;
|
|
2239
3550
|
this.healthMonitor = healthMonitor;
|
|
2240
3551
|
this.irohTransport = irohTransport;
|
|
2241
|
-
this.identity =
|
|
3552
|
+
this.identity = identity2;
|
|
2242
3553
|
this.blossomTransport = blossomTransport;
|
|
3554
|
+
this.sessionStore = sessionStore;
|
|
2243
3555
|
this.limit = pLimit(config.maxConcurrentJobs);
|
|
2244
3556
|
this.maxQueueSize = config.maxQueueSize ?? config.maxConcurrentJobs * 10;
|
|
2245
3557
|
}
|
|
@@ -2287,6 +3599,102 @@ var AgentRuntime = class {
|
|
|
2287
3599
|
* the operator's API key.
|
|
2288
3600
|
*/
|
|
2289
3601
|
freeLlmLimiters = createFreeLlmLimiterSet();
|
|
3602
|
+
/**
|
|
3603
|
+
* Whether a job takes the session path: session ref present (transport only
|
|
3604
|
+
* populates it for NIP-44-encrypted requests) AND the matched skill opted in
|
|
3605
|
+
* (`context: true` - llm or dynamic-script mode) AND a session store is
|
|
3606
|
+
* wired. Everything else processes stateless - deterministically, never
|
|
3607
|
+
* implicitly per-pubkey.
|
|
3608
|
+
*/
|
|
3609
|
+
resolveJobSession(job, skill) {
|
|
3610
|
+
if (job.session === void 0 || this.sessionStore === void 0 || skill === null || skill === void 0 || skill.mode !== "llm" && skill.mode !== "dynamic-script" || skill.context !== true) {
|
|
3611
|
+
return null;
|
|
3612
|
+
}
|
|
3613
|
+
return {
|
|
3614
|
+
store: this.sessionStore,
|
|
3615
|
+
customerId: job.customerId,
|
|
3616
|
+
sessionId: job.session.id,
|
|
3617
|
+
skill
|
|
3618
|
+
};
|
|
3619
|
+
}
|
|
3620
|
+
/**
|
|
3621
|
+
* Run a session-path execution: history load -> execute -> append, under the
|
|
3622
|
+
* session mutex. Lock-order invariant: callers already hold their `pLimit`
|
|
3623
|
+
* slot (live and recovery paths alike), so a mutex holder never waits for a
|
|
3624
|
+
* slot. The mutex is released after the append, BEFORE result seeding and
|
|
3625
|
+
* delivery - it never spans that network I/O (the execute callback's own LLM
|
|
3626
|
+
* call, and the compaction summarize below, are bounded by the LLM HTTP
|
|
3627
|
+
* timeout). Appends only happen on execution success; a failed job leaves
|
|
3628
|
+
* the transcript untouched.
|
|
3629
|
+
*/
|
|
3630
|
+
async runSessionExchange(params) {
|
|
3631
|
+
const { store, customerId, sessionId, skill } = params.session;
|
|
3632
|
+
const release = await store.acquire(customerId, sessionId);
|
|
3633
|
+
try {
|
|
3634
|
+
const opened = store.open(
|
|
3635
|
+
customerId,
|
|
3636
|
+
sessionId,
|
|
3637
|
+
params.excludeOwnTurns === true ? params.jobId : void 0
|
|
3638
|
+
);
|
|
3639
|
+
let messages = opened.messages;
|
|
3640
|
+
if (!opened.stateless && opened.compactionNeeded && opened.summarizeText !== void 0) {
|
|
3641
|
+
messages = await this.compactSession({
|
|
3642
|
+
store,
|
|
3643
|
+
customerId,
|
|
3644
|
+
sessionId,
|
|
3645
|
+
skill,
|
|
3646
|
+
summarizeText: opened.summarizeText,
|
|
3647
|
+
fallbackMessages: messages,
|
|
3648
|
+
signal: params.signal,
|
|
3649
|
+
log: params.log,
|
|
3650
|
+
jobId: params.jobId
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
const history = opened.stateless || messages.length === 0 ? void 0 : messages;
|
|
3654
|
+
const output = await params.execute(history);
|
|
3655
|
+
if (!opened.stateless) {
|
|
3656
|
+
store.appendExchange(customerId, sessionId, {
|
|
3657
|
+
jobId: params.jobId,
|
|
3658
|
+
capability: skill.name,
|
|
3659
|
+
userContent: params.userRecord,
|
|
3660
|
+
assistantContent: buildAssistantRecord(output),
|
|
3661
|
+
skipRoles: opened.recordedRoles
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
return output;
|
|
3665
|
+
} finally {
|
|
3666
|
+
release();
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
/**
|
|
3670
|
+
* Compaction: one summarize call on the invoked skill's own resolved LLM,
|
|
3671
|
+
* then an atomic rewrite to `[summary, ...kept tail]`. Any failure - thrown
|
|
3672
|
+
* error, missing client, or an empty-string summary (rewriting on it would
|
|
3673
|
+
* silently vaporize the summarized context, since empty messages are skipped
|
|
3674
|
+
* at replay) - counts a strike and processes this job with the uncompacted
|
|
3675
|
+
* history; after 2 consecutive strikes the store force-truncates without a
|
|
3676
|
+
* summary so a small-window model can never permanently brick a session.
|
|
3677
|
+
*/
|
|
3678
|
+
async compactSession(params) {
|
|
3679
|
+
const { store, customerId, sessionId, skill } = params;
|
|
3680
|
+
try {
|
|
3681
|
+
const llm = this.skillCtx.getLlm?.(skill.llmOverride) ?? this.skillCtx.llm;
|
|
3682
|
+
if (llm === void 0) {
|
|
3683
|
+
throw new Error("no LLM client available for session compaction");
|
|
3684
|
+
}
|
|
3685
|
+
const summary = await llm.complete(
|
|
3686
|
+
SESSION_SUMMARIZE_SYSTEM,
|
|
3687
|
+
params.summarizeText,
|
|
3688
|
+
params.signal
|
|
3689
|
+
);
|
|
3690
|
+
return store.compact(customerId, sessionId, summary);
|
|
3691
|
+
} catch (err) {
|
|
3692
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
3693
|
+
params.log(`[${params.jobId.slice(0, 8)}] Session compaction failed: ${detail}`);
|
|
3694
|
+
const outcome = store.compactionFailed(customerId, sessionId);
|
|
3695
|
+
return outcome.forced && outcome.messages !== void 0 ? outcome.messages : params.fallbackMessages;
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
2290
3698
|
/**
|
|
2291
3699
|
* Inspect an error thrown by `skill.execute()` and, when it carries a
|
|
2292
3700
|
* billing/invalid signal from the LLM provider, flip the matching
|
|
@@ -2431,6 +3839,7 @@ var AgentRuntime = class {
|
|
|
2431
3839
|
async run() {
|
|
2432
3840
|
const log = this.callbacks.onLog ?? console.log;
|
|
2433
3841
|
this.ledger.pruneOldEntries(LEDGER_RETENTION_MS);
|
|
3842
|
+
this.sessionStore?.init();
|
|
2434
3843
|
await this.recoverPendingJobs();
|
|
2435
3844
|
this.recoveryInterval = setInterval(
|
|
2436
3845
|
() => this.recoverPendingJobs().catch((e) => log(`Recovery error: ${e}`)),
|
|
@@ -2442,6 +3851,11 @@ var AgentRuntime = class {
|
|
|
2442
3851
|
} catch (e) {
|
|
2443
3852
|
log(`GC error: ${e.message}`);
|
|
2444
3853
|
}
|
|
3854
|
+
try {
|
|
3855
|
+
this.sessionStore?.gc();
|
|
3856
|
+
} catch (e) {
|
|
3857
|
+
log(`Session GC error: ${e.message}`);
|
|
3858
|
+
}
|
|
2445
3859
|
this.cleanupRateLimits();
|
|
2446
3860
|
if (!this.transport.isHealthy()) {
|
|
2447
3861
|
log("Warning: no events received in 30+ minutes. Check relay connectivity.");
|
|
@@ -2466,6 +3880,15 @@ var AgentRuntime = class {
|
|
|
2466
3880
|
const isPaid = matched ? matched.priceSubunits > 0 : false;
|
|
2467
3881
|
const customerLimiter = isPaid ? this.paidCustomerLimiter : this.freeCustomerLimiter;
|
|
2468
3882
|
const globalLimiter = isPaid ? this.paidGlobalLimiter : this.freeGlobalLimiter;
|
|
3883
|
+
const jobSession = this.resolveJobSession(job, matched);
|
|
3884
|
+
if (jobSession && jobSession.store.admittedCount(jobSession.customerId, jobSession.sessionId) >= SESSION_MAX_CONCURRENT_JOBS) {
|
|
3885
|
+
this.transport.sendFeedback(job, {
|
|
3886
|
+
type: "error",
|
|
3887
|
+
message: "Session busy - wait for the previous message to finish, then retry."
|
|
3888
|
+
}).catch(() => {
|
|
3889
|
+
});
|
|
3890
|
+
return;
|
|
3891
|
+
}
|
|
2469
3892
|
if (!customerLimiter.peek(job.customerId).allowed) {
|
|
2470
3893
|
this.transport.sendFeedback(job, { type: "error", message: "Rate limited, try again later" }).catch(() => {
|
|
2471
3894
|
});
|
|
@@ -2505,16 +3928,22 @@ var AgentRuntime = class {
|
|
|
2505
3928
|
this.callbacks.onJobReceived?.(job);
|
|
2506
3929
|
this.inFlight.add(job.jobId);
|
|
2507
3930
|
this.pending++;
|
|
3931
|
+
if (jobSession) {
|
|
3932
|
+
jobSession.store.admitLive(jobSession.customerId, jobSession.sessionId);
|
|
3933
|
+
}
|
|
2508
3934
|
this.limit(() => this.processJob(job)).catch((e) => {
|
|
2509
3935
|
this.callbacks.onJobError?.(job.jobId, e.message);
|
|
2510
3936
|
}).finally(() => {
|
|
2511
3937
|
this.inFlight.delete(job.jobId);
|
|
2512
3938
|
this.pending--;
|
|
3939
|
+
if (jobSession) {
|
|
3940
|
+
jobSession.store.releaseLive(jobSession.customerId, jobSession.sessionId);
|
|
3941
|
+
}
|
|
2513
3942
|
});
|
|
2514
3943
|
});
|
|
2515
3944
|
log("Agent runtime started. Listening for jobs...");
|
|
2516
|
-
await new Promise((
|
|
2517
|
-
this.abortController.signal.addEventListener("abort", () =>
|
|
3945
|
+
await new Promise((resolve4) => {
|
|
3946
|
+
this.abortController.signal.addEventListener("abort", () => resolve4(), { once: true });
|
|
2518
3947
|
let shuttingDown = false;
|
|
2519
3948
|
const onSignal = () => {
|
|
2520
3949
|
if (shuttingDown) {
|
|
@@ -2704,68 +4133,82 @@ var AgentRuntime = class {
|
|
|
2704
4133
|
throw new Error("No skill matched for tags: " + job.tags.join(", "));
|
|
2705
4134
|
}
|
|
2706
4135
|
log(`[${job.jobId.slice(0, 8)}] Executing skill: ${skill.name}`);
|
|
2707
|
-
let output;
|
|
2708
4136
|
const budgetMs = this.resolveExecutionBudgetMs(skill);
|
|
2709
|
-
|
|
2710
|
-
const
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
execAbort.abort();
|
|
2715
|
-
} else {
|
|
2716
|
-
signal.addEventListener("abort", onOuterAbort);
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
let budgetTimer;
|
|
2720
|
-
try {
|
|
2721
|
-
const execPromise = skill.execute(
|
|
2722
|
-
{
|
|
2723
|
-
data: inputFile?.inlineText ?? job.input,
|
|
2724
|
-
inputType: job.inputType,
|
|
2725
|
-
tags: job.tags,
|
|
2726
|
-
jobId: job.jobId,
|
|
2727
|
-
filePath: inputFile?.filePath
|
|
2728
|
-
},
|
|
2729
|
-
{ ...this.skillCtx, signal: execAbort.signal }
|
|
2730
|
-
);
|
|
2731
|
-
execPromise.catch(() => {
|
|
2732
|
-
});
|
|
2733
|
-
if (budgetMs > 0) {
|
|
2734
|
-
output = await Promise.race([
|
|
2735
|
-
execPromise,
|
|
2736
|
-
new Promise((_resolve, reject) => {
|
|
2737
|
-
budgetTimer = setTimeout(() => {
|
|
2738
|
-
budgetExceeded = true;
|
|
2739
|
-
execAbort.abort();
|
|
2740
|
-
reject(new ExecutionBudgetExceededError(budgetMs));
|
|
2741
|
-
}, budgetMs);
|
|
2742
|
-
})
|
|
2743
|
-
]);
|
|
2744
|
-
} else {
|
|
2745
|
-
output = await execPromise;
|
|
2746
|
-
}
|
|
2747
|
-
} catch (err) {
|
|
2748
|
-
if (budgetExceeded) {
|
|
2749
|
-
log(`[${job.jobId.slice(0, 8)}] Execution exceeded budget (${budgetMs / 1e3}s)`);
|
|
2750
|
-
throw new ExecutionBudgetExceededError(budgetMs);
|
|
2751
|
-
}
|
|
2752
|
-
const flippedToUnhealthy = this.markHealthFromExecuteError(skill, err, log, job.jobId);
|
|
2753
|
-
if (flippedToUnhealthy) {
|
|
2754
|
-
throw new AgentUnavailableError();
|
|
2755
|
-
}
|
|
2756
|
-
throw err;
|
|
2757
|
-
} finally {
|
|
2758
|
-
if (budgetTimer) {
|
|
2759
|
-
clearTimeout(budgetTimer);
|
|
2760
|
-
}
|
|
4137
|
+
const jobSession = this.resolveJobSession(job, skill);
|
|
4138
|
+
const runExecution = async (history) => {
|
|
4139
|
+
let budgetExceeded = false;
|
|
4140
|
+
const execAbort = new AbortController();
|
|
4141
|
+
const onOuterAbort = () => execAbort.abort();
|
|
2761
4142
|
if (signal) {
|
|
2762
|
-
signal.
|
|
4143
|
+
if (signal.aborted) {
|
|
4144
|
+
execAbort.abort();
|
|
4145
|
+
} else {
|
|
4146
|
+
signal.addEventListener("abort", onOuterAbort);
|
|
4147
|
+
}
|
|
2763
4148
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
4149
|
+
let budgetTimer;
|
|
4150
|
+
try {
|
|
4151
|
+
const execPromise = skill.execute(
|
|
4152
|
+
{
|
|
4153
|
+
data: inputFile?.inlineText ?? job.input,
|
|
4154
|
+
inputType: job.inputType,
|
|
4155
|
+
tags: job.tags,
|
|
4156
|
+
jobId: job.jobId,
|
|
4157
|
+
filePath: inputFile?.filePath,
|
|
4158
|
+
history,
|
|
4159
|
+
...jobSession !== null ? { sessionId: jobSession.sessionId } : {}
|
|
4160
|
+
},
|
|
4161
|
+
{ ...this.skillCtx, signal: execAbort.signal }
|
|
4162
|
+
);
|
|
4163
|
+
execPromise.catch(() => {
|
|
2766
4164
|
});
|
|
4165
|
+
if (budgetMs > 0) {
|
|
4166
|
+
return await Promise.race([
|
|
4167
|
+
execPromise,
|
|
4168
|
+
new Promise((_resolve, reject) => {
|
|
4169
|
+
budgetTimer = setTimeout(() => {
|
|
4170
|
+
budgetExceeded = true;
|
|
4171
|
+
execAbort.abort();
|
|
4172
|
+
reject(new ExecutionBudgetExceededError(budgetMs));
|
|
4173
|
+
}, budgetMs);
|
|
4174
|
+
})
|
|
4175
|
+
]);
|
|
4176
|
+
}
|
|
4177
|
+
return await execPromise;
|
|
4178
|
+
} catch (err) {
|
|
4179
|
+
if (budgetExceeded) {
|
|
4180
|
+
log(`[${job.jobId.slice(0, 8)}] Execution exceeded budget (${budgetMs / 1e3}s)`);
|
|
4181
|
+
throw new ExecutionBudgetExceededError(budgetMs);
|
|
4182
|
+
}
|
|
4183
|
+
const flippedToUnhealthy = this.markHealthFromExecuteError(skill, err, log, job.jobId);
|
|
4184
|
+
if (flippedToUnhealthy) {
|
|
4185
|
+
throw new AgentUnavailableError();
|
|
4186
|
+
}
|
|
4187
|
+
throw err;
|
|
4188
|
+
} finally {
|
|
4189
|
+
if (budgetTimer) {
|
|
4190
|
+
clearTimeout(budgetTimer);
|
|
4191
|
+
}
|
|
4192
|
+
if (signal) {
|
|
4193
|
+
signal.removeEventListener("abort", onOuterAbort);
|
|
4194
|
+
}
|
|
4195
|
+
if (inputFile) {
|
|
4196
|
+
await inputFile.cleanup().catch(() => {
|
|
4197
|
+
});
|
|
4198
|
+
}
|
|
2767
4199
|
}
|
|
2768
|
-
}
|
|
4200
|
+
};
|
|
4201
|
+
const output = jobSession === null ? await runExecution() : await this.runSessionExchange({
|
|
4202
|
+
session: jobSession,
|
|
4203
|
+
jobId: job.jobId,
|
|
4204
|
+
userRecord: buildUserRecord(
|
|
4205
|
+
inputFile?.inlineText ?? job.input,
|
|
4206
|
+
inputFile?.filePath !== void 0 ? job.attachment?.name ?? "attachment" : void 0
|
|
4207
|
+
),
|
|
4208
|
+
signal,
|
|
4209
|
+
log,
|
|
4210
|
+
execute: runExecution
|
|
4211
|
+
});
|
|
2769
4212
|
const { attachments, deliveredContent } = await this.buildResultAttachment(
|
|
2770
4213
|
job.jobId,
|
|
2771
4214
|
output,
|
|
@@ -3144,7 +4587,7 @@ var AgentRuntime = class {
|
|
|
3144
4587
|
let refOutcome;
|
|
3145
4588
|
let result;
|
|
3146
4589
|
try {
|
|
3147
|
-
result = await new Promise((
|
|
4590
|
+
result = await new Promise((resolve4, reject) => {
|
|
3148
4591
|
let settled = false;
|
|
3149
4592
|
let pending = 2;
|
|
3150
4593
|
let lastResult = { verified: false };
|
|
@@ -3154,7 +4597,7 @@ var AgentRuntime = class {
|
|
|
3154
4597
|
}
|
|
3155
4598
|
settled = true;
|
|
3156
4599
|
clearTimeout(deadline);
|
|
3157
|
-
|
|
4600
|
+
resolve4(verified);
|
|
3158
4601
|
};
|
|
3159
4602
|
const lose = (verified, reason) => {
|
|
3160
4603
|
if (settled) {
|
|
@@ -3166,7 +4609,7 @@ var AgentRuntime = class {
|
|
|
3166
4609
|
if (pending === 0) {
|
|
3167
4610
|
settled = true;
|
|
3168
4611
|
clearTimeout(deadline);
|
|
3169
|
-
|
|
4612
|
+
resolve4(lastResult);
|
|
3170
4613
|
}
|
|
3171
4614
|
};
|
|
3172
4615
|
this.transport.waitForPaymentSignature(job.jobId, job.customerId, verifyAbort.signal, sigPathTimeoutMs).then(async (sig) => {
|
|
@@ -3219,7 +4662,7 @@ var AgentRuntime = class {
|
|
|
3219
4662
|
}
|
|
3220
4663
|
settled = true;
|
|
3221
4664
|
log(`[${job.jobId.slice(0, 8)}] verify deadline (${deadlineMs}ms) hit`);
|
|
3222
|
-
|
|
4665
|
+
resolve4(lastResult);
|
|
3223
4666
|
}, deadlineMs);
|
|
3224
4667
|
if (signal?.aborted) {
|
|
3225
4668
|
settled = true;
|
|
@@ -3272,8 +4715,52 @@ var AgentRuntime = class {
|
|
|
3272
4715
|
}
|
|
3273
4716
|
throw new PaymentTimeoutError();
|
|
3274
4717
|
}
|
|
4718
|
+
/**
|
|
4719
|
+
* Session refs of pending `paid` entries, for the recovery pre-pass. `paid`
|
|
4720
|
+
* only: `executed` entries do no session work and the status-predicate
|
|
4721
|
+
* release would drop them at the first re-check anyway. Applies the same
|
|
4722
|
+
* session-path scope as live intake (encrypted-only decode + routed skill
|
|
4723
|
+
* with `context: true`, llm or dynamic-script mode) - a context-off or x402
|
|
4724
|
+
* entry parked for hours must not pin session slots for a job that never
|
|
4725
|
+
* touches the transcript.
|
|
4726
|
+
*/
|
|
4727
|
+
collectRecoverySessionRefs(pending) {
|
|
4728
|
+
const refs = [];
|
|
4729
|
+
for (const entry of pending) {
|
|
4730
|
+
if (entry.status !== "paid" || entry.raw_event_json === void 0) {
|
|
4731
|
+
continue;
|
|
4732
|
+
}
|
|
4733
|
+
const skill = this.skills.route(entry.tags);
|
|
4734
|
+
if (!skill || skill.mode !== "llm" && skill.mode !== "dynamic-script" || skill.context !== true) {
|
|
4735
|
+
continue;
|
|
4736
|
+
}
|
|
4737
|
+
try {
|
|
4738
|
+
const rawEvent = JSON.parse(entry.raw_event_json);
|
|
4739
|
+
const encrypted = rawEvent.tags?.some((tag) => tag[0] === "encrypted" && tag[1] === "nip44") === true;
|
|
4740
|
+
if (!encrypted || typeof rawEvent.content !== "string") {
|
|
4741
|
+
continue;
|
|
4742
|
+
}
|
|
4743
|
+
const session = decodeJobPayload(rawEvent.content).session;
|
|
4744
|
+
if (session !== void 0) {
|
|
4745
|
+
refs.push({
|
|
4746
|
+
jobId: entry.job_id,
|
|
4747
|
+
customerId: entry.customer_id,
|
|
4748
|
+
sessionId: session.id
|
|
4749
|
+
});
|
|
4750
|
+
}
|
|
4751
|
+
} catch {
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
return refs;
|
|
4755
|
+
}
|
|
3275
4756
|
async recoverPendingJobs() {
|
|
3276
4757
|
const pending = this.ledger.pendingJobs().filter((e) => !this.inFlight.has(e.job_id));
|
|
4758
|
+
if (this.sessionStore !== void 0) {
|
|
4759
|
+
this.sessionStore.syncRecoveryRegistrations(
|
|
4760
|
+
this.collectRecoverySessionRefs(pending),
|
|
4761
|
+
(jobId) => this.ledger.getStatus(jobId) === "paid"
|
|
4762
|
+
);
|
|
4763
|
+
}
|
|
3277
4764
|
if (pending.length === 0) {
|
|
3278
4765
|
return;
|
|
3279
4766
|
}
|
|
@@ -3304,7 +4791,7 @@ var AgentRuntime = class {
|
|
|
3304
4791
|
for (const entry of pending) {
|
|
3305
4792
|
const ageMs = (Math.floor(Date.now() / 1e3) - entry.created_at) * 1e3;
|
|
3306
4793
|
const expired = ageMs > MAX_PAID_AGE_MS;
|
|
3307
|
-
const exhaustedRetries = entry.retry_count >= this.config.recoveryMaxRetries;
|
|
4794
|
+
const exhaustedRetries = entry.status === "paid" && entry.retry_count >= this.config.recoveryMaxRetries;
|
|
3308
4795
|
if (expired || exhaustedRetries) {
|
|
3309
4796
|
this.ledger.markFailed(entry.job_id);
|
|
3310
4797
|
const reason = expired ? "Job permanently failed: agent did not recover within 24 hours" : "Job permanently failed after maximum retries";
|
|
@@ -3444,14 +4931,17 @@ var AgentRuntime = class {
|
|
|
3444
4931
|
}
|
|
3445
4932
|
}
|
|
3446
4933
|
let recoveryInputFile;
|
|
4934
|
+
let recoverySession;
|
|
3447
4935
|
try {
|
|
3448
4936
|
const encrypted = rawEvent.tags?.some(
|
|
3449
4937
|
(tag) => tag[0] === "encrypted" && tag[1] === "nip44"
|
|
3450
4938
|
);
|
|
3451
4939
|
const iTag = rawEvent.tags?.find((tag) => tag[0] === "i");
|
|
3452
4940
|
const rawInput = encrypted ? rawEvent.content : iTag?.[1] ?? rawEvent.content;
|
|
4941
|
+
const decodedPayload = decodeJobPayload(rawInput);
|
|
4942
|
+
recoverySession = encrypted ? decodedPayload.session : void 0;
|
|
3453
4943
|
recoveryInputFile = await this.resolveInputFile(
|
|
3454
|
-
|
|
4944
|
+
decodedPayload.attachment,
|
|
3455
4945
|
entry.customer_id,
|
|
3456
4946
|
recoveryAbort.signal
|
|
3457
4947
|
);
|
|
@@ -3461,46 +4951,64 @@ var AgentRuntime = class {
|
|
|
3461
4951
|
return;
|
|
3462
4952
|
}
|
|
3463
4953
|
const recoveryBudgetMs = this.resolveExecutionBudgetMs(skill);
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
recoveryAbort.abort();
|
|
3485
|
-
reject(new ExecutionBudgetExceededError(recoveryBudgetMs));
|
|
3486
|
-
}, recoveryBudgetMs);
|
|
3487
|
-
})
|
|
3488
|
-
]);
|
|
3489
|
-
} else {
|
|
3490
|
-
output = await execPromise;
|
|
3491
|
-
}
|
|
3492
|
-
} catch (err) {
|
|
3493
|
-
this.markHealthFromExecuteError(skill, err, log, entry.job_id);
|
|
3494
|
-
throw err;
|
|
3495
|
-
} finally {
|
|
3496
|
-
if (budgetTimer) {
|
|
3497
|
-
clearTimeout(budgetTimer);
|
|
3498
|
-
}
|
|
3499
|
-
if (recoveryInputFile) {
|
|
3500
|
-
await recoveryInputFile.cleanup().catch(() => {
|
|
4954
|
+
const recoveryJobSession = this.resolveJobSession(
|
|
4955
|
+
{ session: recoverySession, customerId: entry.customer_id },
|
|
4956
|
+
skill
|
|
4957
|
+
);
|
|
4958
|
+
const runRecoveryExecution = async (history) => {
|
|
4959
|
+
let budgetTimer;
|
|
4960
|
+
try {
|
|
4961
|
+
const execPromise = skill.execute(
|
|
4962
|
+
{
|
|
4963
|
+
data: recoveryInputFile?.inlineText ?? entry.input,
|
|
4964
|
+
inputType: entry.input_type,
|
|
4965
|
+
tags: entry.tags,
|
|
4966
|
+
jobId: entry.job_id,
|
|
4967
|
+
filePath: recoveryInputFile?.filePath,
|
|
4968
|
+
history,
|
|
4969
|
+
...recoveryJobSession !== null ? { sessionId: recoveryJobSession.sessionId } : {}
|
|
4970
|
+
},
|
|
4971
|
+
{ ...this.skillCtx, signal: recoveryAbort.signal }
|
|
4972
|
+
);
|
|
4973
|
+
execPromise.catch(() => {
|
|
3501
4974
|
});
|
|
4975
|
+
if (recoveryBudgetMs > 0) {
|
|
4976
|
+
return await Promise.race([
|
|
4977
|
+
execPromise,
|
|
4978
|
+
new Promise((_resolve, reject) => {
|
|
4979
|
+
budgetTimer = setTimeout(() => {
|
|
4980
|
+
recoveryAbort.abort();
|
|
4981
|
+
reject(new ExecutionBudgetExceededError(recoveryBudgetMs));
|
|
4982
|
+
}, recoveryBudgetMs);
|
|
4983
|
+
})
|
|
4984
|
+
]);
|
|
4985
|
+
}
|
|
4986
|
+
return await execPromise;
|
|
4987
|
+
} catch (err) {
|
|
4988
|
+
this.markHealthFromExecuteError(skill, err, log, entry.job_id);
|
|
4989
|
+
throw err;
|
|
4990
|
+
} finally {
|
|
4991
|
+
if (budgetTimer) {
|
|
4992
|
+
clearTimeout(budgetTimer);
|
|
4993
|
+
}
|
|
4994
|
+
if (recoveryInputFile) {
|
|
4995
|
+
await recoveryInputFile.cleanup().catch(() => {
|
|
4996
|
+
});
|
|
4997
|
+
}
|
|
3502
4998
|
}
|
|
3503
|
-
}
|
|
4999
|
+
};
|
|
5000
|
+
const output = recoveryJobSession === null ? await runRecoveryExecution() : await this.runSessionExchange({
|
|
5001
|
+
session: recoveryJobSession,
|
|
5002
|
+
jobId: entry.job_id,
|
|
5003
|
+
userRecord: buildUserRecord(
|
|
5004
|
+
recoveryInputFile?.inlineText ?? entry.input,
|
|
5005
|
+
recoveryInputFile?.filePath !== void 0 ? "attachment" : void 0
|
|
5006
|
+
),
|
|
5007
|
+
signal: recoveryAbort.signal,
|
|
5008
|
+
log,
|
|
5009
|
+
execute: runRecoveryExecution,
|
|
5010
|
+
excludeOwnTurns: true
|
|
5011
|
+
});
|
|
3504
5012
|
const { attachments: resultAttachments, deliveredContent } = await this.buildResultAttachment(
|
|
3505
5013
|
entry.job_id,
|
|
3506
5014
|
output,
|
|
@@ -3615,6 +5123,9 @@ var SkillRegistry = class {
|
|
|
3615
5123
|
return this.skills;
|
|
3616
5124
|
}
|
|
3617
5125
|
};
|
|
5126
|
+
|
|
5127
|
+
// src/skill/loader.ts
|
|
5128
|
+
init_llm();
|
|
3618
5129
|
var StaticFileSkill = class {
|
|
3619
5130
|
name;
|
|
3620
5131
|
description;
|
|
@@ -3644,6 +5155,7 @@ var StaticFileSkill = class {
|
|
|
3644
5155
|
priceSubunits: BigInt(Math.round(params.priceSubunits)),
|
|
3645
5156
|
asset: params.asset,
|
|
3646
5157
|
outputFilePath: params.outputFilePath,
|
|
5158
|
+
skillDir: params.dir,
|
|
3647
5159
|
image: params.image,
|
|
3648
5160
|
imageFile: params.imageFile
|
|
3649
5161
|
});
|
|
@@ -3709,6 +5221,7 @@ var DynamicScriptSkill = class {
|
|
|
3709
5221
|
inputMime;
|
|
3710
5222
|
inputText;
|
|
3711
5223
|
outputMime;
|
|
5224
|
+
context;
|
|
3712
5225
|
inner;
|
|
3713
5226
|
constructor(params) {
|
|
3714
5227
|
this.name = params.name;
|
|
@@ -3720,6 +5233,7 @@ var DynamicScriptSkill = class {
|
|
|
3720
5233
|
this.imageFile = params.imageFile;
|
|
3721
5234
|
this.dir = params.dir;
|
|
3722
5235
|
this.llmOverride = params.llmOverride;
|
|
5236
|
+
this.context = params.context;
|
|
3723
5237
|
this.inputMime = params.inputMime;
|
|
3724
5238
|
this.inputText = params.inputText;
|
|
3725
5239
|
this.outputMime = params.outputMime;
|
|
@@ -3735,7 +5249,8 @@ var DynamicScriptSkill = class {
|
|
|
3735
5249
|
scriptEnv: params.scriptEnv,
|
|
3736
5250
|
image: params.image,
|
|
3737
5251
|
imageFile: params.imageFile,
|
|
3738
|
-
outputMime: params.outputMime
|
|
5252
|
+
outputMime: params.outputMime,
|
|
5253
|
+
context: params.context
|
|
3739
5254
|
});
|
|
3740
5255
|
}
|
|
3741
5256
|
async execute(input, ctx) {
|
|
@@ -3750,11 +5265,12 @@ var ScriptSkill = class {
|
|
|
3750
5265
|
asset;
|
|
3751
5266
|
mode = "llm";
|
|
3752
5267
|
llmOverride;
|
|
5268
|
+
context;
|
|
3753
5269
|
image;
|
|
3754
5270
|
imageFile;
|
|
3755
5271
|
dir;
|
|
3756
5272
|
inner;
|
|
3757
|
-
constructor(name, description, capabilities, priceSubunits, assetOrImage, imageOrImageFile, imageFileOrDir, dirOrPrompt, promptOrTools, toolsOrRounds, rounds, llmOverride) {
|
|
5273
|
+
constructor(name, description, capabilities, priceSubunits, assetOrImage, imageOrImageFile, imageFileOrDir, dirOrPrompt, promptOrTools, toolsOrRounds, rounds, llmOverride, context) {
|
|
3758
5274
|
let asset;
|
|
3759
5275
|
let image;
|
|
3760
5276
|
let imageFile;
|
|
@@ -3785,6 +5301,7 @@ var ScriptSkill = class {
|
|
|
3785
5301
|
this.priceSubunits = priceSubunits;
|
|
3786
5302
|
this.asset = asset;
|
|
3787
5303
|
this.llmOverride = llmOverride;
|
|
5304
|
+
this.context = context;
|
|
3788
5305
|
this.image = image;
|
|
3789
5306
|
this.imageFile = imageFile;
|
|
3790
5307
|
this.dir = skillDir;
|
|
@@ -3799,6 +5316,7 @@ var ScriptSkill = class {
|
|
|
3799
5316
|
tools,
|
|
3800
5317
|
maxToolRounds,
|
|
3801
5318
|
llmOverride,
|
|
5319
|
+
context,
|
|
3802
5320
|
image,
|
|
3803
5321
|
imageFile
|
|
3804
5322
|
});
|
|
@@ -3859,11 +5377,23 @@ var X402Skill = class {
|
|
|
3859
5377
|
};
|
|
3860
5378
|
|
|
3861
5379
|
// src/skill/loader.ts
|
|
5380
|
+
function scopeLlmKeys(scriptEnv, declaredProvider) {
|
|
5381
|
+
if (scriptEnv === void 0) {
|
|
5382
|
+
return void 0;
|
|
5383
|
+
}
|
|
5384
|
+
const env = { ...scriptEnv };
|
|
5385
|
+
for (const descriptor of listLlmProviders()) {
|
|
5386
|
+
if (descriptor.id !== declaredProvider) {
|
|
5387
|
+
delete env[descriptor.envVar];
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
return env;
|
|
5391
|
+
}
|
|
3862
5392
|
function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
3863
5393
|
let safeImageFile = parsed.imageFile;
|
|
3864
|
-
if (safeImageFile !== void 0 &&
|
|
5394
|
+
if (safeImageFile !== void 0 && resolveInsidePathReal(entryPath, safeImageFile) === null) {
|
|
3865
5395
|
console.warn(
|
|
3866
|
-
`SKILL.md "${parsed.name}": ignoring "image_file" that resolves outside the skill directory: ${safeImageFile}`
|
|
5396
|
+
`SKILL.md "${parsed.name}": ignoring "image_file" that resolves outside the skill directory (directly or via symlink): ${safeImageFile}`
|
|
3867
5397
|
);
|
|
3868
5398
|
safeImageFile = void 0;
|
|
3869
5399
|
}
|
|
@@ -3882,7 +5412,8 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3882
5412
|
parsed.systemPrompt,
|
|
3883
5413
|
parsed.tools,
|
|
3884
5414
|
parsed.maxToolRounds,
|
|
3885
|
-
parsed.llmOverride
|
|
5415
|
+
parsed.llmOverride,
|
|
5416
|
+
parsed.context
|
|
3886
5417
|
);
|
|
3887
5418
|
break;
|
|
3888
5419
|
case "static-file": {
|
|
@@ -3891,7 +5422,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3891
5422
|
`SKILL.md "${parsed.name}": internal error - outputFile missing for mode 'static-file'`
|
|
3892
5423
|
);
|
|
3893
5424
|
}
|
|
3894
|
-
const outputFilePath =
|
|
5425
|
+
const outputFilePath = resolveInsidePathReal(entryPath, parsed.outputFile);
|
|
3895
5426
|
if (!outputFilePath) {
|
|
3896
5427
|
throw new Error(
|
|
3897
5428
|
`SKILL.md "${parsed.name}": "output_file" must stay inside the skill directory`
|
|
@@ -3918,7 +5449,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3918
5449
|
`SKILL.md "${parsed.name}": internal error - script missing for mode '${parsed.mode}'`
|
|
3919
5450
|
);
|
|
3920
5451
|
}
|
|
3921
|
-
const scriptPath =
|
|
5452
|
+
const scriptPath = resolveInsidePathReal(entryPath, parsed.script);
|
|
3922
5453
|
if (!scriptPath) {
|
|
3923
5454
|
throw new Error(`SKILL.md "${parsed.name}": "script" must stay inside the skill directory`);
|
|
3924
5455
|
}
|
|
@@ -3931,7 +5462,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3931
5462
|
scriptPath,
|
|
3932
5463
|
scriptArgs: parsed.scriptArgs,
|
|
3933
5464
|
scriptTimeoutMs: parsed.scriptTimeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS,
|
|
3934
|
-
scriptEnv,
|
|
5465
|
+
scriptEnv: scopeLlmKeys(scriptEnv, parsed.llmOverride?.provider),
|
|
3935
5466
|
image: parsed.image,
|
|
3936
5467
|
imageFile: safeImageFile,
|
|
3937
5468
|
dir: entryPath,
|
|
@@ -3941,7 +5472,8 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3941
5472
|
...scriptParams,
|
|
3942
5473
|
outputMime: parsed.outputMime,
|
|
3943
5474
|
inputMime: parsed.inputMime,
|
|
3944
|
-
inputText: parsed.inputText
|
|
5475
|
+
inputText: parsed.inputText,
|
|
5476
|
+
context: parsed.context
|
|
3945
5477
|
}) : new StaticScriptSkill(scriptParams);
|
|
3946
5478
|
break;
|
|
3947
5479
|
}
|
|
@@ -4021,9 +5553,9 @@ function parseBidTag(value) {
|
|
|
4021
5553
|
}
|
|
4022
5554
|
var HEALTH_CHECK_IDLE_MS = 30 * 60 * 1e3;
|
|
4023
5555
|
var NostrTransport = class {
|
|
4024
|
-
constructor(client,
|
|
5556
|
+
constructor(client, identity2, kindOffsets) {
|
|
4025
5557
|
this.client = client;
|
|
4026
|
-
this.identity =
|
|
5558
|
+
this.identity = identity2;
|
|
4027
5559
|
this.kindOffsets = kindOffsets;
|
|
4028
5560
|
}
|
|
4029
5561
|
sub = null;
|
|
@@ -4059,10 +5591,12 @@ var NostrTransport = class {
|
|
|
4059
5591
|
const rawInput = encrypted ? event.content : iTag?.[1] ?? event.content;
|
|
4060
5592
|
let input;
|
|
4061
5593
|
let attachment;
|
|
5594
|
+
let session;
|
|
4062
5595
|
try {
|
|
4063
5596
|
const decoded = decodeJobPayload(rawInput);
|
|
4064
5597
|
input = decoded.text ?? "";
|
|
4065
5598
|
attachment = decoded.attachment;
|
|
5599
|
+
session = encrypted ? decoded.session : void 0;
|
|
4066
5600
|
} catch {
|
|
4067
5601
|
return;
|
|
4068
5602
|
}
|
|
@@ -4075,7 +5609,8 @@ var NostrTransport = class {
|
|
|
4075
5609
|
bid: parseBidTag(bidTag?.[1]),
|
|
4076
5610
|
encrypted,
|
|
4077
5611
|
rawEvent: event,
|
|
4078
|
-
attachment
|
|
5612
|
+
attachment,
|
|
5613
|
+
session
|
|
4079
5614
|
});
|
|
4080
5615
|
}
|
|
4081
5616
|
);
|
|
@@ -4098,7 +5633,7 @@ var NostrTransport = class {
|
|
|
4098
5633
|
* expiry window - without it this path waits forever on the relay.
|
|
4099
5634
|
*/
|
|
4100
5635
|
waitForPaymentSignature(jobId, customerPubkey, signal, timeoutMs) {
|
|
4101
|
-
return new Promise((
|
|
5636
|
+
return new Promise((resolve4) => {
|
|
4102
5637
|
let settled = false;
|
|
4103
5638
|
const filter = {
|
|
4104
5639
|
kinds: [KIND_JOB_FEEDBACK],
|
|
@@ -4117,7 +5652,7 @@ var NostrTransport = class {
|
|
|
4117
5652
|
}
|
|
4118
5653
|
signal.removeEventListener("abort", onAbort);
|
|
4119
5654
|
sub.close();
|
|
4120
|
-
|
|
5655
|
+
resolve4(sig);
|
|
4121
5656
|
};
|
|
4122
5657
|
const sub = this.client.pool.subscribe(filter, (event) => {
|
|
4123
5658
|
if (settled) {
|
|
@@ -4211,7 +5746,7 @@ var NostrTransport = class {
|
|
|
4211
5746
|
function startWatchdog(deps) {
|
|
4212
5747
|
const {
|
|
4213
5748
|
client,
|
|
4214
|
-
identity,
|
|
5749
|
+
identity: identity2,
|
|
4215
5750
|
transport,
|
|
4216
5751
|
onPing,
|
|
4217
5752
|
log,
|
|
@@ -4222,7 +5757,7 @@ function startWatchdog(deps) {
|
|
|
4222
5757
|
selfPingTimeoutMs = WATCHDOG_SELF_PING_TIMEOUT_MS,
|
|
4223
5758
|
now = Date.now
|
|
4224
5759
|
} = deps;
|
|
4225
|
-
let pingSub = client.ping.subscribeToPings(
|
|
5760
|
+
let pingSub = client.ping.subscribeToPings(identity2, onPing);
|
|
4226
5761
|
let stopped = false;
|
|
4227
5762
|
let probeBusy = false;
|
|
4228
5763
|
let selfPingBusy = false;
|
|
@@ -4231,7 +5766,7 @@ function startWatchdog(deps) {
|
|
|
4231
5766
|
const resetPoolAndResubscribe = () => {
|
|
4232
5767
|
transport.stop();
|
|
4233
5768
|
client.pool.reset();
|
|
4234
|
-
pingSub = client.ping.subscribeToPings(
|
|
5769
|
+
pingSub = client.ping.subscribeToPings(identity2, onPing);
|
|
4235
5770
|
transport.restart();
|
|
4236
5771
|
};
|
|
4237
5772
|
const detectAndHandleSleep = () => {
|
|
@@ -4287,7 +5822,7 @@ function startWatchdog(deps) {
|
|
|
4287
5822
|
}
|
|
4288
5823
|
selfPingBusy = true;
|
|
4289
5824
|
try {
|
|
4290
|
-
const result = await client.ping.pingAgent(
|
|
5825
|
+
const result = await client.ping.pingAgent(identity2.publicKey, selfPingTimeoutMs);
|
|
4291
5826
|
if (stopped || result.online) {
|
|
4292
5827
|
return;
|
|
4293
5828
|
}
|
|
@@ -4775,18 +6310,18 @@ async function capChallengeResponse(response) {
|
|
|
4775
6310
|
});
|
|
4776
6311
|
}
|
|
4777
6312
|
function abortableDelay(ms, signal) {
|
|
4778
|
-
return new Promise((
|
|
6313
|
+
return new Promise((resolve4) => {
|
|
4779
6314
|
if (signal?.aborted) {
|
|
4780
|
-
|
|
6315
|
+
resolve4(false);
|
|
4781
6316
|
return;
|
|
4782
6317
|
}
|
|
4783
6318
|
const onAbort = () => {
|
|
4784
6319
|
clearTimeout(timer);
|
|
4785
|
-
|
|
6320
|
+
resolve4(false);
|
|
4786
6321
|
};
|
|
4787
6322
|
const timer = setTimeout(() => {
|
|
4788
6323
|
signal?.removeEventListener("abort", onAbort);
|
|
4789
|
-
|
|
6324
|
+
resolve4(true);
|
|
4790
6325
|
}, ms);
|
|
4791
6326
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4792
6327
|
});
|
|
@@ -5158,6 +6693,9 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5158
6693
|
}
|
|
5159
6694
|
const scriptEnv = { ...process.env };
|
|
5160
6695
|
delete scriptEnv.ELISYM_PASSPHRASE;
|
|
6696
|
+
if (scriptEnv.SOLANA_RPC_URL !== void 0 && !isPublicSolanaRpcUrl(scriptEnv.SOLANA_RPC_URL)) {
|
|
6697
|
+
delete scriptEnv.SOLANA_RPC_URL;
|
|
6698
|
+
}
|
|
5161
6699
|
const llmKeys = loaded.secrets.llm_api_keys ?? {};
|
|
5162
6700
|
for (const descriptor of listLlmProviders()) {
|
|
5163
6701
|
const secretValue = llmKeys[descriptor.id];
|
|
@@ -5412,7 +6950,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5412
6950
|
});
|
|
5413
6951
|
}
|
|
5414
6952
|
console.log(" Connecting to relays and publishing capabilities...");
|
|
5415
|
-
const
|
|
6953
|
+
const identity2 = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
|
|
5416
6954
|
const relays = loaded.yaml.relays.length > 0 ? loaded.yaml.relays : [...RELAYS];
|
|
5417
6955
|
const client = new ElisymClient({ relays });
|
|
5418
6956
|
if (process.env.ELISYM_NET_DIAG === "1") {
|
|
@@ -5434,7 +6972,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5434
6972
|
loaded.dir,
|
|
5435
6973
|
mediaCache,
|
|
5436
6974
|
client.blossom,
|
|
5437
|
-
|
|
6975
|
+
identity2,
|
|
5438
6976
|
(updated) => mediaCacheDirty = mediaCacheDirty || updated
|
|
5439
6977
|
);
|
|
5440
6978
|
const bannerUrl = await resolveMediaField(
|
|
@@ -5442,7 +6980,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5442
6980
|
loaded.dir,
|
|
5443
6981
|
mediaCache,
|
|
5444
6982
|
client.blossom,
|
|
5445
|
-
|
|
6983
|
+
identity2,
|
|
5446
6984
|
(updated) => mediaCacheDirty = mediaCacheDirty || updated
|
|
5447
6985
|
);
|
|
5448
6986
|
for (const skill of allSkills) {
|
|
@@ -5456,9 +6994,10 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5456
6994
|
const url = await uploadOrReuse(
|
|
5457
6995
|
cacheKey,
|
|
5458
6996
|
absPath,
|
|
6997
|
+
skillRoot,
|
|
5459
6998
|
mediaCache,
|
|
5460
6999
|
client.blossom,
|
|
5461
|
-
|
|
7000
|
+
identity2,
|
|
5462
7001
|
() => mediaCacheDirty = true
|
|
5463
7002
|
);
|
|
5464
7003
|
if (url) {
|
|
@@ -5471,11 +7010,12 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5471
7010
|
let profilePublished = false;
|
|
5472
7011
|
try {
|
|
5473
7012
|
await client.discovery.publishProfile(
|
|
5474
|
-
|
|
7013
|
+
identity2,
|
|
5475
7014
|
loaded.yaml.display_name ?? agentName,
|
|
5476
7015
|
loaded.yaml.description ?? "",
|
|
5477
7016
|
pictureUrl,
|
|
5478
|
-
bannerUrl
|
|
7017
|
+
bannerUrl,
|
|
7018
|
+
loaded.yaml.identities?.website
|
|
5479
7019
|
);
|
|
5480
7020
|
profilePublished = true;
|
|
5481
7021
|
logger.debug({ event: "publish_ack", kind: 0 }, "profile published");
|
|
@@ -5483,6 +7023,32 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5483
7023
|
console.warn(` ! Failed to publish profile: ${e.message}`);
|
|
5484
7024
|
logger.warn({ event: "publish_failed", kind: 0, error: e.message }, "profile publish failed");
|
|
5485
7025
|
}
|
|
7026
|
+
if (loaded.yaml.identities !== void 0) {
|
|
7027
|
+
const identityClaims = buildIdentityClaimInputs(loaded.yaml.identities);
|
|
7028
|
+
try {
|
|
7029
|
+
await client.discovery.publishExternalIdentities(identity2, identityClaims);
|
|
7030
|
+
const summaryParts = identityClaims.map((claim) => `${claim.platform}:${claim.handle}`);
|
|
7031
|
+
if (loaded.yaml.identities.website !== void 0) {
|
|
7032
|
+
summaryParts.push(`website:${loaded.yaml.identities.website}`);
|
|
7033
|
+
}
|
|
7034
|
+
console.log(` * Identities: ${summaryParts.join(", ") || "none"}`);
|
|
7035
|
+
logger.debug(
|
|
7036
|
+
{ event: "publish_ack", kind: KIND_EXTERNAL_IDENTITIES, claims: identityClaims.length },
|
|
7037
|
+
"identity claims published"
|
|
7038
|
+
);
|
|
7039
|
+
} catch (e) {
|
|
7040
|
+
console.warn(` ! Failed to publish identity claims: ${e.message}`);
|
|
7041
|
+
logger.warn(
|
|
7042
|
+
{ event: "publish_failed", kind: KIND_EXTERNAL_IDENTITIES, error: e.message },
|
|
7043
|
+
"identity claims publish failed"
|
|
7044
|
+
);
|
|
7045
|
+
}
|
|
7046
|
+
} else {
|
|
7047
|
+
const sweptIdentities = await sweepStaleIdentityClaims(client.discovery, identity2);
|
|
7048
|
+
if (sweptIdentities) {
|
|
7049
|
+
console.log(" Removed stale identity claims (no identities in elisym.yaml).");
|
|
7050
|
+
}
|
|
7051
|
+
}
|
|
5486
7052
|
const policies = existsSync(paths.policies) ? loadPoliciesFromDir(paths.policies) : [];
|
|
5487
7053
|
const localPolicyDTags = new Set(
|
|
5488
7054
|
policies.map((policy) => `${POLICY_D_TAG_PREFIX}${policy.type}`)
|
|
@@ -5491,7 +7057,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5491
7057
|
try {
|
|
5492
7058
|
return await client.pool.querySync({
|
|
5493
7059
|
kinds: [KIND_LONG_FORM_ARTICLE],
|
|
5494
|
-
authors: [
|
|
7060
|
+
authors: [identity2.publicKey],
|
|
5495
7061
|
"#t": [POLICY_T_TAG]
|
|
5496
7062
|
});
|
|
5497
7063
|
} catch {
|
|
@@ -5518,7 +7084,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5518
7084
|
continue;
|
|
5519
7085
|
}
|
|
5520
7086
|
try {
|
|
5521
|
-
const { naddr } = await client.policies.publishPolicy(
|
|
7087
|
+
const { naddr } = await client.policies.publishPolicy(identity2, policy);
|
|
5522
7088
|
console.log(` * Policy: ${policy.type}@${policy.version} -> ${naddr}`);
|
|
5523
7089
|
logger.debug(
|
|
5524
7090
|
{ event: "publish_ack", kind: KIND_LONG_FORM_ARTICLE, policy: policy.type, naddr },
|
|
@@ -5546,12 +7112,12 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5546
7112
|
continue;
|
|
5547
7113
|
}
|
|
5548
7114
|
const type = event.tags.find((tag) => tag[0] === "policy_type")?.[1];
|
|
5549
|
-
if (!type) {
|
|
7115
|
+
if (!type || dTag !== `${POLICY_D_TAG_PREFIX}${type}`) {
|
|
5550
7116
|
continue;
|
|
5551
7117
|
}
|
|
5552
7118
|
try {
|
|
5553
|
-
await client.policies.deletePolicy(
|
|
5554
|
-
console.log(` Removed stale policy: ${type}`);
|
|
7119
|
+
await client.policies.deletePolicy(identity2, type);
|
|
7120
|
+
console.log(` Removed stale policy: ${sanitizeForTerminal(type)}`);
|
|
5555
7121
|
} catch {
|
|
5556
7122
|
}
|
|
5557
7123
|
}
|
|
@@ -5569,6 +7135,9 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5569
7135
|
...skill.inputMime ? { inputMime: skill.inputMime } : {},
|
|
5570
7136
|
...skill.inputText ? { inputText: skill.inputText } : {},
|
|
5571
7137
|
...skill.outputMime ? { outputMime: skill.outputMime } : {},
|
|
7138
|
+
// Conversation-context flag: clients gate chat affordances (session-carrying
|
|
7139
|
+
// sends) on it. Strict true only - the read side coerces anything else away.
|
|
7140
|
+
...skill.context === true ? { context: true } : {},
|
|
5572
7141
|
payment: solanaAddress ? {
|
|
5573
7142
|
chain: "solana",
|
|
5574
7143
|
network: walletNetwork,
|
|
@@ -5610,7 +7179,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5610
7179
|
continue;
|
|
5611
7180
|
}
|
|
5612
7181
|
try {
|
|
5613
|
-
await client.discovery.publishCapability(
|
|
7182
|
+
await client.discovery.publishCapability(identity2, buildCard(skill), kinds);
|
|
5614
7183
|
cardsPublished += 1;
|
|
5615
7184
|
logger.debug(
|
|
5616
7185
|
{ event: "publish_ack", kind: 31990, skill: skill.name },
|
|
@@ -5627,7 +7196,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5627
7196
|
try {
|
|
5628
7197
|
const existingEvents = await client.pool.querySync({
|
|
5629
7198
|
kinds: [31990],
|
|
5630
|
-
authors: [
|
|
7199
|
+
authors: [identity2.publicKey],
|
|
5631
7200
|
"#t": ["elisym"]
|
|
5632
7201
|
});
|
|
5633
7202
|
const skillDTags = new Set(allSkills.map((s) => toDTag(s.name)));
|
|
@@ -5638,9 +7207,9 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5638
7207
|
}
|
|
5639
7208
|
try {
|
|
5640
7209
|
const card = JSON.parse(ev.content);
|
|
5641
|
-
if (card.name) {
|
|
5642
|
-
await client.discovery.deleteCapability(
|
|
5643
|
-
console.log(` Removed stale capability: ${card.name}`);
|
|
7210
|
+
if (card.name && toDTag(card.name) === dTag) {
|
|
7211
|
+
await client.discovery.deleteCapability(identity2, card.name);
|
|
7212
|
+
console.log(` Removed stale capability: ${sanitizeForTerminal(card.name)}`);
|
|
5644
7213
|
}
|
|
5645
7214
|
} catch {
|
|
5646
7215
|
}
|
|
@@ -5655,16 +7224,20 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5655
7224
|
console.log(` [debug] Relays: ${relays.length} configured (${relays.join(", ")})`);
|
|
5656
7225
|
}
|
|
5657
7226
|
const onPing = (senderPubkey, nonce) => {
|
|
5658
|
-
client.ping.sendPong(
|
|
7227
|
+
client.ping.sendPong(identity2, senderPubkey, nonce).catch(() => {
|
|
5659
7228
|
});
|
|
5660
7229
|
};
|
|
5661
|
-
const transport = new NostrTransport(client,
|
|
7230
|
+
const transport = new NostrTransport(client, identity2, [DEFAULT_KIND_OFFSET]);
|
|
5662
7231
|
const ledger = new JobLedger(paths.jobs);
|
|
5663
7232
|
const irohTransport = createIrohTransport({ storePath: join(loaded.dir, ".iroh") });
|
|
5664
7233
|
await ensureGitignoreHasIrohEntry(dirname(loaded.dir));
|
|
5665
7234
|
if (x402Skills.length > 0) {
|
|
5666
7235
|
await ensureGitignoreHasX402Entries(dirname(loaded.dir));
|
|
5667
7236
|
}
|
|
7237
|
+
const hasContextSkills = registry.all().some((skill) => skill.context === true);
|
|
7238
|
+
if (hasContextSkills) {
|
|
7239
|
+
await ensureGitignoreHasSessionsEntry(dirname(loaded.dir));
|
|
7240
|
+
}
|
|
5668
7241
|
const runtimeConfig = {
|
|
5669
7242
|
paymentTimeoutSecs: DEFAULTS.PAYMENT_EXPIRY_SECS,
|
|
5670
7243
|
maxConcurrentJobs: MAX_CONCURRENT_JOBS,
|
|
@@ -5695,7 +7268,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5695
7268
|
};
|
|
5696
7269
|
const watchdog = startWatchdog({
|
|
5697
7270
|
client,
|
|
5698
|
-
identity,
|
|
7271
|
+
identity: identity2,
|
|
5699
7272
|
transport,
|
|
5700
7273
|
onPing,
|
|
5701
7274
|
log: diagLog,
|
|
@@ -5709,7 +7282,11 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5709
7282
|
});
|
|
5710
7283
|
diagLog("LLM health monitor armed (lazy recovery, 5min interval).");
|
|
5711
7284
|
}
|
|
5712
|
-
const blossomTransport = createBlossomTransport({ blossom: client.blossom, identity });
|
|
7285
|
+
const blossomTransport = createBlossomTransport({ blossom: client.blossom, identity: identity2 });
|
|
7286
|
+
const sessionStore = hasContextSkills ? new SessionStore(loaded.dir, diagLog) : void 0;
|
|
7287
|
+
if (sessionStore) {
|
|
7288
|
+
diagLog("Conversation sessions enabled (context: true skills present).");
|
|
7289
|
+
}
|
|
5713
7290
|
const runtime = new AgentRuntime(
|
|
5714
7291
|
transport,
|
|
5715
7292
|
registry,
|
|
@@ -5742,8 +7319,9 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
5742
7319
|
},
|
|
5743
7320
|
healthMonitor,
|
|
5744
7321
|
irohTransport,
|
|
5745
|
-
|
|
5746
|
-
blossomTransport
|
|
7322
|
+
identity2,
|
|
7323
|
+
blossomTransport,
|
|
7324
|
+
sessionStore
|
|
5747
7325
|
);
|
|
5748
7326
|
console.log(" * Running. Press Ctrl+C to stop.\n");
|
|
5749
7327
|
await runtime.run();
|
|
@@ -5768,10 +7346,18 @@ function stripRpcSecrets(raw) {
|
|
|
5768
7346
|
return "[unparseable RPC URL]";
|
|
5769
7347
|
}
|
|
5770
7348
|
}
|
|
7349
|
+
function isPublicSolanaRpcUrl(raw) {
|
|
7350
|
+
try {
|
|
7351
|
+
const parsed = new URL(raw);
|
|
7352
|
+
return PUBLIC_SOLANA_RPC_HOSTS.has(parsed.hostname) && !parsed.username && !parsed.password && !parsed.search && (parsed.pathname === "" || parsed.pathname === "/");
|
|
7353
|
+
} catch {
|
|
7354
|
+
return false;
|
|
7355
|
+
}
|
|
7356
|
+
}
|
|
5771
7357
|
function redactRpcUrlsInText(text) {
|
|
5772
7358
|
return text.replace(/https?:\/\/[^\s)'"]+/g, (url) => stripRpcSecrets(url));
|
|
5773
7359
|
}
|
|
5774
|
-
async function resolveMediaField(value, agentDir, cache, blossom,
|
|
7360
|
+
async function resolveMediaField(value, agentDir, cache, blossom, identity2, onCacheUpdate) {
|
|
5775
7361
|
if (!value) {
|
|
5776
7362
|
return void 0;
|
|
5777
7363
|
}
|
|
@@ -5783,7 +7369,15 @@ async function resolveMediaField(value, agentDir, cache, blossom, identity, onCa
|
|
|
5783
7369
|
console.warn(` ! Skipping media field "${value}": path must stay inside the agent directory.`);
|
|
5784
7370
|
return void 0;
|
|
5785
7371
|
}
|
|
5786
|
-
return uploadOrReuse(
|
|
7372
|
+
return uploadOrReuse(
|
|
7373
|
+
value,
|
|
7374
|
+
absPath,
|
|
7375
|
+
agentDir,
|
|
7376
|
+
cache,
|
|
7377
|
+
blossom,
|
|
7378
|
+
identity2,
|
|
7379
|
+
() => onCacheUpdate(true)
|
|
7380
|
+
);
|
|
5787
7381
|
}
|
|
5788
7382
|
function resolveInsideAgentDir(value, agentDir) {
|
|
5789
7383
|
const agentRoot = resolve(agentDir);
|
|
@@ -5794,17 +7388,37 @@ function resolveInsideAgentDir(value, agentDir) {
|
|
|
5794
7388
|
}
|
|
5795
7389
|
return candidate;
|
|
5796
7390
|
}
|
|
5797
|
-
|
|
7391
|
+
function realpathInsideDir(absPath, rootDir) {
|
|
7392
|
+
try {
|
|
7393
|
+
const realRoot = realpathSync(resolve(rootDir));
|
|
7394
|
+
const realCandidate = realpathSync(absPath);
|
|
7395
|
+
const rel = relative(realRoot, realCandidate);
|
|
7396
|
+
if (rel === "" || rel.startsWith("..") || rel.includes(`..${sep}`)) {
|
|
7397
|
+
return null;
|
|
7398
|
+
}
|
|
7399
|
+
return realCandidate;
|
|
7400
|
+
} catch {
|
|
7401
|
+
return null;
|
|
7402
|
+
}
|
|
7403
|
+
}
|
|
7404
|
+
async function uploadOrReuse(cacheKey, absPath, containRoot, cache, blossom, identity2, onCacheUpdate) {
|
|
7405
|
+
const realPath = realpathInsideDir(absPath, containRoot);
|
|
7406
|
+
if (realPath === null) {
|
|
7407
|
+
console.warn(
|
|
7408
|
+
` ! Skipping upload of ${basename(absPath)}: unreadable or escapes its directory.`
|
|
7409
|
+
);
|
|
7410
|
+
return void 0;
|
|
7411
|
+
}
|
|
5798
7412
|
try {
|
|
5799
|
-
const cached = await lookupCachedUrl(cache, cacheKey,
|
|
7413
|
+
const cached = await lookupCachedUrl(cache, cacheKey, realPath);
|
|
5800
7414
|
if (cached) {
|
|
5801
7415
|
return cached;
|
|
5802
7416
|
}
|
|
5803
|
-
console.log(` Uploading ${basename(
|
|
5804
|
-
const data = readFileSync(
|
|
7417
|
+
console.log(` Uploading ${basename(realPath)}...`);
|
|
7418
|
+
const data = readFileSync(realPath);
|
|
5805
7419
|
const sha256 = createHash("sha256").update(data).digest("hex");
|
|
5806
7420
|
const blob = new Blob([data], { type: mimeFromPath(absPath) });
|
|
5807
|
-
const descriptor = await blossom.upload(
|
|
7421
|
+
const descriptor = await blossom.upload(identity2, blob);
|
|
5808
7422
|
cache[cacheKey] = newCacheEntry(descriptor.url, sha256);
|
|
5809
7423
|
onCacheUpdate();
|
|
5810
7424
|
console.log(` Uploaded: ${descriptor.url}`);
|
|
@@ -6411,8 +8025,8 @@ program.command("list").description("List all agents (project-local and home-glo
|
|
|
6411
8025
|
for (const agent of agents) {
|
|
6412
8026
|
try {
|
|
6413
8027
|
const loaded = await loadAgent(agent.name, cwd);
|
|
6414
|
-
const
|
|
6415
|
-
const npub = nip19.npubEncode(
|
|
8028
|
+
const identity2 = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
|
|
8029
|
+
const npub = nip19.npubEncode(identity2.publicKey);
|
|
6416
8030
|
const solAddr = loaded.yaml.payments[0]?.address ? ` | Solana: ${loaded.yaml.payments[0].address}` : "";
|
|
6417
8031
|
const shadow = agent.shadowsGlobal ? " [shadows global]" : "";
|
|
6418
8032
|
console.log(` ${agent.name} (${agent.source})${shadow} | ${npub}${solAddr}`);
|
|
@@ -6425,6 +8039,29 @@ program.command("list").description("List all agents (project-local and home-glo
|
|
|
6425
8039
|
})
|
|
6426
8040
|
);
|
|
6427
8041
|
program.command("wallet [name]").description("Show wallet balance").action(safe(cmdWallet));
|
|
8042
|
+
var identity = program.command("identity").description("Link, verify, and unlink external identities (GitHub, X, website)");
|
|
8043
|
+
identity.command("link <platform> [agent]").description(
|
|
8044
|
+
"Link a github|x|website identity: prints the NIP-39 proof template, verifies the proof live, writes elisym.yaml, and publishes the claim (kind 10011 / kind-0 nip05)"
|
|
8045
|
+
).action(
|
|
8046
|
+
safe(async (platform, agent) => {
|
|
8047
|
+
await cmdIdentityLink(platform, agent);
|
|
8048
|
+
process.exit(0);
|
|
8049
|
+
})
|
|
8050
|
+
);
|
|
8051
|
+
identity.command("status [agent]").description(
|
|
8052
|
+
"List linked identities with live proof verification and published-claim drift check"
|
|
8053
|
+
).action(
|
|
8054
|
+
safe(async (agent) => {
|
|
8055
|
+
await cmdIdentityStatus(agent);
|
|
8056
|
+
process.exit(0);
|
|
8057
|
+
})
|
|
8058
|
+
);
|
|
8059
|
+
identity.command("unlink <platform> [agent]").description("Remove a github|x|website identity and retract the published claim").action(
|
|
8060
|
+
safe(async (platform, agent) => {
|
|
8061
|
+
await cmdIdentityUnlink(platform, agent);
|
|
8062
|
+
process.exit(0);
|
|
8063
|
+
})
|
|
8064
|
+
);
|
|
6428
8065
|
var x402 = program.command("x402").description("Bridge x402-paid HTTP services into elisym skills");
|
|
6429
8066
|
x402.command("add <url> [agent]").description("Generate a bridge skill from a live x402 endpoint (probes its 402 challenge)").option("--method <method>", "HTTP method for the upstream call: GET or POST (default POST)").option(
|
|
6430
8067
|
"--query-param <name>",
|