@elisym/cli 0.24.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { ReadableStream } from 'node:stream/web';
3
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, SESSION_ID_REGEX, createSlidingWindowLimiter, utf8ByteLength, readAcceptedTransports, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, encodeSecretKeyBase58, NATIVE_SOL } from '@elisym/sdk';
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
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';
@@ -52,9 +52,9 @@ function sleepWithSignal(ms, signal) {
52
52
  return Promise.reject(createAbortError());
53
53
  }
54
54
  if (!signal) {
55
- return new Promise((resolve3) => setTimeout(resolve3, ms));
55
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
56
56
  }
57
- return new Promise((resolve3, reject) => {
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
- resolve3();
68
+ resolve4();
69
69
  }, ms);
70
70
  signal.addEventListener("abort", onAbort, { once: true });
71
71
  });
@@ -1442,6 +1442,538 @@ var init_init = __esm({
1442
1442
  init_llm();
1443
1443
  }
1444
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
+ }
1445
1977
 
1446
1978
  // src/index.ts
1447
1979
  init_init();
@@ -1696,7 +2228,7 @@ function parseRelayUrl(url) {
1696
2228
  }
1697
2229
  }
1698
2230
  async function tcpProbe(host, port, timeoutMs) {
1699
- return new Promise((resolve3) => {
2231
+ return new Promise((resolve4) => {
1700
2232
  const started = Date.now();
1701
2233
  const socket = new Socket();
1702
2234
  let settled = false;
@@ -1706,7 +2238,7 @@ async function tcpProbe(host, port, timeoutMs) {
1706
2238
  }
1707
2239
  settled = true;
1708
2240
  socket.destroy();
1709
- resolve3(result);
2241
+ resolve4(result);
1710
2242
  };
1711
2243
  const timer = setTimeout(() => finish({ error: "timeout" }), timeoutMs);
1712
2244
  socket.once("connect", () => {
@@ -2203,8 +2735,8 @@ var SessionStore = class {
2203
2735
  }
2204
2736
  installLock(key) {
2205
2737
  let release;
2206
- const promise = new Promise((resolve3) => {
2207
- release = resolve3;
2738
+ const promise = new Promise((resolve4) => {
2739
+ release = resolve4;
2208
2740
  });
2209
2741
  const entry = { promise, release };
2210
2742
  this.locks.set(key, entry);
@@ -3008,7 +3540,7 @@ var PAID_GLOBAL_MAX_JOBS_PER_WINDOW = 2e3;
3008
3540
  var MAX_TRACKED_CUSTOMERS = 1e3;
3009
3541
  var GLOBAL_LIMITER_KEY = "__global__";
3010
3542
  var AgentRuntime = class {
3011
- constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor, irohTransport, identity, blossomTransport, sessionStore) {
3543
+ constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor, irohTransport, identity2, blossomTransport, sessionStore) {
3012
3544
  this.transport = transport;
3013
3545
  this.skills = skills;
3014
3546
  this.skillCtx = skillCtx;
@@ -3017,7 +3549,7 @@ var AgentRuntime = class {
3017
3549
  this.callbacks = callbacks;
3018
3550
  this.healthMonitor = healthMonitor;
3019
3551
  this.irohTransport = irohTransport;
3020
- this.identity = identity;
3552
+ this.identity = identity2;
3021
3553
  this.blossomTransport = blossomTransport;
3022
3554
  this.sessionStore = sessionStore;
3023
3555
  this.limit = pLimit(config.maxConcurrentJobs);
@@ -3410,8 +3942,8 @@ var AgentRuntime = class {
3410
3942
  });
3411
3943
  });
3412
3944
  log("Agent runtime started. Listening for jobs...");
3413
- await new Promise((resolve3) => {
3414
- this.abortController.signal.addEventListener("abort", () => resolve3(), { once: true });
3945
+ await new Promise((resolve4) => {
3946
+ this.abortController.signal.addEventListener("abort", () => resolve4(), { once: true });
3415
3947
  let shuttingDown = false;
3416
3948
  const onSignal = () => {
3417
3949
  if (shuttingDown) {
@@ -4055,7 +4587,7 @@ var AgentRuntime = class {
4055
4587
  let refOutcome;
4056
4588
  let result;
4057
4589
  try {
4058
- result = await new Promise((resolve3, reject) => {
4590
+ result = await new Promise((resolve4, reject) => {
4059
4591
  let settled = false;
4060
4592
  let pending = 2;
4061
4593
  let lastResult = { verified: false };
@@ -4065,7 +4597,7 @@ var AgentRuntime = class {
4065
4597
  }
4066
4598
  settled = true;
4067
4599
  clearTimeout(deadline);
4068
- resolve3(verified);
4600
+ resolve4(verified);
4069
4601
  };
4070
4602
  const lose = (verified, reason) => {
4071
4603
  if (settled) {
@@ -4077,7 +4609,7 @@ var AgentRuntime = class {
4077
4609
  if (pending === 0) {
4078
4610
  settled = true;
4079
4611
  clearTimeout(deadline);
4080
- resolve3(lastResult);
4612
+ resolve4(lastResult);
4081
4613
  }
4082
4614
  };
4083
4615
  this.transport.waitForPaymentSignature(job.jobId, job.customerId, verifyAbort.signal, sigPathTimeoutMs).then(async (sig) => {
@@ -4130,7 +4662,7 @@ var AgentRuntime = class {
4130
4662
  }
4131
4663
  settled = true;
4132
4664
  log(`[${job.jobId.slice(0, 8)}] verify deadline (${deadlineMs}ms) hit`);
4133
- resolve3(lastResult);
4665
+ resolve4(lastResult);
4134
4666
  }, deadlineMs);
4135
4667
  if (signal?.aborted) {
4136
4668
  settled = true;
@@ -5021,9 +5553,9 @@ function parseBidTag(value) {
5021
5553
  }
5022
5554
  var HEALTH_CHECK_IDLE_MS = 30 * 60 * 1e3;
5023
5555
  var NostrTransport = class {
5024
- constructor(client, identity, kindOffsets) {
5556
+ constructor(client, identity2, kindOffsets) {
5025
5557
  this.client = client;
5026
- this.identity = identity;
5558
+ this.identity = identity2;
5027
5559
  this.kindOffsets = kindOffsets;
5028
5560
  }
5029
5561
  sub = null;
@@ -5101,7 +5633,7 @@ var NostrTransport = class {
5101
5633
  * expiry window - without it this path waits forever on the relay.
5102
5634
  */
5103
5635
  waitForPaymentSignature(jobId, customerPubkey, signal, timeoutMs) {
5104
- return new Promise((resolve3) => {
5636
+ return new Promise((resolve4) => {
5105
5637
  let settled = false;
5106
5638
  const filter = {
5107
5639
  kinds: [KIND_JOB_FEEDBACK],
@@ -5120,7 +5652,7 @@ var NostrTransport = class {
5120
5652
  }
5121
5653
  signal.removeEventListener("abort", onAbort);
5122
5654
  sub.close();
5123
- resolve3(sig);
5655
+ resolve4(sig);
5124
5656
  };
5125
5657
  const sub = this.client.pool.subscribe(filter, (event) => {
5126
5658
  if (settled) {
@@ -5214,7 +5746,7 @@ var NostrTransport = class {
5214
5746
  function startWatchdog(deps) {
5215
5747
  const {
5216
5748
  client,
5217
- identity,
5749
+ identity: identity2,
5218
5750
  transport,
5219
5751
  onPing,
5220
5752
  log,
@@ -5225,7 +5757,7 @@ function startWatchdog(deps) {
5225
5757
  selfPingTimeoutMs = WATCHDOG_SELF_PING_TIMEOUT_MS,
5226
5758
  now = Date.now
5227
5759
  } = deps;
5228
- let pingSub = client.ping.subscribeToPings(identity, onPing);
5760
+ let pingSub = client.ping.subscribeToPings(identity2, onPing);
5229
5761
  let stopped = false;
5230
5762
  let probeBusy = false;
5231
5763
  let selfPingBusy = false;
@@ -5234,7 +5766,7 @@ function startWatchdog(deps) {
5234
5766
  const resetPoolAndResubscribe = () => {
5235
5767
  transport.stop();
5236
5768
  client.pool.reset();
5237
- pingSub = client.ping.subscribeToPings(identity, onPing);
5769
+ pingSub = client.ping.subscribeToPings(identity2, onPing);
5238
5770
  transport.restart();
5239
5771
  };
5240
5772
  const detectAndHandleSleep = () => {
@@ -5290,7 +5822,7 @@ function startWatchdog(deps) {
5290
5822
  }
5291
5823
  selfPingBusy = true;
5292
5824
  try {
5293
- const result = await client.ping.pingAgent(identity.publicKey, selfPingTimeoutMs);
5825
+ const result = await client.ping.pingAgent(identity2.publicKey, selfPingTimeoutMs);
5294
5826
  if (stopped || result.online) {
5295
5827
  return;
5296
5828
  }
@@ -5778,18 +6310,18 @@ async function capChallengeResponse(response) {
5778
6310
  });
5779
6311
  }
5780
6312
  function abortableDelay(ms, signal) {
5781
- return new Promise((resolve3) => {
6313
+ return new Promise((resolve4) => {
5782
6314
  if (signal?.aborted) {
5783
- resolve3(false);
6315
+ resolve4(false);
5784
6316
  return;
5785
6317
  }
5786
6318
  const onAbort = () => {
5787
6319
  clearTimeout(timer);
5788
- resolve3(false);
6320
+ resolve4(false);
5789
6321
  };
5790
6322
  const timer = setTimeout(() => {
5791
6323
  signal?.removeEventListener("abort", onAbort);
5792
- resolve3(true);
6324
+ resolve4(true);
5793
6325
  }, ms);
5794
6326
  signal?.addEventListener("abort", onAbort, { once: true });
5795
6327
  });
@@ -6418,7 +6950,7 @@ async function cmdStart(nameArg, options = {}) {
6418
6950
  });
6419
6951
  }
6420
6952
  console.log(" Connecting to relays and publishing capabilities...");
6421
- const identity = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
6953
+ const identity2 = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
6422
6954
  const relays = loaded.yaml.relays.length > 0 ? loaded.yaml.relays : [...RELAYS];
6423
6955
  const client = new ElisymClient({ relays });
6424
6956
  if (process.env.ELISYM_NET_DIAG === "1") {
@@ -6440,7 +6972,7 @@ async function cmdStart(nameArg, options = {}) {
6440
6972
  loaded.dir,
6441
6973
  mediaCache,
6442
6974
  client.blossom,
6443
- identity,
6975
+ identity2,
6444
6976
  (updated) => mediaCacheDirty = mediaCacheDirty || updated
6445
6977
  );
6446
6978
  const bannerUrl = await resolveMediaField(
@@ -6448,7 +6980,7 @@ async function cmdStart(nameArg, options = {}) {
6448
6980
  loaded.dir,
6449
6981
  mediaCache,
6450
6982
  client.blossom,
6451
- identity,
6983
+ identity2,
6452
6984
  (updated) => mediaCacheDirty = mediaCacheDirty || updated
6453
6985
  );
6454
6986
  for (const skill of allSkills) {
@@ -6465,7 +6997,7 @@ async function cmdStart(nameArg, options = {}) {
6465
6997
  skillRoot,
6466
6998
  mediaCache,
6467
6999
  client.blossom,
6468
- identity,
7000
+ identity2,
6469
7001
  () => mediaCacheDirty = true
6470
7002
  );
6471
7003
  if (url) {
@@ -6478,11 +7010,12 @@ async function cmdStart(nameArg, options = {}) {
6478
7010
  let profilePublished = false;
6479
7011
  try {
6480
7012
  await client.discovery.publishProfile(
6481
- identity,
7013
+ identity2,
6482
7014
  loaded.yaml.display_name ?? agentName,
6483
7015
  loaded.yaml.description ?? "",
6484
7016
  pictureUrl,
6485
- bannerUrl
7017
+ bannerUrl,
7018
+ loaded.yaml.identities?.website
6486
7019
  );
6487
7020
  profilePublished = true;
6488
7021
  logger.debug({ event: "publish_ack", kind: 0 }, "profile published");
@@ -6490,6 +7023,32 @@ async function cmdStart(nameArg, options = {}) {
6490
7023
  console.warn(` ! Failed to publish profile: ${e.message}`);
6491
7024
  logger.warn({ event: "publish_failed", kind: 0, error: e.message }, "profile publish failed");
6492
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
+ }
6493
7052
  const policies = existsSync(paths.policies) ? loadPoliciesFromDir(paths.policies) : [];
6494
7053
  const localPolicyDTags = new Set(
6495
7054
  policies.map((policy) => `${POLICY_D_TAG_PREFIX}${policy.type}`)
@@ -6498,7 +7057,7 @@ async function cmdStart(nameArg, options = {}) {
6498
7057
  try {
6499
7058
  return await client.pool.querySync({
6500
7059
  kinds: [KIND_LONG_FORM_ARTICLE],
6501
- authors: [identity.publicKey],
7060
+ authors: [identity2.publicKey],
6502
7061
  "#t": [POLICY_T_TAG]
6503
7062
  });
6504
7063
  } catch {
@@ -6525,7 +7084,7 @@ async function cmdStart(nameArg, options = {}) {
6525
7084
  continue;
6526
7085
  }
6527
7086
  try {
6528
- const { naddr } = await client.policies.publishPolicy(identity, policy);
7087
+ const { naddr } = await client.policies.publishPolicy(identity2, policy);
6529
7088
  console.log(` * Policy: ${policy.type}@${policy.version} -> ${naddr}`);
6530
7089
  logger.debug(
6531
7090
  { event: "publish_ack", kind: KIND_LONG_FORM_ARTICLE, policy: policy.type, naddr },
@@ -6557,7 +7116,7 @@ async function cmdStart(nameArg, options = {}) {
6557
7116
  continue;
6558
7117
  }
6559
7118
  try {
6560
- await client.policies.deletePolicy(identity, type);
7119
+ await client.policies.deletePolicy(identity2, type);
6561
7120
  console.log(` Removed stale policy: ${sanitizeForTerminal(type)}`);
6562
7121
  } catch {
6563
7122
  }
@@ -6620,7 +7179,7 @@ async function cmdStart(nameArg, options = {}) {
6620
7179
  continue;
6621
7180
  }
6622
7181
  try {
6623
- await client.discovery.publishCapability(identity, buildCard(skill), kinds);
7182
+ await client.discovery.publishCapability(identity2, buildCard(skill), kinds);
6624
7183
  cardsPublished += 1;
6625
7184
  logger.debug(
6626
7185
  { event: "publish_ack", kind: 31990, skill: skill.name },
@@ -6637,7 +7196,7 @@ async function cmdStart(nameArg, options = {}) {
6637
7196
  try {
6638
7197
  const existingEvents = await client.pool.querySync({
6639
7198
  kinds: [31990],
6640
- authors: [identity.publicKey],
7199
+ authors: [identity2.publicKey],
6641
7200
  "#t": ["elisym"]
6642
7201
  });
6643
7202
  const skillDTags = new Set(allSkills.map((s) => toDTag(s.name)));
@@ -6649,7 +7208,7 @@ async function cmdStart(nameArg, options = {}) {
6649
7208
  try {
6650
7209
  const card = JSON.parse(ev.content);
6651
7210
  if (card.name && toDTag(card.name) === dTag) {
6652
- await client.discovery.deleteCapability(identity, card.name);
7211
+ await client.discovery.deleteCapability(identity2, card.name);
6653
7212
  console.log(` Removed stale capability: ${sanitizeForTerminal(card.name)}`);
6654
7213
  }
6655
7214
  } catch {
@@ -6665,10 +7224,10 @@ async function cmdStart(nameArg, options = {}) {
6665
7224
  console.log(` [debug] Relays: ${relays.length} configured (${relays.join(", ")})`);
6666
7225
  }
6667
7226
  const onPing = (senderPubkey, nonce) => {
6668
- client.ping.sendPong(identity, senderPubkey, nonce).catch(() => {
7227
+ client.ping.sendPong(identity2, senderPubkey, nonce).catch(() => {
6669
7228
  });
6670
7229
  };
6671
- const transport = new NostrTransport(client, identity, [DEFAULT_KIND_OFFSET]);
7230
+ const transport = new NostrTransport(client, identity2, [DEFAULT_KIND_OFFSET]);
6672
7231
  const ledger = new JobLedger(paths.jobs);
6673
7232
  const irohTransport = createIrohTransport({ storePath: join(loaded.dir, ".iroh") });
6674
7233
  await ensureGitignoreHasIrohEntry(dirname(loaded.dir));
@@ -6709,7 +7268,7 @@ async function cmdStart(nameArg, options = {}) {
6709
7268
  };
6710
7269
  const watchdog = startWatchdog({
6711
7270
  client,
6712
- identity,
7271
+ identity: identity2,
6713
7272
  transport,
6714
7273
  onPing,
6715
7274
  log: diagLog,
@@ -6723,7 +7282,7 @@ async function cmdStart(nameArg, options = {}) {
6723
7282
  });
6724
7283
  diagLog("LLM health monitor armed (lazy recovery, 5min interval).");
6725
7284
  }
6726
- const blossomTransport = createBlossomTransport({ blossom: client.blossom, identity });
7285
+ const blossomTransport = createBlossomTransport({ blossom: client.blossom, identity: identity2 });
6727
7286
  const sessionStore = hasContextSkills ? new SessionStore(loaded.dir, diagLog) : void 0;
6728
7287
  if (sessionStore) {
6729
7288
  diagLog("Conversation sessions enabled (context: true skills present).");
@@ -6760,7 +7319,7 @@ async function cmdStart(nameArg, options = {}) {
6760
7319
  },
6761
7320
  healthMonitor,
6762
7321
  irohTransport,
6763
- identity,
7322
+ identity2,
6764
7323
  blossomTransport,
6765
7324
  sessionStore
6766
7325
  );
@@ -6798,7 +7357,7 @@ function isPublicSolanaRpcUrl(raw) {
6798
7357
  function redactRpcUrlsInText(text) {
6799
7358
  return text.replace(/https?:\/\/[^\s)'"]+/g, (url) => stripRpcSecrets(url));
6800
7359
  }
6801
- async function resolveMediaField(value, agentDir, cache, blossom, identity, onCacheUpdate) {
7360
+ async function resolveMediaField(value, agentDir, cache, blossom, identity2, onCacheUpdate) {
6802
7361
  if (!value) {
6803
7362
  return void 0;
6804
7363
  }
@@ -6816,7 +7375,7 @@ async function resolveMediaField(value, agentDir, cache, blossom, identity, onCa
6816
7375
  agentDir,
6817
7376
  cache,
6818
7377
  blossom,
6819
- identity,
7378
+ identity2,
6820
7379
  () => onCacheUpdate(true)
6821
7380
  );
6822
7381
  }
@@ -6842,7 +7401,7 @@ function realpathInsideDir(absPath, rootDir) {
6842
7401
  return null;
6843
7402
  }
6844
7403
  }
6845
- async function uploadOrReuse(cacheKey, absPath, containRoot, cache, blossom, identity, onCacheUpdate) {
7404
+ async function uploadOrReuse(cacheKey, absPath, containRoot, cache, blossom, identity2, onCacheUpdate) {
6846
7405
  const realPath = realpathInsideDir(absPath, containRoot);
6847
7406
  if (realPath === null) {
6848
7407
  console.warn(
@@ -6859,7 +7418,7 @@ async function uploadOrReuse(cacheKey, absPath, containRoot, cache, blossom, ide
6859
7418
  const data = readFileSync(realPath);
6860
7419
  const sha256 = createHash("sha256").update(data).digest("hex");
6861
7420
  const blob = new Blob([data], { type: mimeFromPath(absPath) });
6862
- const descriptor = await blossom.upload(identity, blob);
7421
+ const descriptor = await blossom.upload(identity2, blob);
6863
7422
  cache[cacheKey] = newCacheEntry(descriptor.url, sha256);
6864
7423
  onCacheUpdate();
6865
7424
  console.log(` Uploaded: ${descriptor.url}`);
@@ -7466,8 +8025,8 @@ program.command("list").description("List all agents (project-local and home-glo
7466
8025
  for (const agent of agents) {
7467
8026
  try {
7468
8027
  const loaded = await loadAgent(agent.name, cwd);
7469
- const identity = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
7470
- const npub = nip19.npubEncode(identity.publicKey);
8028
+ const identity2 = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
8029
+ const npub = nip19.npubEncode(identity2.publicKey);
7471
8030
  const solAddr = loaded.yaml.payments[0]?.address ? ` | Solana: ${loaded.yaml.payments[0].address}` : "";
7472
8031
  const shadow = agent.shadowsGlobal ? " [shadows global]" : "";
7473
8032
  console.log(` ${agent.name} (${agent.source})${shadow} | ${npub}${solAddr}`);
@@ -7480,6 +8039,29 @@ program.command("list").description("List all agents (project-local and home-glo
7480
8039
  })
7481
8040
  );
7482
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
+ );
7483
8065
  var x402 = program.command("x402").description("Bridge x402-paid HTTP services into elisym skills");
7484
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(
7485
8067
  "--query-param <name>",