This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

Add island/connection CLI commands for graph analysis

- island:discover: BFS to find connected components in global network.cosmik.connection graph, caches in SQLite
- island:detect: match our carry graph against discovered components to find closest island
- island:explore: find fringe nodes and suggest bridge edges back into island
- connection:detect: compare our carry graph against individual DIDs' connection records
- vault:analyze: parse Obsidian vault notes into carry data via letta-code-sdk agent

Dependencies: @atproto/api, @letta-ai/letta-code-sdk, jsdom

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>

author
nandi
co-author
Letta Code
date (May 15, 2026, 4:11 AM UTC) commit 6111c633 parent 9d554df6
+2683 -28
+10 -2
package.json
··· 7 7 "build:frontend": "bash build-frontend.sh", 8 8 "dev": "pnpm build:frontend && wrangler dev", 9 9 "deploy": "pnpm build:frontend && wrangler deploy", 10 - "typecheck": "tsc --noEmit" 10 + "typecheck": "tsc --noEmit", 11 + "island:explore": "bun run src/island-explore.ts", 12 + "island:detect": "bun run src/island-detect.ts", 13 + "island:discover": "bun run src/island-discover.ts", 14 + "connection:detect": "bun run src/connection-detect.ts", 15 + "vault:analyze": "bun run src/vault-analyze.ts" 11 16 }, 12 17 "dependencies": { 13 18 "@atmo-dev/contrail": "^0.6.0", 14 19 "@cloudflare/containers": "^0.1.0", 15 - "hono": "^4.7.0" 20 + "hono": "^4.7.0", 21 + "@atproto/api": "^0.19.17", 22 + "@letta-ai/letta-code-sdk": "^0.1.14", 23 + "jsdom": "^29.1.1" 16 24 }, 17 25 "devDependencies": { 18 26 "@atcute/atproto": "^4.0.0",
+1056 -26
pnpm-lock.yaml
··· 10 10 dependencies: 11 11 '@atmo-dev/contrail': 12 12 specifier: ^0.6.0 13 - version: 0.6.0(wrangler@4.90.1(@cloudflare/workers-types@4.20260511.1)) 13 + version: 0.6.0(react@19.2.6)(wrangler@4.90.1(@cloudflare/workers-types@4.20260511.1)) 14 + '@atproto/api': 15 + specifier: ^0.19.17 16 + version: 0.19.18 14 17 '@cloudflare/containers': 15 18 specifier: ^0.1.0 16 19 version: 0.1.1 20 + '@letta-ai/letta-code-sdk': 21 + specifier: ^0.1.14 22 + version: 0.1.14(ink@7.0.3(react@19.2.6)) 17 23 hono: 18 24 specifier: ^4.7.0 19 25 version: 4.12.18 26 + jsdom: 27 + specifier: ^29.1.1 28 + version: 29.1.1 20 29 devDependencies: 21 30 '@atcute/atproto': 22 31 specifier: ^4.0.0 ··· 29 38 version: 2.0.0 30 39 '@atmo-dev/contrail-lexicons': 31 40 specifier: ^0.4.2 32 - version: 0.4.6(@atcute/cbor@2.3.3(@atcute/cid@2.4.1))(@atcute/cid@2.4.1)(wrangler@4.90.1(@cloudflare/workers-types@4.20260511.1)) 41 + version: 0.4.6(@atcute/cbor@2.3.3(@atcute/cid@2.4.1))(@atcute/cid@2.4.1)(react@19.2.6)(wrangler@4.90.1(@cloudflare/workers-types@4.20260511.1)) 33 42 '@atproto/oauth-client-browser': 34 43 specifier: ^0.3.42 35 44 version: 0.3.42 ··· 47 56 version: 4.90.1(@cloudflare/workers-types@4.20260511.1) 48 57 49 58 packages: 59 + 60 + '@alcalzone/ansi-tokenize@0.3.0': 61 + resolution: {integrity: sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==} 62 + engines: {node: '>=18'} 63 + 64 + '@asamuzakjp/css-color@5.1.11': 65 + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} 66 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 67 + 68 + '@asamuzakjp/dom-selector@7.1.1': 69 + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} 70 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 71 + 72 + '@asamuzakjp/generational-cache@1.0.1': 73 + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} 74 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 75 + 76 + '@asamuzakjp/nwsapi@2.3.9': 77 + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} 50 78 51 79 '@atcute/atproto@3.1.12': 52 80 resolution: {integrity: sha512-SaHY0vV5+VfS2ViOcbYtxPmmh82vbxoK5ccHTGn5+ciHNY2arEVcBUFbIQKtsQP4PPZ+lNAVooH+Wh62flvCzg==} ··· 187 215 '@atproto-labs/simple-store@0.3.0': 188 216 resolution: {integrity: sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ==} 189 217 218 + '@atproto/api@0.19.18': 219 + resolution: {integrity: sha512-kyY9cHmmVLE09caTCwmudENQVsqn+Nd7G0vhC6Dk8OJFao0tNv9vvPx3hFm0CpxvvmimFJYL764SkDwlMLL3wQ==} 220 + 190 221 '@atproto/common-web@0.4.21': 191 222 resolution: {integrity: sha512-Odq+wdk3YNasGCjjlpl3bCIPvqYHige5DLfMkIffNv/2PI/iIj5ZvAvMvJlJ59OhReKSxtpI0invx5UQPc3+fw==} 192 223 ··· 229 260 '@badrap/valita@0.4.6': 230 261 resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==} 231 262 engines: {node: '>= 18'} 263 + 264 + '@bramus/specificity@2.4.2': 265 + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} 266 + hasBin: true 232 267 233 268 '@cloudflare/containers@0.1.1': 234 269 resolution: {integrity: sha512-YTdobRTnTlUOUPMFemufH367A9Z8pDfZ+UboYMLbGpO0VlvEXZDiioSmXPQMHld2vRtkL31mcRii3bcbQU6fdw==} ··· 283 318 resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 284 319 engines: {node: '>=12'} 285 320 321 + '@csstools/color-helpers@6.0.2': 322 + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} 323 + engines: {node: '>=20.19.0'} 324 + 325 + '@csstools/css-calc@3.2.1': 326 + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} 327 + engines: {node: '>=20.19.0'} 328 + peerDependencies: 329 + '@csstools/css-parser-algorithms': ^4.0.0 330 + '@csstools/css-tokenizer': ^4.0.0 331 + 332 + '@csstools/css-color-parser@4.1.1': 333 + resolution: {integrity: sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==} 334 + engines: {node: '>=20.19.0'} 335 + peerDependencies: 336 + '@csstools/css-parser-algorithms': ^4.0.0 337 + '@csstools/css-tokenizer': ^4.0.0 338 + 339 + '@csstools/css-parser-algorithms@4.0.0': 340 + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} 341 + engines: {node: '>=20.19.0'} 342 + peerDependencies: 343 + '@csstools/css-tokenizer': ^4.0.0 344 + 345 + '@csstools/css-syntax-patches-for-csstree@1.1.4': 346 + resolution: {integrity: sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==} 347 + peerDependencies: 348 + css-tree: ^3.2.1 349 + peerDependenciesMeta: 350 + css-tree: 351 + optional: true 352 + 353 + '@csstools/css-tokenizer@4.0.0': 354 + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} 355 + engines: {node: '>=20.19.0'} 356 + 286 357 '@emnapi/runtime@1.10.0': 287 358 resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} 288 359 ··· 598 669 cpu: [x64] 599 670 os: [win32] 600 671 672 + '@exodus/bytes@1.15.0': 673 + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} 674 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 675 + peerDependencies: 676 + '@noble/hashes': ^1.8.0 || ^2.0.0 677 + peerDependenciesMeta: 678 + '@noble/hashes': 679 + optional: true 680 + 601 681 '@img/colour@1.1.0': 602 682 resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} 603 683 engines: {node: '>=18'} ··· 761 841 '@jridgewell/trace-mapping@0.3.9': 762 842 resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 763 843 844 + '@letta-ai/letta-client@1.10.3': 845 + resolution: {integrity: sha512-E0nDGWZ00nkn43iIM1vlFuL84FhefstJnGqNuMamI3e0NzAtnpF5Z/BVxUnIPd1kVBRyId+QWSqVp8p66aztzQ==} 846 + 847 + '@letta-ai/letta-code-sdk@0.1.14': 848 + resolution: {integrity: sha512-rSMp7kYwRZ4PAe3jET+PETFesuYCbeodEp6Qf7a5rLu97epqs+zNegSR+UUgq6c9+c5eqbuo+BsRThTKiSNJkA==} 849 + 850 + '@letta-ai/letta-code@0.19.5': 851 + resolution: {integrity: sha512-INEDS79dkzJoQyL3IJRof+HNob3GZXgAge/JdJRFaVfJhU/o/6aTPcPWpQwxygE5ExIDSUlL85OlZ3CcBv0TyA==} 852 + engines: {node: '>=18'} 853 + hasBin: true 854 + 764 855 '@mary-ext/event-iterator@1.0.0': 765 856 resolution: {integrity: sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ==} 766 857 ··· 797 888 '@standard-schema/spec@1.1.0': 798 889 resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 799 890 891 + '@types/hast@3.0.4': 892 + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} 893 + 894 + '@types/unist@3.0.3': 895 + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} 896 + 897 + '@vscode/ripgrep-darwin-arm64@1.18.0': 898 + resolution: {integrity: sha512-r3ktHSvbFycQNF6sl7sNDPocpsI7J+mEzh1IaZFkY0spm3k2Z9t8hPAeOK7+p0l6p6/swkQC14XWX01low+94Q==} 899 + cpu: [arm64] 900 + os: [darwin] 901 + 902 + '@vscode/ripgrep-darwin-x64@1.18.0': 903 + resolution: {integrity: sha512-25b4gWbL138dGuQU244ebCKKc0q05ULBMoFSz9oAEUHNeqK/lOJViDS7DRvbDazzAzSEdan391Znks/R5mkaTQ==} 904 + cpu: [x64] 905 + os: [darwin] 906 + 907 + '@vscode/ripgrep-linux-arm64@1.18.0': 908 + resolution: {integrity: sha512-lQ/5zTG++U0E3IhVgS4EPTTn/U4okncaRMM5GOFfOYZywS4nuD31GhkHbNYlDk5CuDC68+hYJ0/eQeyCKJDA+g==} 909 + cpu: [arm64] 910 + os: [linux] 911 + 912 + '@vscode/ripgrep-linux-arm@1.18.0': 913 + resolution: {integrity: sha512-GDAvufNDHu8zqLEmXstalQF0Wh6wQvdsBi/Vg3Yi3CK4a8XoFXqqXVEHEZ9xQz3t0NfoSEc9JbvK9DDS6FxyxQ==} 914 + cpu: [arm] 915 + os: [linux] 916 + 917 + '@vscode/ripgrep-linux-ia32@1.18.0': 918 + resolution: {integrity: sha512-YWLkSUtFd4Jh5EepIhA9RJSfv3uMAVMo+2rBIGHPBnvgLrZciIs2cDKei1/p6Wc/aCzUoHyMAg2R6tw4ZCBKGg==} 919 + cpu: [ia32] 920 + os: [linux] 921 + 922 + '@vscode/ripgrep-linux-ppc64@1.18.0': 923 + resolution: {integrity: sha512-quXVY8fwQ8O/lvU1yrSqSl3jlUzysRSb+AfUfCL/tRtphxsKlFvPAejryZ6vg4Bgvn8XL74xb4qMCDmWgYrT5w==} 924 + cpu: [ppc64] 925 + os: [linux] 926 + 927 + '@vscode/ripgrep-linux-riscv64@1.18.0': 928 + resolution: {integrity: sha512-f5kBQBrWfQt8Q7OhSORuNDei5dkYagBj3y4jImSUXGMy8B/Ke7SltSRcUtjPv166FAFfHCAmWuZp3+cWnX2/Vw==} 929 + cpu: [riscv64] 930 + os: [linux] 931 + 932 + '@vscode/ripgrep-linux-s390x@1.18.0': 933 + resolution: {integrity: sha512-rTOcJFGGcl2c07RUOWUo4U1ndnemKhY6A9hnMB18uk7jSgJc0d/QLBGWMWpumdtoJtpizn/wIv5mXIisJukusQ==} 934 + cpu: [s390x] 935 + os: [linux] 936 + 937 + '@vscode/ripgrep-linux-x64@1.18.0': 938 + resolution: {integrity: sha512-mQ3bVrUpnD2vs7QT0vX90Lt0cnUq467uFtEktIdsJJmW296RoSULRGqWgzG1AKxyBpNDD6l4ZO4qKf6SgyC23Q==} 939 + cpu: [x64] 940 + os: [linux] 941 + 942 + '@vscode/ripgrep-win32-arm64@1.18.0': 943 + resolution: {integrity: sha512-vfTIjq1OHnzUjxZcHVQAMbnggp8dpGf+0QKFOZHwWPqFwXxQC8eCWM+5NUdoJ6yrElCeMzoUTXoK/LdZaniB+Q==} 944 + cpu: [arm64] 945 + os: [win32] 946 + 947 + '@vscode/ripgrep-win32-ia32@1.18.0': 948 + resolution: {integrity: sha512-//rfAE+BOw5AC2EMmepmiE36jUuevtQYNQqqlw1s3m9FlRxjxEut97RkRPHAu9BG4mSojatZx+kXZXNdyI9caQ==} 949 + cpu: [ia32] 950 + os: [win32] 951 + 952 + '@vscode/ripgrep-win32-x64@1.18.0': 953 + resolution: {integrity: sha512-KNPvtElldqILHdnAetujPaowkNbpqJy3ssIGGN6F6Kve9Qi+nNLI2DN01O83JjCEVQbCzl8Ov3QZ9Eov3BR8Dg==} 954 + cpu: [x64] 955 + os: [win32] 956 + 957 + '@vscode/ripgrep@1.18.0': 958 + resolution: {integrity: sha512-ns5lWe44tSfbTMbVUsyB+I1819PVSw4AdpgK0RNkzfWfwy6+3IUNSxwSrfTno1/oWaS/hERNz+XLWVyga2aJBQ==} 959 + 960 + ansi-escapes@7.3.0: 961 + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} 962 + engines: {node: '>=18'} 963 + 964 + ansi-regex@6.2.2: 965 + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} 966 + engines: {node: '>=12'} 967 + 968 + ansi-styles@6.2.3: 969 + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} 970 + engines: {node: '>=12'} 971 + 972 + auto-bind@5.0.1: 973 + resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} 974 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 975 + 976 + await-lock@2.2.2: 977 + resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} 978 + 979 + balanced-match@4.0.4: 980 + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} 981 + engines: {node: 18 || 20 || >=22} 982 + 983 + bidi-js@1.0.3: 984 + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} 985 + 800 986 blake3-wasm@2.1.5: 801 987 resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 802 988 989 + brace-expansion@5.0.6: 990 + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} 991 + engines: {node: 18 || 20 || >=22} 992 + 993 + bundle-name@4.1.0: 994 + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} 995 + engines: {node: '>=18'} 996 + 803 997 cac@7.0.0: 804 998 resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} 805 999 engines: {node: '>=20.19.0'} 806 1000 1001 + chalk@5.6.2: 1002 + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} 1003 + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} 1004 + 1005 + cli-boxes@4.0.1: 1006 + resolution: {integrity: sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw==} 1007 + engines: {node: '>=18.20 <19 || >=20.10'} 1008 + 1009 + cli-cursor@4.0.0: 1010 + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} 1011 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 1012 + 1013 + cli-truncate@6.0.0: 1014 + resolution: {integrity: sha512-3+YKIUFsohD9MIoOFPFBldjAlnfCmCDcqe6aYGFqlDTRKg80p4wg35L+j83QQ63iOlKRccEkbn8IuM++HsgEjA==} 1015 + engines: {node: '>=22'} 1016 + 1017 + code-excerpt@4.0.0: 1018 + resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} 1019 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 1020 + 1021 + convert-to-spaces@2.0.1: 1022 + resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} 1023 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 1024 + 807 1025 cookie@1.1.1: 808 1026 resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} 809 1027 engines: {node: '>=18'} ··· 811 1029 core-js@3.49.0: 812 1030 resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} 813 1031 1032 + css-tree@3.2.1: 1033 + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} 1034 + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} 1035 + 1036 + data-urls@7.0.0: 1037 + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} 1038 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 1039 + 1040 + decimal.js@10.6.0: 1041 + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} 1042 + 1043 + default-browser-id@5.0.1: 1044 + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} 1045 + engines: {node: '>=18'} 1046 + 1047 + default-browser@5.5.0: 1048 + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} 1049 + engines: {node: '>=18'} 1050 + 1051 + define-lazy-prop@3.0.0: 1052 + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} 1053 + engines: {node: '>=12'} 1054 + 1055 + dequal@2.0.3: 1056 + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} 1057 + engines: {node: '>=6'} 1058 + 814 1059 detect-libc@2.1.2: 815 1060 resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 816 1061 engines: {node: '>=8'} 817 1062 1063 + devlop@1.1.0: 1064 + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} 1065 + 1066 + entities@8.0.0: 1067 + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} 1068 + engines: {node: '>=20.19.0'} 1069 + 1070 + environment@1.1.0: 1071 + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} 1072 + engines: {node: '>=18'} 1073 + 818 1074 error-stack-parser-es@1.0.5: 819 1075 resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} 1076 + 1077 + es-toolkit@1.46.1: 1078 + resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} 820 1079 821 1080 esbuild@0.27.3: 822 1081 resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} ··· 828 1087 engines: {node: '>=18'} 829 1088 hasBin: true 830 1089 1090 + escape-string-regexp@2.0.0: 1091 + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} 1092 + engines: {node: '>=8'} 1093 + 831 1094 esm-env@1.2.2: 832 1095 resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 833 1096 ··· 839 1102 engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 840 1103 os: [darwin] 841 1104 1105 + get-east-asian-width@1.6.0: 1106 + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} 1107 + engines: {node: '>=18'} 1108 + 1109 + glob@13.0.6: 1110 + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} 1111 + engines: {node: 18 || 20 || >=22} 1112 + 1113 + has-flag@5.0.1: 1114 + resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} 1115 + engines: {node: '>=12'} 1116 + 1117 + highlight.js@11.11.1: 1118 + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} 1119 + engines: {node: '>=12.0.0'} 1120 + 842 1121 hono@4.12.18: 843 1122 resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==} 844 1123 engines: {node: '>=16.9.0'} 845 1124 1125 + html-encoding-sniffer@6.0.0: 1126 + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} 1127 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 1128 + 1129 + indent-string@5.0.0: 1130 + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} 1131 + engines: {node: '>=12'} 1132 + 1133 + ink-link@5.0.0: 1134 + resolution: {integrity: sha512-TFDXc/0mwUW7LMjsr0/LeLxPVV5BnHDuDQff9RCgP4rb3R+V/4dIwGBZbCevcJZtQnVcW+Iz1LUrUbpq+UDwYA==} 1135 + engines: {node: '>=18'} 1136 + peerDependencies: 1137 + ink: '>=6' 1138 + 1139 + ink@7.0.3: 1140 + resolution: {integrity: sha512-5kxHkIj9+RuqCU3zyvP4qvYWNOSHP2TW/SHayHGHOmk87KwfVcZwvJGemi9ch+ci2gXUqerK/Eh2DGEDt5q45g==} 1141 + engines: {node: '>=22'} 1142 + peerDependencies: 1143 + '@types/react': '>=19.2.0' 1144 + react: '>=19.2.0' 1145 + react-devtools-core: '>=6.1.2' 1146 + peerDependenciesMeta: 1147 + '@types/react': 1148 + optional: true 1149 + react-devtools-core: 1150 + optional: true 1151 + 1152 + is-docker@3.0.0: 1153 + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} 1154 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 1155 + hasBin: true 1156 + 1157 + is-fullwidth-code-point@5.1.0: 1158 + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} 1159 + engines: {node: '>=18'} 1160 + 1161 + is-in-ci@2.0.0: 1162 + resolution: {integrity: sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==} 1163 + engines: {node: '>=20'} 1164 + hasBin: true 1165 + 1166 + is-inside-container@1.0.0: 1167 + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} 1168 + engines: {node: '>=14.16'} 1169 + hasBin: true 1170 + 1171 + is-potential-custom-element-name@1.0.1: 1172 + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} 1173 + 1174 + is-wsl@3.1.1: 1175 + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} 1176 + engines: {node: '>=16'} 1177 + 846 1178 iso-datestring-validator@2.2.2: 847 1179 resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==} 848 1180 ··· 853 1185 jose@5.10.0: 854 1186 resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} 855 1187 1188 + jsdom@29.1.1: 1189 + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} 1190 + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} 1191 + peerDependencies: 1192 + canvas: ^3.0.0 1193 + peerDependenciesMeta: 1194 + canvas: 1195 + optional: true 1196 + 856 1197 kleur@4.1.5: 857 1198 resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} 858 1199 engines: {node: '>=6'} 1200 + 1201 + lowlight@3.3.0: 1202 + resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} 859 1203 860 1204 lru-cache@10.4.3: 861 1205 resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 862 1206 1207 + lru-cache@11.3.6: 1208 + resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} 1209 + engines: {node: 20 || >=22} 1210 + 1211 + mdn-data@2.27.1: 1212 + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} 1213 + 1214 + mimic-fn@2.1.0: 1215 + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} 1216 + engines: {node: '>=6'} 1217 + 863 1218 miniflare@4.20260508.0: 864 1219 resolution: {integrity: sha512-h3aG+PA8jEH76V4ZtBAbs3g7kjMfHJUF8hPvxeeajLTKwir+G+dqfBODg5yF9MT29LqrZKCRQRqzfHPWX4kCIg==} 865 1220 engines: {node: '>=22.0.0'} 866 1221 hasBin: true 867 1222 1223 + minimatch@10.2.5: 1224 + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} 1225 + engines: {node: 18 || 20 || >=22} 1226 + 1227 + minipass@7.1.3: 1228 + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} 1229 + engines: {node: '>=16 || 14 >=14.17'} 1230 + 868 1231 multiformats@9.9.0: 869 1232 resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} 870 1233 ··· 873 1236 engines: {node: ^18 || >=20} 874 1237 hasBin: true 875 1238 1239 + node-addon-api@7.1.1: 1240 + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} 1241 + 1242 + node-pty@1.1.0: 1243 + resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==} 1244 + 1245 + onetime@5.1.2: 1246 + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} 1247 + engines: {node: '>=6'} 1248 + 1249 + open@10.2.0: 1250 + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} 1251 + engines: {node: '>=18'} 1252 + 1253 + parse5@8.0.1: 1254 + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} 1255 + 876 1256 partysocket@1.1.19: 877 1257 resolution: {integrity: sha512-hPwsXSdUc8PKNCinET6TD3JQOxzQ2JaP0bUZQXBVl6UM8UuLn1odgf1LcJXHy4UHSQwWL/RU3AnyhEsGM+W+sg==} 878 1258 peerDependencies: ··· 881 1261 react: 882 1262 optional: true 883 1263 1264 + patch-console@2.0.0: 1265 + resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} 1266 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 1267 + 1268 + path-scurry@2.0.2: 1269 + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} 1270 + engines: {node: 18 || 20 || >=22} 1271 + 884 1272 path-to-regexp@6.3.0: 885 1273 resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} 886 1274 ··· 895 1283 engines: {node: '>=14'} 896 1284 hasBin: true 897 1285 1286 + punycode@2.3.1: 1287 + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 1288 + engines: {node: '>=6'} 1289 + 1290 + react-reconciler@0.33.0: 1291 + resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==} 1292 + engines: {node: '>=0.10.0'} 1293 + peerDependencies: 1294 + react: ^19.2.0 1295 + 1296 + react@19.2.6: 1297 + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} 1298 + engines: {node: '>=0.10.0'} 1299 + 1300 + require-from-string@2.0.2: 1301 + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 1302 + engines: {node: '>=0.10.0'} 1303 + 1304 + restore-cursor@4.0.0: 1305 + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} 1306 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 1307 + 1308 + run-applescript@7.1.0: 1309 + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} 1310 + engines: {node: '>=18'} 1311 + 1312 + saxes@6.0.0: 1313 + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} 1314 + engines: {node: '>=v12.22.7'} 1315 + 1316 + scheduler@0.27.0: 1317 + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} 1318 + 898 1319 semver@7.8.0: 899 1320 resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} 900 1321 engines: {node: '>=10'} ··· 904 1325 resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} 905 1326 engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 906 1327 1328 + signal-exit@3.0.7: 1329 + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} 1330 + 1331 + slice-ansi@9.0.0: 1332 + resolution: {integrity: sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA==} 1333 + engines: {node: '>=22'} 1334 + 1335 + source-map-js@1.2.1: 1336 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1337 + engines: {node: '>=0.10.0'} 1338 + 1339 + stack-utils@2.0.6: 1340 + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} 1341 + engines: {node: '>=10'} 1342 + 1343 + string-width@8.2.1: 1344 + resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} 1345 + engines: {node: '>=20'} 1346 + 1347 + strip-ansi@7.2.0: 1348 + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} 1349 + engines: {node: '>=12'} 1350 + 907 1351 supports-color@10.2.2: 908 1352 resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} 909 1353 engines: {node: '>=18'} 910 1354 1355 + supports-hyperlinks@4.4.0: 1356 + resolution: {integrity: sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==} 1357 + engines: {node: '>=20'} 1358 + 1359 + symbol-tree@3.2.4: 1360 + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} 1361 + 1362 + tagged-tag@1.0.0: 1363 + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} 1364 + engines: {node: '>=20'} 1365 + 1366 + terminal-link@5.0.0: 1367 + resolution: {integrity: sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==} 1368 + engines: {node: '>=20'} 1369 + 1370 + terminal-size@4.0.1: 1371 + resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==} 1372 + engines: {node: '>=18'} 1373 + 1374 + tlds@1.261.0: 1375 + resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} 1376 + hasBin: true 1377 + 1378 + tldts-core@7.0.30: 1379 + resolution: {integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==} 1380 + 1381 + tldts@7.0.30: 1382 + resolution: {integrity: sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==} 1383 + hasBin: true 1384 + 1385 + tough-cookie@6.0.1: 1386 + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} 1387 + engines: {node: '>=16'} 1388 + 1389 + tr46@6.0.0: 1390 + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} 1391 + engines: {node: '>=20'} 1392 + 911 1393 tslib@2.8.1: 912 1394 resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 913 1395 ··· 915 1397 resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} 916 1398 engines: {node: '>=16'} 917 1399 1400 + type-fest@5.6.0: 1401 + resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} 1402 + engines: {node: '>=20'} 1403 + 918 1404 typescript@5.9.3: 919 1405 resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 920 1406 engines: {node: '>=14.17'} ··· 927 1413 resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} 928 1414 engines: {node: '>=20.18.1'} 929 1415 1416 + undici@7.25.0: 1417 + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} 1418 + engines: {node: '>=20.18.1'} 1419 + 930 1420 unenv@2.0.0-rc.24: 931 1421 resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} 932 1422 933 1423 unicode-segmenter@0.14.5: 934 1424 resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 935 1425 1426 + w3c-xmlserializer@5.0.0: 1427 + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} 1428 + engines: {node: '>=18'} 1429 + 1430 + webidl-conversions@8.0.1: 1431 + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} 1432 + engines: {node: '>=20'} 1433 + 1434 + whatwg-mimetype@5.0.0: 1435 + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} 1436 + engines: {node: '>=20'} 1437 + 1438 + whatwg-url@16.0.1: 1439 + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} 1440 + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} 1441 + 1442 + widest-line@6.0.0: 1443 + resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} 1444 + engines: {node: '>=20'} 1445 + 936 1446 workerd@1.20260508.1: 937 1447 resolution: {integrity: sha512-VlnjyH3AjVddpSK7J54nsCVgf8i2733pl8GjKttfNi7vN/hEjjAk20d2b1nDToOLKvRQpTewRnVkqaaeGHCaAw==} 938 1448 engines: {node: '>=16'} ··· 948 1458 '@cloudflare/workers-types': 949 1459 optional: true 950 1460 1461 + wrap-ansi@10.0.0: 1462 + resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} 1463 + engines: {node: '>=20'} 1464 + 951 1465 ws@8.18.0: 952 1466 resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} 953 1467 engines: {node: '>=10.0.0'} ··· 959 1473 optional: true 960 1474 utf-8-validate: 961 1475 optional: true 1476 + 1477 + ws@8.20.1: 1478 + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} 1479 + engines: {node: '>=10.0.0'} 1480 + peerDependencies: 1481 + bufferutil: ^4.0.1 1482 + utf-8-validate: '>=5.0.2' 1483 + peerDependenciesMeta: 1484 + bufferutil: 1485 + optional: true 1486 + utf-8-validate: 1487 + optional: true 1488 + 1489 + wsl-utils@0.1.0: 1490 + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} 1491 + engines: {node: '>=18'} 1492 + 1493 + xml-name-validator@5.0.0: 1494 + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} 1495 + engines: {node: '>=18'} 1496 + 1497 + xmlchars@2.2.0: 1498 + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} 962 1499 963 1500 yocto-queue@1.2.2: 964 1501 resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} 965 1502 engines: {node: '>=12.20'} 966 1503 1504 + yoga-layout@3.2.1: 1505 + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} 1506 + 967 1507 youch-core@0.3.3: 968 1508 resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} 969 1509 ··· 974 1514 resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} 975 1515 976 1516 snapshots: 1517 + 1518 + '@alcalzone/ansi-tokenize@0.3.0': 1519 + dependencies: 1520 + ansi-styles: 6.2.3 1521 + is-fullwidth-code-point: 5.1.0 1522 + 1523 + '@asamuzakjp/css-color@5.1.11': 1524 + dependencies: 1525 + '@asamuzakjp/generational-cache': 1.0.1 1526 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) 1527 + '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) 1528 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) 1529 + '@csstools/css-tokenizer': 4.0.0 1530 + 1531 + '@asamuzakjp/dom-selector@7.1.1': 1532 + dependencies: 1533 + '@asamuzakjp/generational-cache': 1.0.1 1534 + '@asamuzakjp/nwsapi': 2.3.9 1535 + bidi-js: 1.0.3 1536 + css-tree: 3.2.1 1537 + is-potential-custom-element-name: 1.0.1 1538 + 1539 + '@asamuzakjp/generational-cache@1.0.1': {} 1540 + 1541 + '@asamuzakjp/nwsapi@2.3.9': {} 977 1542 978 1543 '@atcute/atproto@3.1.12(@atcute/lexicons@1.3.1)': 979 1544 dependencies: ··· 1019 1584 '@atcute/util-fetch': 1.0.5 1020 1585 '@badrap/valita': 0.4.6 1021 1586 1022 - '@atcute/identity-resolver@1.2.3(@atcute/identity@1.1.5(@atcute/lexicons@2.0.0))(@atcute/lexicons@2.0.0)': 1023 - dependencies: 1024 - '@atcute/identity': 1.1.5(@atcute/lexicons@2.0.0) 1025 - '@atcute/lexicons': 2.0.0 1026 - '@atcute/util-fetch': 1.0.5 1027 - '@badrap/valita': 0.4.6 1028 - 1029 1587 '@atcute/identity@1.1.5(@atcute/lexicons@1.3.1)': 1030 1588 dependencies: 1031 1589 '@atcute/lexicons': 1.3.1 ··· 1036 1594 '@atcute/lexicons': 2.0.0 1037 1595 '@badrap/valita': 0.4.6 1038 1596 1039 - '@atcute/jetstream@1.1.3(@atcute/lexicons@1.3.1)': 1597 + '@atcute/jetstream@1.1.3(@atcute/lexicons@1.3.1)(react@19.2.6)': 1040 1598 dependencies: 1041 1599 '@atcute/lexicons': 1.3.1 1042 1600 '@badrap/valita': 0.4.6 1043 1601 '@mary-ext/event-iterator': 1.0.0 1044 1602 '@mary-ext/simple-event-emitter': 1.0.1 1045 - partysocket: 1.1.19 1603 + partysocket: 1.1.19(react@19.2.6) 1046 1604 type-fest: 4.41.0 1047 1605 transitivePeerDependencies: 1048 1606 - react ··· 1071 1629 '@atcute/util-text': 1.3.1 1072 1630 '@badrap/valita': 0.4.6 1073 1631 1074 - '@atcute/lexicon-doc@2.2.1(@atcute/lexicons@2.0.0)': 1075 - dependencies: 1076 - '@atcute/identity': 1.1.5(@atcute/lexicons@2.0.0) 1077 - '@atcute/lexicons': 2.0.0 1078 - '@atcute/uint8array': 1.1.2 1079 - '@atcute/util-text': 1.3.1 1080 - '@badrap/valita': 0.4.6 1081 - 1082 1632 '@atcute/lexicon-resolver@0.1.7(@atcute/cbor@2.3.3(@atcute/cid@2.4.1))(@atcute/cid@2.4.1)(@atcute/identity-resolver@1.2.3(@atcute/identity@1.1.5(@atcute/lexicons@2.0.0))(@atcute/lexicons@2.0.0))(@atcute/identity@1.1.5(@atcute/lexicons@2.0.0))(@atcute/lexicon-doc@2.2.1(@atcute/lexicons@2.0.0))(@atcute/lexicons@1.3.1)': 1083 1633 dependencies: 1084 1634 '@atcute/crypto': 2.4.1 1085 1635 '@atcute/identity': 1.1.5(@atcute/lexicons@2.0.0) 1086 - '@atcute/identity-resolver': 1.2.3(@atcute/identity@1.1.5(@atcute/lexicons@2.0.0))(@atcute/lexicons@2.0.0) 1087 - '@atcute/lexicon-doc': 2.2.1(@atcute/lexicons@2.0.0) 1636 + '@atcute/identity-resolver': 1.2.3(@atcute/identity@1.1.5(@atcute/lexicons@2.0.0))(@atcute/lexicons@1.3.1) 1637 + '@atcute/lexicon-doc': 2.2.1(@atcute/lexicons@1.3.1) 1088 1638 '@atcute/lexicons': 1.3.1 1089 1639 '@atcute/repo': 0.1.5(@atcute/cbor@2.3.3(@atcute/cid@2.4.1))(@atcute/cid@2.4.1)(@atcute/lexicons@1.3.1) 1090 1640 '@atcute/util-fetch': 1.0.5 ··· 1153 1703 transitivePeerDependencies: 1154 1704 - '@atcute/cid' 1155 1705 1156 - '@atmo-dev/contrail-lexicons@0.4.6(@atcute/cbor@2.3.3(@atcute/cid@2.4.1))(@atcute/cid@2.4.1)(wrangler@4.90.1(@cloudflare/workers-types@4.20260511.1))': 1706 + '@atmo-dev/contrail-lexicons@0.4.6(@atcute/cbor@2.3.3(@atcute/cid@2.4.1))(@atcute/cid@2.4.1)(react@19.2.6)(wrangler@4.90.1(@cloudflare/workers-types@4.20260511.1))': 1157 1707 dependencies: 1158 1708 '@atcute/lex-cli': 2.8.2(@atcute/cbor@2.3.3(@atcute/cid@2.4.1))(@atcute/cid@2.4.1) 1159 - '@atmo-dev/contrail': 0.6.0(wrangler@4.90.1(@cloudflare/workers-types@4.20260511.1)) 1709 + '@atmo-dev/contrail': 0.6.0(react@19.2.6)(wrangler@4.90.1(@cloudflare/workers-types@4.20260511.1)) 1160 1710 transitivePeerDependencies: 1161 1711 - '@atcute/cbor' 1162 1712 - '@atcute/cid' ··· 1164 1714 - react 1165 1715 - wrangler 1166 1716 1167 - '@atmo-dev/contrail@0.6.0(wrangler@4.90.1(@cloudflare/workers-types@4.20260511.1))': 1717 + '@atmo-dev/contrail@0.6.0(react@19.2.6)(wrangler@4.90.1(@cloudflare/workers-types@4.20260511.1))': 1168 1718 dependencies: 1169 1719 '@atcute/atproto': 3.1.12(@atcute/lexicons@1.3.1) 1170 1720 '@atcute/cbor': 2.3.3(@atcute/cid@2.4.1) ··· 1172 1722 '@atcute/client': 4.2.2(@atcute/lexicons@1.3.1) 1173 1723 '@atcute/identity': 1.1.5(@atcute/lexicons@1.3.1) 1174 1724 '@atcute/identity-resolver': 1.2.3(@atcute/identity@1.1.5(@atcute/lexicons@2.0.0))(@atcute/lexicons@1.3.1) 1175 - '@atcute/jetstream': 1.1.3(@atcute/lexicons@1.3.1) 1725 + '@atcute/jetstream': 1.1.3(@atcute/lexicons@1.3.1)(react@19.2.6) 1176 1726 '@atcute/lexicons': 1.3.1 1177 1727 '@atcute/xrpc-server': 0.1.12(@atcute/cid@2.4.1) 1178 1728 cac: 7.0.0 ··· 1217 1767 1218 1768 '@atproto-labs/simple-store@0.3.0': {} 1219 1769 1770 + '@atproto/api@0.19.18': 1771 + dependencies: 1772 + '@atproto/common-web': 0.4.21 1773 + '@atproto/lexicon': 0.6.2 1774 + '@atproto/syntax': 0.5.4 1775 + '@atproto/xrpc': 0.7.7 1776 + await-lock: 2.2.2 1777 + multiformats: 9.9.0 1778 + tlds: 1.261.0 1779 + zod: 3.25.76 1780 + 1220 1781 '@atproto/common-web@0.4.21': 1221 1782 dependencies: 1222 1783 '@atproto/lex-data': 0.0.15 ··· 1309 1870 1310 1871 '@badrap/valita@0.4.6': {} 1311 1872 1873 + '@bramus/specificity@2.4.2': 1874 + dependencies: 1875 + css-tree: 3.2.1 1876 + 1312 1877 '@cloudflare/containers@0.1.1': {} 1313 1878 1314 1879 '@cloudflare/kv-asset-handler@0.5.0': {} ··· 1340 1905 dependencies: 1341 1906 '@jridgewell/trace-mapping': 0.3.9 1342 1907 1908 + '@csstools/color-helpers@6.0.2': {} 1909 + 1910 + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': 1911 + dependencies: 1912 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) 1913 + '@csstools/css-tokenizer': 4.0.0 1914 + 1915 + '@csstools/css-color-parser@4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': 1916 + dependencies: 1917 + '@csstools/color-helpers': 6.0.2 1918 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) 1919 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) 1920 + '@csstools/css-tokenizer': 4.0.0 1921 + 1922 + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': 1923 + dependencies: 1924 + '@csstools/css-tokenizer': 4.0.0 1925 + 1926 + '@csstools/css-syntax-patches-for-csstree@1.1.4(css-tree@3.2.1)': 1927 + optionalDependencies: 1928 + css-tree: 3.2.1 1929 + 1930 + '@csstools/css-tokenizer@4.0.0': {} 1931 + 1343 1932 '@emnapi/runtime@1.10.0': 1344 1933 dependencies: 1345 1934 tslib: 2.8.1 ··· 1501 2090 '@esbuild/win32-x64@0.28.0': 1502 2091 optional: true 1503 2092 2093 + '@exodus/bytes@1.15.0': {} 2094 + 1504 2095 '@img/colour@1.1.0': {} 1505 2096 1506 2097 '@img/sharp-darwin-arm64@0.34.5': ··· 1606 2197 '@jridgewell/resolve-uri': 3.1.2 1607 2198 '@jridgewell/sourcemap-codec': 1.5.5 1608 2199 2200 + '@letta-ai/letta-client@1.10.3': {} 2201 + 2202 + '@letta-ai/letta-code-sdk@0.1.14(ink@7.0.3(react@19.2.6))': 2203 + dependencies: 2204 + '@letta-ai/letta-code': 0.19.5(ink@7.0.3(react@19.2.6)) 2205 + transitivePeerDependencies: 2206 + - bufferutil 2207 + - ink 2208 + - utf-8-validate 2209 + 2210 + '@letta-ai/letta-code@0.19.5(ink@7.0.3(react@19.2.6))': 2211 + dependencies: 2212 + '@letta-ai/letta-client': 1.10.3 2213 + glob: 13.0.6 2214 + highlight.js: 11.11.1 2215 + ink-link: 5.0.0(ink@7.0.3(react@19.2.6)) 2216 + lowlight: 3.3.0 2217 + node-pty: 1.1.0 2218 + open: 10.2.0 2219 + sharp: 0.34.5 2220 + ws: 8.20.1 2221 + optionalDependencies: 2222 + '@vscode/ripgrep': 1.18.0 2223 + transitivePeerDependencies: 2224 + - bufferutil 2225 + - ink 2226 + - utf-8-validate 2227 + 1609 2228 '@mary-ext/event-iterator@1.0.0': 1610 2229 dependencies: 1611 2230 yocto-queue: 1.2.2 ··· 1638 2257 1639 2258 '@standard-schema/spec@1.1.0': {} 1640 2259 2260 + '@types/hast@3.0.4': 2261 + dependencies: 2262 + '@types/unist': 3.0.3 2263 + 2264 + '@types/unist@3.0.3': {} 2265 + 2266 + '@vscode/ripgrep-darwin-arm64@1.18.0': 2267 + optional: true 2268 + 2269 + '@vscode/ripgrep-darwin-x64@1.18.0': 2270 + optional: true 2271 + 2272 + '@vscode/ripgrep-linux-arm64@1.18.0': 2273 + optional: true 2274 + 2275 + '@vscode/ripgrep-linux-arm@1.18.0': 2276 + optional: true 2277 + 2278 + '@vscode/ripgrep-linux-ia32@1.18.0': 2279 + optional: true 2280 + 2281 + '@vscode/ripgrep-linux-ppc64@1.18.0': 2282 + optional: true 2283 + 2284 + '@vscode/ripgrep-linux-riscv64@1.18.0': 2285 + optional: true 2286 + 2287 + '@vscode/ripgrep-linux-s390x@1.18.0': 2288 + optional: true 2289 + 2290 + '@vscode/ripgrep-linux-x64@1.18.0': 2291 + optional: true 2292 + 2293 + '@vscode/ripgrep-win32-arm64@1.18.0': 2294 + optional: true 2295 + 2296 + '@vscode/ripgrep-win32-ia32@1.18.0': 2297 + optional: true 2298 + 2299 + '@vscode/ripgrep-win32-x64@1.18.0': 2300 + optional: true 2301 + 2302 + '@vscode/ripgrep@1.18.0': 2303 + optionalDependencies: 2304 + '@vscode/ripgrep-darwin-arm64': 1.18.0 2305 + '@vscode/ripgrep-darwin-x64': 1.18.0 2306 + '@vscode/ripgrep-linux-arm': 1.18.0 2307 + '@vscode/ripgrep-linux-arm64': 1.18.0 2308 + '@vscode/ripgrep-linux-ia32': 1.18.0 2309 + '@vscode/ripgrep-linux-ppc64': 1.18.0 2310 + '@vscode/ripgrep-linux-riscv64': 1.18.0 2311 + '@vscode/ripgrep-linux-s390x': 1.18.0 2312 + '@vscode/ripgrep-linux-x64': 1.18.0 2313 + '@vscode/ripgrep-win32-arm64': 1.18.0 2314 + '@vscode/ripgrep-win32-ia32': 1.18.0 2315 + '@vscode/ripgrep-win32-x64': 1.18.0 2316 + optional: true 2317 + 2318 + ansi-escapes@7.3.0: 2319 + dependencies: 2320 + environment: 1.1.0 2321 + 2322 + ansi-regex@6.2.2: {} 2323 + 2324 + ansi-styles@6.2.3: {} 2325 + 2326 + auto-bind@5.0.1: {} 2327 + 2328 + await-lock@2.2.2: {} 2329 + 2330 + balanced-match@4.0.4: {} 2331 + 2332 + bidi-js@1.0.3: 2333 + dependencies: 2334 + require-from-string: 2.0.2 2335 + 1641 2336 blake3-wasm@2.1.5: {} 1642 2337 2338 + brace-expansion@5.0.6: 2339 + dependencies: 2340 + balanced-match: 4.0.4 2341 + 2342 + bundle-name@4.1.0: 2343 + dependencies: 2344 + run-applescript: 7.1.0 2345 + 1643 2346 cac@7.0.0: {} 1644 2347 2348 + chalk@5.6.2: {} 2349 + 2350 + cli-boxes@4.0.1: {} 2351 + 2352 + cli-cursor@4.0.0: 2353 + dependencies: 2354 + restore-cursor: 4.0.0 2355 + 2356 + cli-truncate@6.0.0: 2357 + dependencies: 2358 + slice-ansi: 9.0.0 2359 + string-width: 8.2.1 2360 + 2361 + code-excerpt@4.0.0: 2362 + dependencies: 2363 + convert-to-spaces: 2.0.1 2364 + 2365 + convert-to-spaces@2.0.1: {} 2366 + 1645 2367 cookie@1.1.1: {} 1646 2368 1647 2369 core-js@3.49.0: {} 1648 2370 2371 + css-tree@3.2.1: 2372 + dependencies: 2373 + mdn-data: 2.27.1 2374 + source-map-js: 1.2.1 2375 + 2376 + data-urls@7.0.0: 2377 + dependencies: 2378 + whatwg-mimetype: 5.0.0 2379 + whatwg-url: 16.0.1 2380 + transitivePeerDependencies: 2381 + - '@noble/hashes' 2382 + 2383 + decimal.js@10.6.0: {} 2384 + 2385 + default-browser-id@5.0.1: {} 2386 + 2387 + default-browser@5.5.0: 2388 + dependencies: 2389 + bundle-name: 4.1.0 2390 + default-browser-id: 5.0.1 2391 + 2392 + define-lazy-prop@3.0.0: {} 2393 + 2394 + dequal@2.0.3: {} 2395 + 1649 2396 detect-libc@2.1.2: {} 1650 2397 2398 + devlop@1.1.0: 2399 + dependencies: 2400 + dequal: 2.0.3 2401 + 2402 + entities@8.0.0: {} 2403 + 2404 + environment@1.1.0: {} 2405 + 1651 2406 error-stack-parser-es@1.0.5: {} 2407 + 2408 + es-toolkit@1.46.1: {} 1652 2409 1653 2410 esbuild@0.27.3: 1654 2411 optionalDependencies: ··· 1708 2465 '@esbuild/win32-ia32': 0.28.0 1709 2466 '@esbuild/win32-x64': 0.28.0 1710 2467 2468 + escape-string-regexp@2.0.0: {} 2469 + 1711 2470 esm-env@1.2.2: {} 1712 2471 1713 2472 event-target-polyfill@0.0.4: {} ··· 1715 2474 fsevents@2.3.3: 1716 2475 optional: true 1717 2476 2477 + get-east-asian-width@1.6.0: {} 2478 + 2479 + glob@13.0.6: 2480 + dependencies: 2481 + minimatch: 10.2.5 2482 + minipass: 7.1.3 2483 + path-scurry: 2.0.2 2484 + 2485 + has-flag@5.0.1: {} 2486 + 2487 + highlight.js@11.11.1: {} 2488 + 1718 2489 hono@4.12.18: {} 1719 2490 2491 + html-encoding-sniffer@6.0.0: 2492 + dependencies: 2493 + '@exodus/bytes': 1.15.0 2494 + transitivePeerDependencies: 2495 + - '@noble/hashes' 2496 + 2497 + indent-string@5.0.0: {} 2498 + 2499 + ink-link@5.0.0(ink@7.0.3(react@19.2.6)): 2500 + dependencies: 2501 + ink: 7.0.3(react@19.2.6) 2502 + terminal-link: 5.0.0 2503 + 2504 + ink@7.0.3(react@19.2.6): 2505 + dependencies: 2506 + '@alcalzone/ansi-tokenize': 0.3.0 2507 + ansi-escapes: 7.3.0 2508 + ansi-styles: 6.2.3 2509 + auto-bind: 5.0.1 2510 + chalk: 5.6.2 2511 + cli-boxes: 4.0.1 2512 + cli-cursor: 4.0.0 2513 + cli-truncate: 6.0.0 2514 + code-excerpt: 4.0.0 2515 + es-toolkit: 1.46.1 2516 + indent-string: 5.0.0 2517 + is-in-ci: 2.0.0 2518 + patch-console: 2.0.0 2519 + react: 19.2.6 2520 + react-reconciler: 0.33.0(react@19.2.6) 2521 + scheduler: 0.27.0 2522 + signal-exit: 3.0.7 2523 + slice-ansi: 9.0.0 2524 + stack-utils: 2.0.6 2525 + string-width: 8.2.1 2526 + terminal-size: 4.0.1 2527 + type-fest: 5.6.0 2528 + widest-line: 6.0.0 2529 + wrap-ansi: 10.0.0 2530 + ws: 8.20.1 2531 + yoga-layout: 3.2.1 2532 + transitivePeerDependencies: 2533 + - bufferutil 2534 + - utf-8-validate 2535 + 2536 + is-docker@3.0.0: {} 2537 + 2538 + is-fullwidth-code-point@5.1.0: 2539 + dependencies: 2540 + get-east-asian-width: 1.6.0 2541 + 2542 + is-in-ci@2.0.0: {} 2543 + 2544 + is-inside-container@1.0.0: 2545 + dependencies: 2546 + is-docker: 3.0.0 2547 + 2548 + is-potential-custom-element-name@1.0.1: {} 2549 + 2550 + is-wsl@3.1.1: 2551 + dependencies: 2552 + is-inside-container: 1.0.0 2553 + 1720 2554 iso-datestring-validator@2.2.2: {} 1721 2555 1722 2556 jiti@2.7.0: {} 1723 2557 1724 2558 jose@5.10.0: {} 1725 2559 2560 + jsdom@29.1.1: 2561 + dependencies: 2562 + '@asamuzakjp/css-color': 5.1.11 2563 + '@asamuzakjp/dom-selector': 7.1.1 2564 + '@bramus/specificity': 2.4.2 2565 + '@csstools/css-syntax-patches-for-csstree': 1.1.4(css-tree@3.2.1) 2566 + '@exodus/bytes': 1.15.0 2567 + css-tree: 3.2.1 2568 + data-urls: 7.0.0 2569 + decimal.js: 10.6.0 2570 + html-encoding-sniffer: 6.0.0 2571 + is-potential-custom-element-name: 1.0.1 2572 + lru-cache: 11.3.6 2573 + parse5: 8.0.1 2574 + saxes: 6.0.0 2575 + symbol-tree: 3.2.4 2576 + tough-cookie: 6.0.1 2577 + undici: 7.25.0 2578 + w3c-xmlserializer: 5.0.0 2579 + webidl-conversions: 8.0.1 2580 + whatwg-mimetype: 5.0.0 2581 + whatwg-url: 16.0.1 2582 + xml-name-validator: 5.0.0 2583 + transitivePeerDependencies: 2584 + - '@noble/hashes' 2585 + 1726 2586 kleur@4.1.5: {} 1727 2587 2588 + lowlight@3.3.0: 2589 + dependencies: 2590 + '@types/hast': 3.0.4 2591 + devlop: 1.1.0 2592 + highlight.js: 11.11.1 2593 + 1728 2594 lru-cache@10.4.3: {} 2595 + 2596 + lru-cache@11.3.6: {} 2597 + 2598 + mdn-data@2.27.1: {} 2599 + 2600 + mimic-fn@2.1.0: {} 1729 2601 1730 2602 miniflare@4.20260508.0: 1731 2603 dependencies: ··· 1739 2611 - bufferutil 1740 2612 - utf-8-validate 1741 2613 2614 + minimatch@10.2.5: 2615 + dependencies: 2616 + brace-expansion: 5.0.6 2617 + 2618 + minipass@7.1.3: {} 2619 + 1742 2620 multiformats@9.9.0: {} 1743 2621 1744 2622 nanoid@5.1.11: {} 1745 2623 1746 - partysocket@1.1.19: 2624 + node-addon-api@7.1.1: {} 2625 + 2626 + node-pty@1.1.0: 2627 + dependencies: 2628 + node-addon-api: 7.1.1 2629 + 2630 + onetime@5.1.2: 2631 + dependencies: 2632 + mimic-fn: 2.1.0 2633 + 2634 + open@10.2.0: 2635 + dependencies: 2636 + default-browser: 5.5.0 2637 + define-lazy-prop: 3.0.0 2638 + is-inside-container: 1.0.0 2639 + wsl-utils: 0.1.0 2640 + 2641 + parse5@8.0.1: 2642 + dependencies: 2643 + entities: 8.0.0 2644 + 2645 + partysocket@1.1.19(react@19.2.6): 1747 2646 dependencies: 1748 2647 event-target-polyfill: 0.0.4 2648 + optionalDependencies: 2649 + react: 19.2.6 2650 + 2651 + patch-console@2.0.0: {} 2652 + 2653 + path-scurry@2.0.2: 2654 + dependencies: 2655 + lru-cache: 11.3.6 2656 + minipass: 7.1.3 1749 2657 1750 2658 path-to-regexp@6.3.0: {} 1751 2659 ··· 1755 2663 1756 2664 prettier@3.8.3: {} 1757 2665 2666 + punycode@2.3.1: {} 2667 + 2668 + react-reconciler@0.33.0(react@19.2.6): 2669 + dependencies: 2670 + react: 19.2.6 2671 + scheduler: 0.27.0 2672 + 2673 + react@19.2.6: {} 2674 + 2675 + require-from-string@2.0.2: {} 2676 + 2677 + restore-cursor@4.0.0: 2678 + dependencies: 2679 + onetime: 5.1.2 2680 + signal-exit: 3.0.7 2681 + 2682 + run-applescript@7.1.0: {} 2683 + 2684 + saxes@6.0.0: 2685 + dependencies: 2686 + xmlchars: 2.2.0 2687 + 2688 + scheduler@0.27.0: {} 2689 + 1758 2690 semver@7.8.0: {} 1759 2691 1760 2692 sharp@0.34.5: ··· 1788 2720 '@img/sharp-win32-ia32': 0.34.5 1789 2721 '@img/sharp-win32-x64': 0.34.5 1790 2722 2723 + signal-exit@3.0.7: {} 2724 + 2725 + slice-ansi@9.0.0: 2726 + dependencies: 2727 + ansi-styles: 6.2.3 2728 + is-fullwidth-code-point: 5.1.0 2729 + 2730 + source-map-js@1.2.1: {} 2731 + 2732 + stack-utils@2.0.6: 2733 + dependencies: 2734 + escape-string-regexp: 2.0.0 2735 + 2736 + string-width@8.2.1: 2737 + dependencies: 2738 + get-east-asian-width: 1.6.0 2739 + strip-ansi: 7.2.0 2740 + 2741 + strip-ansi@7.2.0: 2742 + dependencies: 2743 + ansi-regex: 6.2.2 2744 + 1791 2745 supports-color@10.2.2: {} 1792 2746 2747 + supports-hyperlinks@4.4.0: 2748 + dependencies: 2749 + has-flag: 5.0.1 2750 + supports-color: 10.2.2 2751 + 2752 + symbol-tree@3.2.4: {} 2753 + 2754 + tagged-tag@1.0.0: {} 2755 + 2756 + terminal-link@5.0.0: 2757 + dependencies: 2758 + ansi-escapes: 7.3.0 2759 + supports-hyperlinks: 4.4.0 2760 + 2761 + terminal-size@4.0.1: {} 2762 + 2763 + tlds@1.261.0: {} 2764 + 2765 + tldts-core@7.0.30: {} 2766 + 2767 + tldts@7.0.30: 2768 + dependencies: 2769 + tldts-core: 7.0.30 2770 + 2771 + tough-cookie@6.0.1: 2772 + dependencies: 2773 + tldts: 7.0.30 2774 + 2775 + tr46@6.0.0: 2776 + dependencies: 2777 + punycode: 2.3.1 2778 + 1793 2779 tslib@2.8.1: {} 1794 2780 1795 2781 type-fest@4.41.0: {} 1796 2782 2783 + type-fest@5.6.0: 2784 + dependencies: 2785 + tagged-tag: 1.0.0 2786 + 1797 2787 typescript@5.9.3: {} 1798 2788 1799 2789 uint8arrays@3.0.0: ··· 1802 2792 1803 2793 undici@7.24.8: {} 1804 2794 2795 + undici@7.25.0: {} 2796 + 1805 2797 unenv@2.0.0-rc.24: 1806 2798 dependencies: 1807 2799 pathe: 2.0.3 1808 2800 1809 2801 unicode-segmenter@0.14.5: {} 1810 2802 2803 + w3c-xmlserializer@5.0.0: 2804 + dependencies: 2805 + xml-name-validator: 5.0.0 2806 + 2807 + webidl-conversions@8.0.1: {} 2808 + 2809 + whatwg-mimetype@5.0.0: {} 2810 + 2811 + whatwg-url@16.0.1: 2812 + dependencies: 2813 + '@exodus/bytes': 1.15.0 2814 + tr46: 6.0.0 2815 + webidl-conversions: 8.0.1 2816 + transitivePeerDependencies: 2817 + - '@noble/hashes' 2818 + 2819 + widest-line@6.0.0: 2820 + dependencies: 2821 + string-width: 8.2.1 2822 + 1811 2823 workerd@1.20260508.1: 1812 2824 optionalDependencies: 1813 2825 '@cloudflare/workerd-darwin-64': 1.20260508.1 ··· 1833 2845 - bufferutil 1834 2846 - utf-8-validate 1835 2847 2848 + wrap-ansi@10.0.0: 2849 + dependencies: 2850 + ansi-styles: 6.2.3 2851 + string-width: 8.2.1 2852 + strip-ansi: 7.2.0 2853 + 1836 2854 ws@8.18.0: {} 1837 2855 2856 + ws@8.20.1: {} 2857 + 2858 + wsl-utils@0.1.0: 2859 + dependencies: 2860 + is-wsl: 3.1.1 2861 + 2862 + xml-name-validator@5.0.0: {} 2863 + 2864 + xmlchars@2.2.0: {} 2865 + 1838 2866 yocto-queue@1.2.2: {} 2867 + 2868 + yoga-layout@3.2.1: {} 1839 2869 1840 2870 youch-core@0.3.3: 1841 2871 dependencies:
+22
src/cli-utils.ts
··· 1 + /** 2 + * CLI utilities for stigmergic commands 3 + */ 4 + 5 + import { existsSync } from 'node:fs'; 6 + import { readFile } from 'node:fs/promises'; 7 + 8 + export async function loadDotEnv(filePath: string): Promise<void> { 9 + if (!existsSync(filePath)) return; 10 + const content = await readFile(filePath, 'utf-8'); 11 + for (const line of content.split('\n')) { 12 + const trimmed = line.trim(); 13 + if (!trimmed || trimmed.startsWith('#')) continue; 14 + const eq = trimmed.indexOf('='); 15 + if (eq === -1) continue; 16 + const key = trimmed.slice(0, eq).trim(); 17 + const value = trimmed.slice(eq + 1).trim(); 18 + if (key && value) { 19 + process.env[key] = value.replace(/^["']|["']$/g, ''); 20 + } 21 + } 22 + }
+329
src/connection-detect.ts
··· 1 + /** 2 + * connection:detect 3 + * 4 + * Compare our carry graph against other DIDs' connection records. 5 + * Discovers DIDs publishing network.cosmik.connection records, 6 + * fetches their edge sets, and computes Jaccard similarity against our graph. 7 + * 8 + * Usage: 9 + * bun connection:detect # compare against all network DIDs 10 + * bun connection:detect --limit 10 # only check 10 DIDs 11 + * bun connection:detect --threshold 0.05 # only show similarity >= 0.05 12 + * bun connection:detect --json # machine-readable output 13 + * bun connection:detect --write # create connection records for top matches 14 + */ 15 + 16 + import { resolve, dirname } from 'node:path'; 17 + import { fileURLToPath } from 'node:url'; 18 + import { execFileSync } from 'node:child_process'; 19 + import { AtpAgent } from '@atproto/api'; 20 + import { loadDotEnv } from './cli-utils.js'; 21 + 22 + const __dirname = dirname(fileURLToPath(import.meta.url)); 23 + await loadDotEnv(resolve(__dirname, '..', '.env')); 24 + 25 + const RELAY = process.env.ATPROTO_RELAY ?? 'https://bsky.network'; 26 + const BLUESKY_HANDLE = process.env.BLUESKY_IDENTIFIER ?? ''; 27 + const BLUESKY_APP_PASSWORD = process.env.BLUESKY_APP_PASSWORD ?? ''; 28 + const ATPROTO_SERVICE = process.env.ATPROTO_SERVICE ?? 'https://bsky.social'; 29 + const PDS_SERVICE = process.env.PDS_SERVICE ?? 'https://amanita.us-east.host.bsky.network'; 30 + const CARRY_DIR = process.env.CARRY_DIR ?? '/home/nandi/.local/share/carry-vault'; 31 + const COLLECTION = 'network.cosmik.connection'; 32 + 33 + interface NetworkIsland { 34 + did: string; 35 + handle?: string; 36 + pds?: string; 37 + urls: Set<string>; 38 + edgeCount: number; 39 + } 40 + 41 + interface SimilarityResult { 42 + did: string; 43 + handle?: string; 44 + edgeCount: number; 45 + sharedCount: number; 46 + similarity: number; 47 + sharedUrls: string[]; 48 + } 49 + 50 + // --- Network discovery --- 51 + 52 + async function discoverDIDs(limit?: number): Promise<string[]> { 53 + const dids: string[] = []; 54 + let cursor = ''; 55 + while (true) { 56 + const url = new URL(`${RELAY}/xrpc/com.atproto.sync.listReposByCollection`); 57 + url.searchParams.set('collection', COLLECTION); 58 + url.searchParams.set('limit', '1000'); 59 + if (cursor) url.searchParams.set('cursor', cursor); 60 + 61 + const res = await fetch(url.toString()); 62 + if (!res.ok) { 63 + console.error(`Relay query failed (${res.status}): ${await res.text().catch(() => '')}`); 64 + break; 65 + } 66 + const data = await res.json() as { repos: Array<{ did: string }>; cursor?: string }; 67 + for (const r of data.repos) { 68 + dids.push(r.did); 69 + if (limit && dids.length >= limit) return dids; 70 + } 71 + cursor = data.cursor ?? ''; 72 + if (!cursor) break; 73 + await new Promise((r) => setTimeout(r, 200)); // rate limit 74 + } 75 + return dids; 76 + } 77 + 78 + async function resolvePDS(did: string): Promise<string | null> { 79 + try { 80 + const res = await fetch(`https://plc.directory/${did}`); 81 + if (!res.ok) return null; 82 + const doc = await res.json() as { service?: Array<{ id: string; serviceEndpoint: string }> }; 83 + const pds = doc.service?.find((s) => s.id === '#atproto_pds'); 84 + return pds?.serviceEndpoint ?? null; 85 + } catch { 86 + return null; 87 + } 88 + } 89 + 90 + async function resolveHandle(did: string): Promise<string | null> { 91 + try { 92 + const res = await fetch(`https://plc.directory/${did}`); 93 + if (!res.ok) return null; 94 + const doc = await res.json() as { alsoKnownAs?: string[] }; 95 + const handle = doc.alsoKnownAs?.find((h) => h.startsWith('at://')); 96 + return handle?.replace('at://', '') ?? null; 97 + } catch { 98 + return null; 99 + } 100 + } 101 + 102 + async function fetchConnectionRecords(pds: string, did: string): Promise<Array<{ source?: string; target?: string; connectionType?: string }>> { 103 + const records: Array<{ source?: string; target?: string; connectionType?: string }> = []; 104 + let cursor = ''; 105 + while (true) { 106 + const url = new URL(`${pds}/xrpc/com.atproto.repo.listRecords`); 107 + url.searchParams.set('repo', did); 108 + url.searchParams.set('collection', COLLECTION); 109 + url.searchParams.set('limit', '100'); 110 + if (cursor) url.searchParams.set('cursor', cursor); 111 + 112 + const res = await fetch(url.toString()); 113 + if (!res.ok) break; 114 + const data = await res.json() as { records: Array<{ value: { source?: string; target?: string; connectionType?: string } }>; cursor?: string }; 115 + for (const r of data.records) { 116 + records.push(r.value); 117 + } 118 + cursor = data.cursor ?? ''; 119 + if (!cursor) break; 120 + await new Promise((r) => setTimeout(r, 100)); 121 + } 122 + return records; 123 + } 124 + 125 + // --- Our graph --- 126 + 127 + function normalizeUrl(url: string): string { 128 + try { 129 + const u = new URL(url); 130 + u.hash = ''; 131 + if (u.hostname.startsWith('www.')) u.hostname = u.hostname.slice(4); 132 + return u.toString(); 133 + } catch { 134 + return url; 135 + } 136 + } 137 + 138 + function buildOurUrlSet(): Set<string> { 139 + const urls = new Set<string>(); 140 + 141 + // From carry connections 142 + const connections = carryQueryJson<{ source?: string; target?: string }>('org.latha.connection', ['source', 'target']); 143 + const entities = carryQueryJson<{ name?: string; url?: string }>('org.latha.entity', ['name', 'url']); 144 + const entityUrls = new Map<string, string>(); 145 + for (const e of entities) { 146 + if (e.name && e.url?.startsWith('https://')) { 147 + entityUrls.set(e.name, e.url); 148 + } 149 + } 150 + 151 + for (const c of connections) { 152 + const sUrl = entityUrls.get(c.source ?? '') ?? (c.source?.startsWith('https://') ? c.source : undefined); 153 + const tUrl = entityUrls.get(c.target ?? '') ?? (c.target?.startsWith('https://') ? c.target : undefined); 154 + if (sUrl) urls.add(normalizeUrl(sUrl)); 155 + if (tUrl) urls.add(normalizeUrl(tUrl)); 156 + } 157 + 158 + return urls; 159 + } 160 + 161 + function carryQueryJson<T>(domain: string, fields: string[]): T[] { 162 + try { 163 + const out = execFileSync('carry', ['query', domain, ...fields, '--format', 'json'], { 164 + cwd: CARRY_DIR, 165 + encoding: 'utf-8', 166 + timeout: 15_000, 167 + }); 168 + return JSON.parse(out) as T[]; 169 + } catch { 170 + return []; 171 + } 172 + } 173 + 174 + // --- Similarity --- 175 + 176 + function jaccard(a: Set<string>, b: Set<string>): number { 177 + if (a.size === 0 && b.size === 0) return 0; 178 + let intersection = 0; 179 + for (const item of a) { 180 + if (b.has(item)) intersection++; 181 + } 182 + const union = a.size + b.size - intersection; 183 + return union === 0 ? 0 : intersection / union; 184 + } 185 + 186 + function sharedUrls(a: Set<string>, b: Set<string>): string[] { 187 + const shared: string[] = []; 188 + for (const url of a) { 189 + if (b.has(url)) shared.push(url); 190 + } 191 + return shared.sort(); 192 + } 193 + 194 + // --- Main --- 195 + 196 + async function main(): Promise<void> { 197 + const args = process.argv.slice(2); 198 + const wantJson = args.includes('--json'); 199 + const wantWrite = args.includes('--write'); 200 + const limitArg = args.find((a) => a.startsWith('--limit=')); 201 + const limit = limitArg ? Number(limitArg.split('=')[1]) : undefined; 202 + const thresholdArg = args.find((a) => a.startsWith('--threshold=')); 203 + const threshold = thresholdArg ? Number(thresholdArg.split('=')[1]) : 0.01; 204 + 205 + console.error('Building our carry graph URL set...'); 206 + const ourUrls = buildOurUrlSet(); 207 + 208 + // Also fetch our own PDS connections 209 + const ourDid = await resolveOurDid(); 210 + if (ourDid) { 211 + const pds = await resolvePDS(ourDid); 212 + if (pds) { 213 + const ourRecords = await fetchConnectionRecords(pds, ourDid); 214 + for (const r of ourRecords) { 215 + if (r.source?.startsWith('https://')) ourUrls.add(normalizeUrl(r.source)); 216 + if (r.target?.startsWith('https://')) ourUrls.add(normalizeUrl(r.target)); 217 + } 218 + } 219 + } 220 + 221 + console.error(`Our graph: ${ourUrls.size} unique HTTPS URLs`); 222 + console.error('Discovering network DIDs...'); 223 + 224 + const dids = await discoverDIDs(limit); 225 + console.error(`Found ${dids.length} DIDs with ${COLLECTION} records`); 226 + 227 + // Skip our own DID 228 + const otherDids = ourDid ? dids.filter((d) => d !== ourDid) : dids; 229 + 230 + const results: SimilarityResult[] = []; 231 + 232 + for (let i = 0; i < otherDids.length; i++) { 233 + const did = otherDids[i]; 234 + console.error(`[${i + 1}/${otherDids.length}] Fetching ${did}...`); 235 + 236 + const pds = await resolvePDS(did); 237 + if (!pds) { 238 + console.error(` Could not resolve PDS`); 239 + continue; 240 + } 241 + 242 + const records = await fetchConnectionRecords(pds, did); 243 + const theirUrls = new Set<string>(); 244 + for (const r of records) { 245 + if (r.source?.startsWith('https://')) theirUrls.add(normalizeUrl(r.source)); 246 + if (r.target?.startsWith('https://')) theirUrls.add(normalizeUrl(r.target)); 247 + } 248 + 249 + const sim = jaccard(ourUrls, theirUrls); 250 + const shared = sharedUrls(ourUrls, theirUrls); 251 + 252 + if (sim >= threshold) { 253 + const handle = await resolveHandle(did); 254 + results.push({ 255 + did, 256 + handle: handle ?? undefined, 257 + edgeCount: records.length, 258 + sharedCount: shared.length, 259 + similarity: sim, 260 + sharedUrls: shared, 261 + }); 262 + } 263 + } 264 + 265 + results.sort((a, b) => b.similarity - a.similarity); 266 + 267 + if (wantWrite && results.length > 0) { 268 + const agent = new AtpAgent({ service: PDS_SERVICE }); 269 + if (BLUESKY_HANDLE && BLUESKY_APP_PASSWORD) { 270 + await agent.login({ identifier: BLUESKY_HANDLE, password: BLUESKY_APP_PASSWORD }); 271 + } 272 + let wrote = 0; 273 + for (const r of results.slice(0, 10)) { 274 + if (r.similarity < 0.05) continue; 275 + const profileUrl = r.handle ? `https://bsky.app/profile/${r.handle}` : `https://bsky.app/profile/${r.did}`; 276 + try { 277 + await agent.api.com.atproto.repo.createRecord({ 278 + repo: agent.session!.did, 279 + collection: COLLECTION, 280 + record: { 281 + $type: COLLECTION, 282 + source: profileUrl, 283 + target: `https://bsky.app/profile/${BLUESKY_HANDLE}`, 284 + connectionType: 'SIMILAR_ISLAND', 285 + note: `Island similarity: ${(r.similarity * 100).toFixed(1)}% (${r.sharedCount} shared URLs)`, 286 + createdAt: new Date().toISOString(), 287 + }, 288 + }); 289 + wrote++; 290 + } catch { 291 + // skip 292 + } 293 + } 294 + console.error(`Wrote ${wrote} similarity connection records`); 295 + } 296 + 297 + if (wantJson) { 298 + console.log(JSON.stringify({ ourUrlCount: ourUrls.size, didCount: otherDids.length, results }, null, 2)); 299 + return; 300 + } 301 + 302 + console.log(`\nConnection detect: compared against ${otherDids.length} network DIDs`); 303 + console.log(`Our graph: ${ourUrls.size} unique HTTPS URLs`); 304 + console.log(`Matches (similarity >= ${threshold}): ${results.length}\n`); 305 + 306 + for (const r of results.slice(0, 20)) { 307 + const label = r.handle ?? r.did; 308 + console.log(`${r.similarity.toFixed(4)} | ${label} (${r.edgeCount} edges, ${r.sharedCount} shared)`); 309 + for (const url of r.sharedUrls) { 310 + console.log(` ${url}`); 311 + } 312 + } 313 + } 314 + 315 + async function resolveOurDid(): Promise<string | null> { 316 + if (!BLUESKY_HANDLE) return null; 317 + try { 318 + const agent = new AtpAgent({ service: ATPROTO_SERVICE }); 319 + await agent.login({ identifier: BLUESKY_HANDLE, password: BLUESKY_APP_PASSWORD }); 320 + return agent.session!.did; 321 + } catch { 322 + return null; 323 + } 324 + } 325 + 326 + main().catch((err) => { 327 + console.error('Fatal:', err instanceof Error ? err.message : String(err)); 328 + process.exit(1); 329 + });
+298
src/island-detect.ts
··· 1 + /** 2 + * island:detect 3 + * 4 + * Match our carry graph against discovered components to find closest island. 5 + * Uses the SQLite cache from island:discover. 6 + * 7 + * Usage: 8 + * bun island:detect # find which component we're closest to 9 + * bun island:detect --top 5 # show top 5 closest components 10 + * bun island:detect --json # machine-readable output 11 + */ 12 + 13 + import { resolve, dirname } from 'node:path'; 14 + import { fileURLToPath } from 'node:url'; 15 + import { execFileSync } from 'node:child_process'; 16 + import { Database } from 'bun:sqlite'; 17 + import { AtpAgent } from '@atproto/api'; 18 + import { loadDotEnv } from './cli-utils.js'; 19 + 20 + const __dirname = dirname(fileURLToPath(import.meta.url)); 21 + await loadDotEnv(resolve(__dirname, '..', '.env')); 22 + 23 + const RELAY = process.env.ATPROTO_RELAY ?? 'https://bsky.network'; 24 + const BLUESKY_HANDLE = process.env.BLUESKY_IDENTIFIER ?? ''; 25 + const BLUESKY_APP_PASSWORD = process.env.BLUESKY_APP_PASSWORD ?? ''; 26 + const ATPROTO_SERVICE = process.env.ATPROTO_SERVICE ?? 'https://bsky.social'; 27 + const PDS_SERVICE = process.env.PDS_SERVICE ?? 'https://amanita.us-east.host.bsky.network'; 28 + const CARRY_DIR = process.env.CARRY_DIR ?? '/home/nandi/.local/share/carry-vault'; 29 + const DB_PATH = process.env.ISLAND_DETECT_DB ?? resolve(CARRY_DIR, '.island-detect.db'); 30 + const COLLECTION = 'network.cosmik.connection'; 31 + 32 + interface Component { 33 + id: number; 34 + nodes: Set<string>; 35 + dids: Set<string>; 36 + } 37 + 38 + interface MatchResult { 39 + componentId: number; 40 + nodeCount: number; 41 + didCount: number; 42 + sharedCount: number; 43 + similarity: number; 44 + sharedUrls: string[]; 45 + } 46 + 47 + // --- SQLite --- 48 + 49 + function openDb(): Database { 50 + const db = new Database(DB_PATH, { readonly: true }); 51 + return db; 52 + } 53 + 54 + // --- Our graph --- 55 + 56 + function normalizeUrl(url: string): string { 57 + try { 58 + const u = new URL(url); 59 + u.hash = ''; 60 + if (u.hostname.startsWith('www.')) u.hostname = u.hostname.slice(4); 61 + return u.toString(); 62 + } catch { 63 + return url; 64 + } 65 + } 66 + 67 + function carryQueryJson<T>(domain: string, fields: string[]): T[] { 68 + try { 69 + const out = execFileSync('carry', ['query', domain, ...fields, '--format', 'json'], { 70 + cwd: CARRY_DIR, 71 + encoding: 'utf-8', 72 + timeout: 15_000, 73 + }); 74 + return JSON.parse(out) as T[]; 75 + } catch { 76 + return []; 77 + } 78 + } 79 + 80 + function buildOurUrlSet(): Set<string> { 81 + const urls = new Set<string>(); 82 + 83 + // From carry connections 84 + const connections = carryQueryJson<{ source?: string; target?: string }>('org.latha.connection', ['source', 'target']); 85 + const entities = carryQueryJson<{ name?: string; url?: string }>('org.latha.entity', ['name', 'url']); 86 + const entityUrls = new Map<string, string>(); 87 + for (const e of entities) { 88 + if (e.name && e.url?.startsWith('https://')) { 89 + entityUrls.set(e.name, e.url); 90 + } 91 + } 92 + 93 + for (const c of connections) { 94 + const sUrl = entityUrls.get(c.source ?? '') ?? (c.source?.startsWith('https://') ? c.source : undefined); 95 + const tUrl = entityUrls.get(c.target ?? '') ?? (c.target?.startsWith('https://') ? c.target : undefined); 96 + if (sUrl) urls.add(normalizeUrl(sUrl)); 97 + if (tUrl) urls.add(normalizeUrl(tUrl)); 98 + } 99 + 100 + return urls; 101 + } 102 + 103 + async function fetchOurPdsUrls(): Promise<Set<string>> { 104 + const urls = new Set<string>(); 105 + if (!BLUESKY_HANDLE) return urls; 106 + 107 + try { 108 + const agent = new AtpAgent({ service: ATPROTO_SERVICE }); 109 + await agent.login({ identifier: BLUESKY_HANDLE, password: BLUESKY_APP_PASSWORD }); 110 + const did = agent.session!.did; 111 + 112 + // Resolve PDS 113 + const doc = await fetch(`https://plc.directory/${did}`).then((r) => r.json()) as { service?: Array<{ id: string; serviceEndpoint: string }> }; 114 + const pds = doc.service?.find((s) => s.id === '#atproto_pds')?.serviceEndpoint ?? PDS_SERVICE; 115 + 116 + // Fetch our connection records 117 + let cursor = ''; 118 + while (true) { 119 + const url = new URL(`${pds}/xrpc/com.atproto.repo.listRecords`); 120 + url.searchParams.set('repo', did); 121 + url.searchParams.set('collection', COLLECTION); 122 + url.searchParams.set('limit', '100'); 123 + if (cursor) url.searchParams.set('cursor', cursor); 124 + 125 + const res = await fetch(url.toString()); 126 + if (!res.ok) break; 127 + const data = await res.json() as { 128 + records: Array<{ value: { source?: string; target?: string } }>; 129 + cursor?: string; 130 + }; 131 + 132 + for (const r of data.records) { 133 + if (r.value.source?.startsWith('https://')) urls.add(normalizeUrl(r.value.source)); 134 + if (r.value.target?.startsWith('https://')) urls.add(normalizeUrl(r.value.target)); 135 + } 136 + 137 + cursor = data.cursor ?? ''; 138 + if (!cursor) break; 139 + } 140 + } catch {} 141 + 142 + return urls; 143 + } 144 + 145 + // --- Components --- 146 + 147 + function findComponents(db: Database): Component[] { 148 + const adj = new Map<string, Set<string>>(); 149 + const edgeDids = new Map<string, Set<string>>(); 150 + 151 + const rows = db.query('SELECT source, target, did FROM edges').all() as Array<{ source: string; target: string; did: string }>; 152 + for (const row of rows) { 153 + const s = row.source; 154 + const t = row.target; 155 + if (!adj.has(s)) adj.set(s, new Set()); 156 + if (!adj.has(t)) adj.set(t, new Set()); 157 + adj.get(s)!.add(t); 158 + adj.get(t)!.add(s); 159 + if (!edgeDids.has(s)) edgeDids.set(s, new Set()); 160 + if (!edgeDids.has(t)) edgeDids.set(t, new Set()); 161 + edgeDids.get(s)!.add(row.did); 162 + edgeDids.get(t)!.add(row.did); 163 + } 164 + 165 + const visited = new Set<string>(); 166 + const components: Component[] = []; 167 + let compId = 0; 168 + 169 + for (const node of adj.keys()) { 170 + if (visited.has(node)) continue; 171 + compId++; 172 + const compNodes = new Set<string>(); 173 + const compDids = new Set<string>(); 174 + const queue = [node]; 175 + while (queue.length > 0) { 176 + const current = queue.pop()!; 177 + if (visited.has(current)) continue; 178 + visited.add(current); 179 + compNodes.add(current); 180 + for (const did of edgeDids.get(current) ?? []) compDids.add(did); 181 + for (const neighbor of adj.get(current) ?? []) { 182 + if (!visited.has(neighbor)) queue.push(neighbor); 183 + } 184 + } 185 + components.push({ id: compId, nodes: compNodes, dids: compDids }); 186 + } 187 + 188 + return components.sort((a, b) => b.nodes.size - a.nodes.size); 189 + } 190 + 191 + function jaccard(a: Set<string>, b: Set<string>): number { 192 + if (a.size === 0 && b.size === 0) return 0; 193 + let intersection = 0; 194 + for (const item of a) { 195 + if (b.has(item)) intersection++; 196 + } 197 + const union = a.size + b.size - intersection; 198 + return union === 0 ? 0 : intersection / union; 199 + } 200 + 201 + function sharedUrls(a: Set<string>, b: Set<string>): string[] { 202 + const shared: string[] = []; 203 + for (const url of a) { 204 + if (b.has(url)) shared.push(url); 205 + } 206 + return shared.sort(); 207 + } 208 + 209 + // --- Main --- 210 + 211 + async function main(): Promise<void> { 212 + const args = process.argv.slice(2); 213 + const wantJson = args.includes('--json'); 214 + const topArg = args.find((a) => a.startsWith('--top=')); 215 + const topN = topArg ? Number(topArg.split('=')[1]) : 5; 216 + 217 + // Check if discover has been run 218 + let db: Database; 219 + try { 220 + db = openDb(); 221 + const count = (db.query('SELECT COUNT(*) as c FROM edges').get() as { c: number } | null)?.c ?? 0; 222 + if (count === 0) { 223 + console.error('No components found. Run `bun island:discover` first.'); 224 + process.exit(1); 225 + } 226 + } catch { 227 + console.error('Database not found. Run `bun island:discover` first.'); 228 + process.exit(1); 229 + } 230 + 231 + console.error('Building our carry graph URL set...'); 232 + const ourUrls = buildOurUrlSet(); 233 + const pdsUrls = await fetchOurPdsUrls(); 234 + for (const url of pdsUrls) ourUrls.add(url); 235 + console.error(`Our graph: ${ourUrls.size} unique HTTPS URLs`); 236 + 237 + console.error('Finding components...'); 238 + const components = findComponents(db); 239 + console.error(`Found ${components.length} components`); 240 + 241 + // Match against each component 242 + const matches: MatchResult[] = []; 243 + for (const comp of components) { 244 + const sim = jaccard(ourUrls, comp.nodes); 245 + const shared = sharedUrls(ourUrls, comp.nodes); 246 + if (shared.length > 0) { 247 + matches.push({ 248 + componentId: comp.id, 249 + nodeCount: comp.nodes.size, 250 + didCount: comp.dids.size, 251 + sharedCount: shared.length, 252 + similarity: sim, 253 + sharedUrls: shared, 254 + }); 255 + } 256 + } 257 + 258 + matches.sort((a, b) => b.similarity - a.similarity); 259 + const topMatches = matches.slice(0, topN); 260 + 261 + if (wantJson) { 262 + console.log(JSON.stringify({ 263 + ourUrlCount: ourUrls.size, 264 + componentCount: components.length, 265 + matchCount: matches.length, 266 + topMatches: topMatches.map((m) => ({ 267 + ...m, 268 + sharedUrls: m.sharedUrls.slice(0, 20), 269 + })), 270 + }, null, 2)); 271 + db.close(); 272 + return; 273 + } 274 + 275 + console.log(`\nIsland detect: our graph (${ourUrls.size} URLs) matched against ${components.length} components`); 276 + console.log(`Components with overlap: ${matches.length}\n`); 277 + 278 + for (const m of topMatches) { 279 + console.log(`Component ${m.componentId}: ${m.nodeCount} nodes, ${m.didCount} DIDs`); 280 + console.log(` Similarity: ${(m.similarity * 100).toFixed(2)}% (${m.sharedCount} shared URLs)`); 281 + for (const url of m.sharedUrls) { 282 + console.log(` ${url}`); 283 + } 284 + console.log(); 285 + } 286 + 287 + if (topMatches.length > 0) { 288 + const best = topMatches[0]; 289 + console.log(`\n→ We belong to Component ${best.componentId} (${(best.similarity * 100).toFixed(1)}% match)`); 290 + } 291 + 292 + db.close(); 293 + } 294 + 295 + main().catch((err) => { 296 + console.error('Fatal:', err instanceof Error ? err.message : String(err)); 297 + process.exit(1); 298 + });
+326
src/island-discover.ts
··· 1 + /** 2 + * island:discover 3 + * 4 + * Find connected components in the global network.cosmik.connection graph. 5 + * Uses SQLite to cache network data so we don't rebuild each time. 6 + * 7 + * Usage: 8 + * bun island:discover # find all connected components 9 + * bun island:discover --refresh # force re-fetch all DIDs 10 + * bun island:discover --component 1 # show full details for component 1 11 + * bun island:discover --min-size 5 # only show components with >= 5 nodes 12 + * bun island:discover --json # machine-readable output 13 + */ 14 + 15 + import { resolve, dirname } from 'node:path'; 16 + import { fileURLToPath } from 'node:url'; 17 + import { execFileSync } from 'node:fs'; 18 + import { Database } from 'bun:sqlite'; 19 + import { loadDotEnv } from './cli-utils.js'; 20 + 21 + const __dirname = dirname(fileURLToPath(import.meta.url)); 22 + await loadDotEnv(resolve(__dirname, '..', '.env')); 23 + 24 + const RELAY = process.env.ATPROTO_RELAY ?? 'https://bsky.network'; 25 + const CARRY_DIR = process.env.CARRY_DIR ?? '/home/nandi/.local/share/carry-vault'; 26 + const DB_PATH = process.env.ISLAND_DETECT_DB ?? resolve(CARRY_DIR, '.island-detect.db'); 27 + const COLLECTION = 'network.cosmik.connection'; 28 + const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour 29 + 30 + interface Component { 31 + id: number; 32 + nodes: Set<string>; 33 + dids: Set<string>; 34 + } 35 + 36 + // --- SQLite --- 37 + 38 + function openDb(): Database { 39 + const db = new Database(DB_PATH, { create: true }); 40 + db.exec('PRAGMA journal_mode = WAL'); 41 + db.exec('PRAGMA synchronous = NORMAL'); 42 + db.exec(` 43 + CREATE TABLE IF NOT EXISTS edges ( 44 + source TEXT NOT NULL, 45 + target TEXT NOT NULL, 46 + relation TEXT, 47 + did TEXT NOT NULL, 48 + fetched_at TEXT NOT NULL, 49 + PRIMARY KEY (source, target, did) 50 + ); 51 + CREATE TABLE IF NOT EXISTS dids ( 52 + did TEXT PRIMARY KEY, 53 + handle TEXT, 54 + pds TEXT, 55 + edge_count INTEGER DEFAULT 0, 56 + fetched_at TEXT 57 + ); 58 + CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source); 59 + CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target); 60 + `); 61 + return db; 62 + } 63 + 64 + // --- Network discovery --- 65 + 66 + async function discoverDIDs(): Promise<string[]> { 67 + const dids: string[] = []; 68 + let cursor = ''; 69 + while (true) { 70 + const url = new URL(`${RELAY}/xrpc/com.atproto.sync.listReposByCollection`); 71 + url.searchParams.set('collection', COLLECTION); 72 + url.searchParams.set('limit', '1000'); 73 + if (cursor) url.searchParams.set('cursor', cursor); 74 + const res = await fetch(url.toString()); 75 + if (!res.ok) break; 76 + const data = await res.json() as { repos: Array<{ did: string }>; cursor?: string }; 77 + for (const r of data.repos) dids.push(r.did); 78 + cursor = data.cursor ?? ''; 79 + if (!cursor) break; 80 + await new Promise((r) => setTimeout(r, 200)); 81 + } 82 + return dids; 83 + } 84 + 85 + async function resolvePDS(did: string): Promise<string | null> { 86 + try { 87 + const res = await fetch(`https://plc.directory/${did}`); 88 + if (!res.ok) return null; 89 + const doc = await res.json() as { service?: Array<{ id: string; serviceEndpoint: string }> }; 90 + return doc.service?.find((s) => s.id === '#atproto_pds')?.serviceEndpoint ?? null; 91 + } catch { return null; } 92 + } 93 + 94 + async function resolveHandle(did: string): Promise<string | null> { 95 + try { 96 + const res = await fetch(`https://plc.directory/${did}`); 97 + if (!res.ok) return null; 98 + const doc = await res.json() as { alsoKnownAs?: string[] }; 99 + return doc.alsoKnownAs?.find((h) => h.startsWith('at://'))?.replace('at://', '') ?? null; 100 + } catch { return null; } 101 + } 102 + 103 + async function fetchConnectionRecords(pds: string, did: string): Promise<Array<{ source: string; target: string; relation?: string }>> { 104 + const records: Array<{ source: string; target: string; relation?: string }> = []; 105 + let cursor = ''; 106 + while (true) { 107 + const url = new URL(`${pds}/xrpc/com.atproto.repo.listRecords`); 108 + url.searchParams.set('repo', did); 109 + url.searchParams.set('collection', COLLECTION); 110 + url.searchParams.set('limit', '100'); 111 + if (cursor) url.searchParams.set('cursor', cursor); 112 + const res = await fetch(url.toString()); 113 + if (!res.ok) break; 114 + const data = await res.json() as { 115 + records: Array<{ value: { source?: string; target?: string; connectionType?: string } }>; 116 + cursor?: string; 117 + }; 118 + for (const r of data.records) { 119 + if (r.value.source && r.value.target) { 120 + records.push({ source: r.value.source, target: r.value.target, relation: r.value.connectionType }); 121 + } 122 + } 123 + cursor = data.cursor ?? ''; 124 + if (!cursor) break; 125 + await new Promise((r) => setTimeout(r, 100)); 126 + } 127 + return records; 128 + } 129 + 130 + // --- Cache operations --- 131 + 132 + function isCached(db: Database, did: string): boolean { 133 + const row = db.query('SELECT fetched_at FROM dids WHERE did = ?').get(did) as { fetched_at: string } | null; 134 + if (!row) return false; 135 + return Date.now() - new Date(row.fetched_at).getTime() < CACHE_TTL_MS; 136 + } 137 + 138 + function upsertDid(db: Database, did: string, handle: string | null, pds: string | null, edgeCount: number): void { 139 + const now = new Date().toISOString(); 140 + db.query(` 141 + INSERT INTO dids (did, handle, pds, edge_count, fetched_at) 142 + VALUES (?, ?, ?, ?, ?) 143 + ON CONFLICT(did) DO UPDATE SET handle=?, pds=?, edge_count=?, fetched_at=? 144 + `).run(did, handle, pds, edgeCount, now, handle, pds, edgeCount, now); 145 + } 146 + 147 + function upsertEdges(db: Database, did: string, records: Array<{ source: string; target: string; relation?: string }>): void { 148 + const now = new Date().toISOString(); 149 + const insert = db.query(` 150 + INSERT INTO edges (source, target, relation, did, fetched_at) 151 + VALUES (?, ?, ?, ?, ?) 152 + ON CONFLICT(source, target, did) DO UPDATE SET relation=?, fetched_at=? 153 + `); 154 + // Delete stale edges for this DID before inserting 155 + db.query('DELETE FROM edges WHERE did = ?').run(did); 156 + for (const r of records) { 157 + insert.run(r.source, r.target, r.relation ?? null, did, now, r.relation ?? null, now); 158 + } 159 + } 160 + 161 + // --- Connected components --- 162 + 163 + function findComponents(db: Database): Component[] { 164 + // Build adjacency list from all edges 165 + const adj = new Map<string, Set<string>>(); 166 + const edgeDids = new Map<string, Set<string>>(); // node -> which DIDs have edges touching it 167 + 168 + const rows = db.query('SELECT source, target, did FROM edges').all() as Array<{ source: string; target: string; did: string }>; 169 + for (const row of rows) { 170 + const s = row.source; 171 + const t = row.target; 172 + if (!adj.has(s)) adj.set(s, new Set()); 173 + if (!adj.has(t)) adj.set(t, new Set()); 174 + adj.get(s)!.add(t); 175 + adj.get(t)!.add(s); 176 + if (!edgeDids.has(s)) edgeDids.set(s, new Set()); 177 + if (!edgeDids.has(t)) edgeDids.set(t, new Set()); 178 + edgeDids.get(s)!.add(row.did); 179 + edgeDids.get(t)!.add(row.did); 180 + } 181 + 182 + // BFS to find components 183 + const visited = new Set<string>(); 184 + const components: Component[] = []; 185 + let compId = 0; 186 + 187 + for (const node of adj.keys()) { 188 + if (visited.has(node)) continue; 189 + compId++; 190 + const compNodes = new Set<string>(); 191 + const compDids = new Set<string>(); 192 + const queue = [node]; 193 + while (queue.length > 0) { 194 + const current = queue.pop()!; 195 + if (visited.has(current)) continue; 196 + visited.add(current); 197 + compNodes.add(current); 198 + for (const did of edgeDids.get(current) ?? []) compDids.add(did); 199 + for (const neighbor of adj.get(current) ?? []) { 200 + if (!visited.has(neighbor)) queue.push(neighbor); 201 + } 202 + } 203 + components.push({ id: compId, nodes: compNodes, dids: compDids }); 204 + } 205 + 206 + return components.sort((a, b) => b.nodes.size - a.nodes.size); 207 + } 208 + 209 + // --- Main --- 210 + 211 + async function main(): Promise<void> { 212 + const args = process.argv.slice(2); 213 + const wantJson = args.includes('--json'); 214 + const refresh = args.includes('--refresh'); 215 + const componentArg = args.find((a) => a.startsWith('--component=')); 216 + const showComponent = componentArg ? Number(componentArg.split('=')[1]) : null; 217 + const minSizeArg = args.find((a) => a.startsWith('--min-size=')); 218 + const minSize = minSizeArg ? Number(minSizeArg.split('=')[1]) : 3; 219 + 220 + const db = openDb(); 221 + 222 + console.error('Discovering network DIDs...'); 223 + const networkDids = await discoverDIDs(); 224 + console.error(`Found ${networkDids.length} DIDs`); 225 + 226 + // Fetch edges for each DID (use cache unless --refresh) 227 + let fetched = 0; 228 + let cached = 0; 229 + for (let i = 0; i < networkDids.length; i++) { 230 + const did = networkDids[i]; 231 + if (!refresh && isCached(db, did)) { 232 + cached++; 233 + continue; 234 + } 235 + console.error(`[${i + 1}/${networkDids.length}] Fetching ${did}...`); 236 + const pds = await resolvePDS(did); 237 + if (!pds) { console.error(' Could not resolve PDS'); continue; } 238 + const handle = await resolveHandle(did); 239 + const records = await fetchConnectionRecords(pds, did); 240 + upsertDid(db, did, handle, pds, records.length); 241 + upsertEdges(db, did, records); 242 + fetched++; 243 + } 244 + console.error(`Fetched: ${fetched}, Cached: ${cached}`); 245 + 246 + // Stats 247 + const totalEdges = (db.query('SELECT COUNT(*) as c FROM edges').get() as { c: number }).c; 248 + const totalDids = (db.query('SELECT COUNT(*) as c FROM dids').get() as { c: number }).c; 249 + const uniqueUrls = new Set<string>(); 250 + const urlRows = db.query('SELECT DISTINCT source FROM edges UNION SELECT DISTINCT target FROM edges').all() as Array<{ source: string }>; 251 + for (const r of urlRows) uniqueUrls.add(r.source); 252 + 253 + console.error(`Database: ${totalEdges} edges, ${totalDids} DIDs, ${uniqueUrls.size} unique URLs`); 254 + 255 + // Find connected components 256 + const components = findComponents(db); 257 + const filtered = components.filter((c) => c.nodes.size >= minSize); 258 + 259 + if (showComponent !== null) { 260 + const comp = components[showComponent - 1]; 261 + if (!comp) { console.error(`Component ${showComponent} not found`); process.exit(1); } 262 + const didRows = db.query('SELECT did, handle, edge_count FROM dids WHERE did IN (?)').all(...comp.dids) as Array<{ did: string; handle: string | null; edge_count: number }>; 263 + if (wantJson) { 264 + console.log(JSON.stringify({ 265 + id: comp.id, 266 + nodeCount: comp.nodes.size, 267 + didCount: comp.dids.size, 268 + nodes: [...comp.nodes].sort(), 269 + dids: didRows, 270 + }, null, 2)); 271 + } else { 272 + console.log(`Component ${showComponent}: ${comp.nodes.size} nodes, ${comp.dids.size} DIDs`); 273 + console.log('\nNodes:'); 274 + for (const url of [...comp.nodes].sort()) console.log(` ${url}`); 275 + console.log('\nDIDs:'); 276 + for (const d of didRows) console.log(` ${d.handle ?? d.did} (${d.edge_count} edges)`); 277 + } 278 + db.close(); 279 + return; 280 + } 281 + 282 + if (wantJson) { 283 + console.log(JSON.stringify({ 284 + totalEdges, 285 + totalDids, 286 + uniqueUrls: uniqueUrls.size, 287 + componentCount: filtered.length, 288 + components: filtered.map((c) => ({ 289 + id: c.id, 290 + nodeCount: c.nodes.size, 291 + didCount: c.dids.size, 292 + sampleNodes: [...c.nodes].sort().slice(0, 10), 293 + dids: [...c.dids], 294 + })), 295 + }, null, 2)); 296 + db.close(); 297 + return; 298 + } 299 + 300 + console.log(`\nIsland detect: ${totalDids} DIDs, ${totalEdges} edges, ${uniqueUrls.size} unique URLs`); 301 + console.log(`Connected components (>= ${minSize} nodes): ${filtered.length}\n`); 302 + 303 + for (const comp of filtered.slice(0, 30)) { 304 + const sampleNodes = [...comp.nodes].sort().slice(0, 5); 305 + console.log(`Component ${comp.id}: ${comp.nodes.size} nodes, ${comp.dids.size} DIDs`); 306 + console.log(` URLs: ${sampleNodes.join(', ')}${comp.nodes.size > 5 ? ` ... (+${comp.nodes.size - 5})` : ''}`); 307 + } 308 + 309 + // Show which component we're in (if we can determine our DID) 310 + const ourDid = process.env.BLUESKY_IDENTIFIER 311 + ? (db.query('SELECT did FROM dids WHERE handle = ? OR did LIKE ?').get(process.env.BLUESKY_IDENTIFIER, `%${process.env.BLUESKY_IDENTIFIER}%`) as { did: string } | null) 312 + : null; 313 + if (ourDid) { 314 + const ourComp = components.find((c) => c.dids.has(ourDid.did)); 315 + if (ourComp) { 316 + console.log(`\nOur island: Component ${ourComp.id} (${ourComp.nodes.size} nodes, shared with ${ourComp.dids.size - 1} other DIDs)`); 317 + } 318 + } 319 + 320 + db.close(); 321 + } 322 + 323 + main().catch((err) => { 324 + console.error('Fatal:', err instanceof Error ? err.message : String(err)); 325 + process.exit(1); 326 + });
+382
src/island-explore.ts
··· 1 + /** 2 + * island:explore <aturi> 3 + * 4 + * Local-first island explorer that ingests an org.latha.strata record, identifies 5 + * fringe nodes, and emits endofunctor suggestions for re-attaching them to the island. 6 + * 7 + * Scoring is carry-backed: crawled page text is matched against the carry entity 8 + * and citation index, and overlap with island edges determines confidence. 9 + */ 10 + 11 + import { resolve, dirname } from 'node:path'; 12 + import { fileURLToPath } from 'node:url'; 13 + import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; 14 + import { execFileSync } from 'node:child_process'; 15 + import { AtpAgent } from '@atproto/api'; 16 + import { loadDotEnv } from './cli-utils.js'; 17 + import { JSDOM, VirtualConsole } from 'jsdom'; 18 + 19 + const __dirname = dirname(fileURLToPath(import.meta.url)); 20 + await loadDotEnv(resolve(__dirname, '..', '.env')); 21 + 22 + const SEMBLE_API = 'https://api.semble.so'; 23 + const BLUESKY_HANDLE = process.env.BLUESKY_IDENTIFIER ?? ''; 24 + const BLUESKY_APP_PASSWORD = process.env.BLUESKY_APP_PASSWORD ?? ''; 25 + const ATPROTO_SERVICE = process.env.ATPROTO_SERVICE ?? 'https://bsky.social'; 26 + const PDS_IDENTIFIER = process.env.PDS_IDENTIFIER ?? BLUESKY_HANDLE; 27 + const PDS_APP_PASSWORD = process.env.PDS_APP_PASSWORD ?? BLUESKY_APP_PASSWORD; 28 + const PDS_SERVICE = process.env.PDS_SERVICE ?? 'https://amanita.us-east.host.bsky.network'; 29 + const CARRY_DIR = process.env.CARRY_DIR ?? '/home/nandi/.local/share/carry-vault'; 30 + const FEED_LIMIT = Number(process.env.ISLAND_EXPLORE_LIMIT ?? '50'); 31 + const PROVENANCE_PATH = process.env.ISLAND_EXPLORE_PROVENANCE ?? resolve(CARRY_DIR, '.island-explore.json'); 32 + 33 + interface IslandRecord { 34 + $type?: string; 35 + source?: { uri?: string; collection?: string }; 36 + analysis?: { title?: string; themes?: string[]; highlights?: string[]; tensions?: string[]; openQuestions?: string[]; synthesis?: string }; 37 + edges?: Array<{ source?: string; target?: string; relation?: string; title?: string; description?: string }>; 38 + } 39 + 40 + interface CardContent { url?: string; title?: string; description?: string; author?: string; siteName?: string; type?: string; } 41 + interface FeedItem { 42 + id: string; 43 + user: { handle?: string; name?: string }; 44 + createdAt: string; 45 + activityType: 'CARD_COLLECTED' | 'CONNECTION_CREATED'; 46 + card?: { url?: string; cardContent?: CardContent }; 47 + connection?: { sourceCard?: { url?: string; title?: string; cardContent?: CardContent }; targetCard?: { url?: string; title?: string; cardContent?: CardContent }; relation?: string }; 48 + } 49 + interface FeedResponse { activities: FeedItem[]; } 50 + 51 + type Suggestion = { source: string; target: string; connectionType: string; confidence: number; evidence: string; fringe: boolean; }; 52 + 53 + interface CarryEntity { name: string; url: string; description: string } 54 + interface CarryCitation { url: string; title: string; takeaway: string; domain: string } 55 + interface CarryIndex { 56 + entitiesByName: Map<string, CarryEntity>; 57 + entitiesByUrl: Map<string, CarryEntity>; 58 + citationsByUrl: Map<string, CarryCitation>; 59 + islandEntityUrls: Set<string>; 60 + } 61 + 62 + function normalizeUrl(url: string | undefined): string | null { 63 + if (!url) return null; 64 + try { 65 + const u = new URL(url); 66 + u.hash = ''; 67 + if (u.hostname.startsWith('www.')) u.hostname = u.hostname.slice(4); 68 + if (u.protocol !== 'https:') u.protocol = 'https:'; 69 + return u.toString(); 70 + } catch { 71 + return url.trim().startsWith('https://') ? url.trim() : null; 72 + } 73 + } 74 + 75 + function isHttps(url: string | null): url is string { 76 + return !!url && url.startsWith('https://'); 77 + } 78 + 79 + function carryQueryJson<T>(domain: string, fields: string[]): T[] { 80 + try { 81 + const out = execFileSync('carry', ['query', domain, ...fields, '--format', 'json'], { 82 + cwd: CARRY_DIR, 83 + encoding: 'utf-8', 84 + timeout: 15_000, 85 + }); 86 + return JSON.parse(out) as T[]; 87 + } catch { 88 + return []; 89 + } 90 + } 91 + 92 + function buildCarryIndex(islandTargets: Set<string>): CarryIndex { 93 + const entitiesByName = new Map<string, CarryEntity>(); 94 + const entitiesByUrl = new Map<string, CarryEntity>(); 95 + 96 + const rawEntities = carryQueryJson<{ name?: string; url?: string; description?: string }>('org.latha.entity', ['name', 'url', 'description']); 97 + for (const e of rawEntities) { 98 + if (!e.name) continue; 99 + const ent: CarryEntity = { name: e.name, url: e.url ?? '', description: e.description ?? '' }; 100 + entitiesByName.set(e.name.toLowerCase(), ent); 101 + if (e.url) { 102 + const norm = normalizeUrl(e.url); 103 + if (norm) entitiesByUrl.set(norm, ent); 104 + } 105 + } 106 + 107 + const citationsByUrl = new Map<string, CarryCitation>(); 108 + const rawCitations = carryQueryJson<{ url?: string; title?: string; takeaway?: string; domain?: string }>('org.latha.citation', ['url', 'title', 'takeaway', 'domain']); 109 + for (const c of rawCitations) { 110 + if (!c.url) continue; 111 + const norm = normalizeUrl(c.url); 112 + if (norm) citationsByUrl.set(norm, { url: norm, title: c.title ?? '', takeaway: c.takeaway ?? '', domain: c.domain ?? '' }); 113 + } 114 + 115 + const islandEntityUrls = new Set<string>(); 116 + for (const target of islandTargets) { 117 + if (entitiesByUrl.has(target)) islandEntityUrls.add(target); 118 + if (citationsByUrl.has(target)) islandEntityUrls.add(target); 119 + } 120 + 121 + return { entitiesByName, entitiesByUrl, citationsByUrl, islandEntityUrls }; 122 + } 123 + 124 + function extractCarryRefs(text: string, index: CarryIndex): { entities: CarryEntity[]; citations: CarryCitation[]; islandHits: number } { 125 + const lower = text.toLowerCase(); 126 + const foundEntities: CarryEntity[] = []; 127 + const foundCitations: CarryCitation[] = []; 128 + const seen = new Set<string>(); 129 + let islandHits = 0; 130 + 131 + for (const [name, entity] of index.entitiesByName) { 132 + if (name.length < 4 || seen.has(name)) continue; 133 + if (lower.includes(name)) { 134 + seen.add(name); 135 + foundEntities.push(entity); 136 + const norm = normalizeUrl(entity.url); 137 + if (norm && index.islandEntityUrls.has(norm)) islandHits++; 138 + } 139 + } 140 + 141 + const urlPattern = /https?:\/\/[^\s)"'<>]+/gi; 142 + const urls = lower.match(urlPattern) ?? []; 143 + for (const raw of urls) { 144 + const norm = normalizeUrl(raw); 145 + if (!norm || seen.has(norm)) continue; 146 + seen.add(norm); 147 + const citation = index.citationsByUrl.get(norm); 148 + if (citation) { 149 + foundCitations.push(citation); 150 + if (index.islandEntityUrls.has(norm)) islandHits++; 151 + } 152 + const entity = index.entitiesByUrl.get(norm); 153 + if (entity && !foundEntities.includes(entity)) { 154 + foundEntities.push(entity); 155 + if (index.islandEntityUrls.has(norm)) islandHits++; 156 + } 157 + } 158 + 159 + return { entities: foundEntities, citations: foundCitations, islandHits }; 160 + } 161 + 162 + function scoreCarryOverlap(refs: ReturnType<typeof extractCarryRefs>, islandTargets: Set<string>): { score: number; evidence: string; type: string } { 163 + const totalRefs = refs.entities.length + refs.citations.length; 164 + if (totalRefs === 0) return { score: 0, evidence: 'no carry refs found', type: 'RELATED' }; 165 + 166 + const islandRatio = refs.islandHits / totalRefs; 167 + const score = Math.min(0.97, 0.3 + islandRatio * 0.5 + Math.min(totalRefs, 10) * 0.02); 168 + 169 + const entityNames = refs.entities.map((e) => e.name).slice(0, 8); 170 + const citationTitles = refs.citations.map((c) => c.title).slice(0, 4); 171 + const evidence = [ 172 + entityNames.length > 0 ? `entities: ${entityNames.join(', ')}` : '', 173 + citationTitles.length > 0 ? `citations: ${citationTitles.join(', ')}` : '', 174 + `island overlap: ${refs.islandHits}/${totalRefs}`, 175 + ].filter(Boolean).join('; '); 176 + 177 + const type = islandRatio > 0.5 ? 'LEADS_TO' 178 + : islandRatio > 0.2 ? 'SUPPORTS' 179 + : refs.entities.some((e) => indexIsInIsland(e, islandTargets)) ? 'ANNOTATES' 180 + : 'RELATED'; 181 + 182 + return { score, evidence, type }; 183 + } 184 + 185 + function indexIsInIsland(entity: CarryEntity, islandTargets: Set<string>): boolean { 186 + const norm = normalizeUrl(entity.url); 187 + return !!norm && islandTargets.has(norm); 188 + } 189 + 190 + async function loginToSemble(): Promise<string> { 191 + if (!BLUESKY_HANDLE || !BLUESKY_APP_PASSWORD) throw new Error('BLUESKY_IDENTIFIER and BLUESKY_APP_PASSWORD must be set'); 192 + const res = await fetch(`${SEMBLE_API}/api/users/login/app-password`, { 193 + method: 'POST', headers: { 'Content-Type': 'application/json' }, 194 + body: JSON.stringify({ identifier: BLUESKY_HANDLE, appPassword: BLUESKY_APP_PASSWORD }), redirect: 'manual', 195 + }); 196 + if (res.status !== 200 && res.status !== 302) { 197 + const body = await res.text().catch(() => ''); 198 + throw new Error(`Semble login failed (${res.status}): ${body.slice(0, 200)}`); 199 + } 200 + const setCookieHeaders = res.headers.getSetCookie?.() ?? []; 201 + if (setCookieHeaders.length === 0) { 202 + const rawCookie = res.headers.get('set-cookie'); 203 + if (rawCookie) setCookieHeaders.push(rawCookie); 204 + } 205 + if (setCookieHeaders.length === 0) throw new Error('Semble login succeeded but no cookies returned'); 206 + return setCookieHeaders.map((c: string) => c.split(';')[0].trim()).join('; '); 207 + } 208 + 209 + async function fetchFollowingFeed(cookies: string): Promise<FeedItem[]> { 210 + const url = new URL(`${SEMBLE_API}/api/feeds/following`); 211 + url.searchParams.set('limit', String(FEED_LIMIT)); 212 + const res = await fetch(url.toString(), { headers: { Cookie: cookies, Accept: 'application/json' } }); 213 + if (!res.ok) { 214 + const body = await res.text().catch(() => ''); 215 + throw new Error(`Semble feed fetch failed (${res.status}): ${body.slice(0, 200)}`); 216 + } 217 + const data = (await res.json()) as FeedResponse; 218 + return data.activities ?? []; 219 + } 220 + 221 + async function scrapeUrlText(url: string): Promise<string | null> { 222 + try { 223 + const res = await fetch(url, { signal: AbortSignal.timeout(10_000), headers: { 'User-Agent': 'Mozilla/5.0 (compatible; island-explore/1.0)', Accept: 'text/html' }, redirect: 'follow' }); 224 + if (!res.ok) return null; 225 + const dom = new JSDOM(await res.text(), { url, virtualConsole: new VirtualConsole({ silent: true }) }); 226 + const doc = dom.window.document; 227 + for (const el of doc.querySelectorAll('nav, script, style, footer, header, aside')) el.remove(); 228 + const root = doc.querySelector('article') ?? doc.querySelector('main') ?? doc.body; 229 + const text = root?.textContent?.replace(/\s+/g, ' ').trim() ?? ''; 230 + return text.length >= 50 ? text.slice(0, 5000) : null; 231 + } catch { 232 + return null; 233 + } 234 + } 235 + 236 + async function fetchIslandRecord(aturi: string): Promise<IslandRecord> { 237 + const localPath = resolve(CARRY_DIR, '.islands', `${aturi.replace(/[^a-zA-Z0-9._-]+/g, '_')}.json`); 238 + if (existsSync(localPath)) return JSON.parse(readFileSync(localPath, 'utf-8')) as IslandRecord; 239 + const agent = new AtpAgent({ service: ATPROTO_SERVICE }); 240 + await agent.login({ identifier: BLUESKY_HANDLE, password: BLUESKY_APP_PASSWORD }); 241 + const parts = aturi.split('/'); 242 + if (parts.length < 5) throw new Error(`Invalid AT URI: ${aturi}`); 243 + const collection = parts[3]; 244 + const rkey = parts[4]; 245 + const res = await agent.api.com.atproto.repo.getRecord({ repo: parts[2], collection, rkey }); 246 + return res.data.value as IslandRecord; 247 + } 248 + 249 + function islandEdgeTargets(island: IslandRecord): string[] { 250 + return [...new Set((island.edges ?? []).map((e) => normalizeUrl(e.target)).filter(isHttps))]; 251 + } 252 + 253 + function carryAssertEdge(source: string, target: string, relation: string, context: string): void { 254 + const payload = [`edge:`, ` org.latha.connection:`, ` source: ${JSON.stringify(source)}`, ` target: ${JSON.stringify(target)}`, ` relation: ${JSON.stringify(relation)}`, ` context: ${JSON.stringify(context.slice(0, 1000))}`, ''].join('\n'); 255 + execFileSync('carry', ['assert', '-'], { cwd: CARRY_DIR, input: payload, encoding: 'utf-8' }); 256 + } 257 + 258 + async function createPdsEdge(agent: AtpAgent, source: string, target: string, connectionType: string, note: string): Promise<string | null> { 259 + const r = await agent.api.com.atproto.repo.createRecord({ repo: agent.session!.did, collection: 'network.cosmik.connection', record: { $type: 'network.cosmik.connection', source, target, connectionType, note, createdAt: new Date().toISOString() } }); 260 + return r.data.uri; 261 + } 262 + 263 + async function main(): Promise<void> { 264 + const aturi = process.argv[2]; 265 + if (!aturi) throw new Error('Usage: bun island:explore <aturi> [--json] [--write]'); 266 + const wantJson = process.argv.includes('--json'); 267 + const wantWrite = process.argv.includes('--write'); 268 + const limitArg = process.argv.find((arg) => arg.startsWith('--limit=')); 269 + const limit = limitArg ? Number(limitArg.split('=')[1]) : FEED_LIMIT; 270 + 271 + const island = await fetchIslandRecord(aturi); 272 + const targets = islandEdgeTargets(island); 273 + if (targets.length === 0) { 274 + throw new Error(`Island record has no canonical https edge URLs in edges (found ${island.edges?.length ?? 0} edges)`); 275 + } 276 + const islandTargetSet = new Set(targets); 277 + 278 + console.error(`Building carry index...`); 279 + const carryIndex = buildCarryIndex(islandTargetSet); 280 + console.error(`Carry index: ${carryIndex.entitiesByName.size} entities, ${carryIndex.citationsByUrl.size} citations, ${carryIndex.islandEntityUrls.size} island overlaps`); 281 + 282 + const cookies = await loginToSemble(); 283 + const feed = (await fetchFollowingFeed(cookies)).slice(0, limit); 284 + 285 + const entityIndex = new Map<string, { url?: string }>(); 286 + for (const e of carryQueryJson<{ name?: string; url?: string }>('org.latha.entity', ['name', 'url'])) { 287 + if (e.url) entityIndex.set(normalizeUrl(e.url) ?? e.url, e); 288 + } 289 + const carryConnections = new Set<string>(); 290 + 291 + const fringeUrls = new Map<string, CardContent>(); 292 + for (const item of feed) { 293 + const url = item.activityType === 'CARD_COLLECTED' 294 + ? normalizeUrl(item.card?.cardContent?.url ?? item.card?.url) 295 + : normalizeUrl(item.connection?.sourceCard?.url ?? item.connection?.targetCard?.url); 296 + const card = item.activityType === 'CARD_COLLECTED' ? item.card?.cardContent : item.connection?.targetCard?.cardContent ?? item.connection?.sourceCard?.cardContent; 297 + if (url && card && !entityIndex.has(url)) fringeUrls.set(url, card); 298 + } 299 + 300 + const suggestions: Suggestion[] = []; 301 + for (const [url, card] of fringeUrls) { 302 + const crawled = await scrapeUrlText(url); 303 + if (!crawled) continue; 304 + const text = `${card.title ?? ''} ${card.description ?? ''} ${crawled}`; 305 + const refs = extractCarryRefs(text, carryIndex); 306 + if (refs.entities.length + refs.citations.length === 0) continue; 307 + const scored = scoreCarryOverlap(refs, islandTargetSet); 308 + if (scored.score < 0.3) continue; 309 + // Suggest edges to the specific island targets that overlap 310 + const overlapTargets = refs.entities 311 + .map((e) => normalizeUrl(e.url)) 312 + .filter((u): u is string => !!u && islandTargetSet.has(u)); 313 + const citationOverlapTargets = refs.citations 314 + .map((c) => c.url) 315 + .filter((u) => islandTargetSet.has(u)); 316 + const allTargets = [...new Set([...overlapTargets, ...citationOverlapTargets])]; 317 + if (allTargets.length === 0) { 318 + // No direct overlap — suggest to the first island target as a weaker connection 319 + suggestions.push({ source: url, target: targets[0], connectionType: scored.type, confidence: scored.score * 0.6, evidence: `${scored.evidence} (no direct island overlap)`, fringe: true }); 320 + } else { 321 + for (const target of allTargets) { 322 + suggestions.push({ source: url, target, connectionType: scored.type, confidence: scored.score, evidence: scored.evidence, fringe: true }); 323 + } 324 + } 325 + } 326 + 327 + const output = { 328 + island: { aturi, title: island.analysis?.title ?? island.source?.uri ?? 'Island', targets, carryEntities: carryIndex.entitiesByName.size, carryCitations: carryIndex.citationsByUrl.size, islandOverlaps: carryIndex.islandEntityUrls.size }, 329 + fringeCount: fringeUrls.size, 330 + suggestionCount: suggestions.length, 331 + suggestions: suggestions.sort((a, b) => b.confidence - a.confidence), 332 + writeEnabled: wantWrite, 333 + }; 334 + 335 + if (wantWrite) { 336 + const pds = new AtpAgent({ service: PDS_SERVICE }); 337 + if (PDS_IDENTIFIER && PDS_APP_PASSWORD) await pds.login({ identifier: PDS_IDENTIFIER, password: PDS_APP_PASSWORD }); 338 + let wrote = 0; 339 + for (const suggestion of output.suggestions) { 340 + if (suggestion.confidence < 0.6) continue; 341 + const source = normalizeUrl(suggestion.source); 342 + const target = normalizeUrl(suggestion.target); 343 + if (!isHttps(source) || !isHttps(target)) continue; 344 + const key = `${source}::${target}`; 345 + if (carryConnections.has(key)) continue; 346 + carryAssertEdge(source, target, suggestion.connectionType, suggestion.evidence); 347 + carryConnections.add(key); 348 + const uri = await createPdsEdge(pds, source, target, suggestion.connectionType, suggestion.evidence); 349 + if (uri) { 350 + wrote += 1; 351 + console.log(`Wrote edge: ${uri}`); 352 + } 353 + } 354 + saveJson(PROVENANCE_PATH, { aturi, wrote, wroteAt: new Date().toISOString(), suggestions: output.suggestions }); 355 + output.writeEnabled = true; 356 + } 357 + 358 + if (wantJson) { 359 + console.log(JSON.stringify(output, null, 2)); 360 + return; 361 + } 362 + 363 + console.log(`Island: ${output.island.title} (${aturi})`); 364 + console.log(`Targets: ${output.island.targets.length} edge URLs`); 365 + console.log(`Carry: ${output.island.carryEntities} entities, ${output.island.carryCitations} citations, ${output.island.islandOverlaps} island overlaps`); 366 + console.log(`Fringe nodes: ${output.fringeCount}`); 367 + console.log(`Suggestions: ${output.suggestionCount}`); 368 + for (const s of output.suggestions.slice(0, 20)) { 369 + console.log(`- ${s.connectionType} ${s.source} -> ${s.target} (${s.confidence.toFixed(2)})`); 370 + console.log(` ${s.evidence}`); 371 + } 372 + } 373 + 374 + function saveJson(path: string, value: unknown): void { 375 + mkdirSync(dirname(path), { recursive: true }); 376 + writeFileSync(path, JSON.stringify(value, null, 2) + '\n', 'utf-8'); 377 + } 378 + 379 + main().catch((err) => { 380 + console.error('Fatal:', err instanceof Error ? err.message : String(err)); 381 + process.exit(1); 382 + });
+260
src/vault-analyze.ts
··· 1 + /** 2 + * vault:analyze 3 + * 4 + * Parse Obsidian vault notes into structured carry data using a letta-code-sdk agent. 5 + * 6 + * Usage: 7 + * bun vault:analyze # process all vault notes 8 + * bun vault:analyze --note "Astera Institute" # process single note 9 + * bun vault:analyze --force # reprocess already-analyzed notes 10 + * bun vault:analyze --dry-run # show what would be extracted 11 + * bun vault:analyze --limit 5 # only process 5 notes 12 + */ 13 + 14 + import { resolve, dirname } from 'node:path'; 15 + import { fileURLToPath } from 'node:url'; 16 + import { readFileSync, readdirSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; 17 + import { execFileSync } from 'node:child_process'; 18 + import { 19 + createAgent, 20 + createSession, 21 + type Session, 22 + type AnyAgentTool, 23 + type AgentToolResult, 24 + type SDKResultMessage, 25 + } from '@letta-ai/letta-code-sdk'; 26 + import { loadDotEnv } from './cli-utils.js'; 27 + 28 + const __dirname = dirname(fileURLToPath(import.meta.url)); 29 + await loadDotEnv(resolve(__dirname, '..', '.env')); 30 + 31 + const VAULT_DIR = process.env.VAULT_DIR ?? '/home/nandi/vault'; 32 + const CARRY_DIR = process.env.CARRY_DIR ?? '/home/nandi/.local/share/carry-vault'; 33 + const PROVENANCE_DIR = resolve(CARRY_DIR, '.vault-analyze'); 34 + 35 + interface ExtractedItem { 36 + type: 'entity' | 'citation' | 'connection'; 37 + data: Record<string, string>; 38 + } 39 + 40 + // --- carry_assert tool --- 41 + 42 + function carryAssert(type: string, data: Record<string, string>): string { 43 + const fields = Object.entries(data) 44 + .filter(([, v]) => v && v.trim()) 45 + .map(([k, v]) => `${k}=${JSON.stringify(v)}`) 46 + .join(' '); 47 + const domain = type === 'entity' ? 'org.latha.entity' 48 + : type === 'citation' ? 'org.latha.citation' 49 + : 'org.latha.connection'; 50 + try { 51 + const out = execFileSync('carry', ['assert', domain, ...fields.split(' ')], { 52 + cwd: CARRY_DIR, 53 + encoding: 'utf-8', 54 + timeout: 10_000, 55 + }); 56 + return out.trim() || 'ok'; 57 + } catch (err: any) { 58 + return `error: ${err?.message ?? err}`; 59 + } 60 + } 61 + 62 + const carryAssertTool: AnyAgentTool = { 63 + label: 'carry_assert', 64 + name: 'carry_assert', 65 + description: `Assert structured data into carry. Call for each entity, citation, or connection you extract from the note. 66 + 67 + For entities: {type: "entity", data: {name, url, description}} 68 + For citations: {type: "citation", data: {url, title, takeaway, domain, confidence}} 69 + For connections: {type: "connection", data: {source, target, relation}} 70 + 71 + - Entity names should be canonical (e.g. "Ink and Switch" not "Ink & Switch") 72 + - URLs must be real HTTPS URLs found in the note 73 + - Connection source/target should be entity names, not URLs 74 + - Confidence should be one of: org.latha.strata.defs#high, org.latha.strata.defs#medium, org.latha.strata.defs#low 75 + - Relation should be lowercase-hyphenated (e.g. "related-to", "director", "division-of", "member-of")`, 76 + parameters: { 77 + type: 'object', 78 + properties: { 79 + type: { 80 + type: 'string', 81 + enum: ['entity', 'citation', 'connection'], 82 + description: 'Type of carry record to assert', 83 + }, 84 + data: { 85 + type: 'object', 86 + description: 'Fields for the carry record', 87 + properties: { 88 + name: { type: 'string', description: 'Entity name' }, 89 + url: { type: 'string', description: 'HTTPS URL' }, 90 + description: { type: 'string', description: 'Short description' }, 91 + title: { type: 'string', description: 'Citation title' }, 92 + takeaway: { type: 'string', description: 'Key insight from citation' }, 93 + domain: { type: 'string', description: 'Domain/discipline' }, 94 + confidence: { type: 'string', description: 'Confidence level' }, 95 + source: { type: 'string', description: 'Connection source entity name' }, 96 + target: { type: 'string', description: 'Connection target entity name' }, 97 + relation: { type: 'string', description: 'Relationship type' }, 98 + }, 99 + }, 100 + }, 101 + required: ['type', 'data'], 102 + }, 103 + execute: async (_id, args): Promise<AgentToolResult<unknown>> => { 104 + const { type, data } = args as { type: string; data: Record<string, string> }; 105 + const result = carryAssert(type, data); 106 + return { 107 + content: [{ type: 'text', text: result }], 108 + }; 109 + }, 110 + }; 111 + 112 + // --- Note processing --- 113 + 114 + function readVaultNotes(): string[] { 115 + return readdirSync(VAULT_DIR) 116 + .filter((f) => f.endsWith('.md')) 117 + .sort(); 118 + } 119 + 120 + function readNote(filename: string): string { 121 + return readFileSync(resolve(VAULT_DIR, filename), 'utf-8'); 122 + } 123 + 124 + function isAlreadyProcessed(filename: string): boolean { 125 + const provenancePath = resolve(PROVENANCE_DIR, `${filename}.json`); 126 + return existsSync(provenancePath); 127 + } 128 + 129 + function saveProvenance(filename: string, items: ExtractedItem[]): void { 130 + mkdirSync(PROVENANCE_DIR, { recursive: true }); 131 + writeFileSync( 132 + resolve(PROVENANCE_DIR, `${filename}.json`), 133 + JSON.stringify({ filename, processedAt: new Date().toISOString(), items }, null, 2) + '\n', 134 + 'utf-8', 135 + ); 136 + } 137 + 138 + const EXTRACTION_PROMPT = `Analyze this Obsidian vault note and extract structured data into carry. 139 + 140 + For each note, extract: 141 + 1. **Entities**: People, organizations, concepts, tools mentioned. Include name, URL (if found in the note), and a short description from the note content. 142 + 2. **Citations**: Any referenced URLs with titles and key takeaways summarizing what the note says about that source. 143 + 3. **Connections**: Relationships between entities mentioned (e.g. "X is director of Y", "A is related-to B", "C is a division of D"). 144 + 145 + Rules: 146 + - Use carry_assert for each extraction 147 + - Entity names should be canonical and complete (e.g. "Ink and Switch" not "I&S") 148 + - Only include URLs that actually appear in the note text 149 + - Connection source/target should be entity names, not URLs 150 + - Be thorough — extract every named entity and every stated relationship 151 + - For wikilinks like [[Some Topic]], treat "Some Topic" as an entity name 152 + - Confidence for citations: use org.latha.strata.defs#high for well-known sources, org.latha.strata.defs#medium otherwise 153 + 154 + Note: {title} 155 + --- 156 + {content}`; 157 + 158 + async function processNote(session: Session, filename: string, dryRun: boolean): Promise<ExtractedItem[]> { 159 + const content = readNote(filename); 160 + const title = filename.replace(/\.md$/, ''); 161 + const prompt = EXTRACTION_PROMPT.replace('{title}', title).replace('{content}', content); 162 + 163 + if (dryRun) { 164 + console.log(` [dry-run] Would send ${content.length} chars to agent`); 165 + return []; 166 + } 167 + 168 + const result: SDKResultMessage = await session.runTurn(prompt); 169 + 170 + if (!result.success) { 171 + console.error(` Agent failed: ${result.error ?? result.errorCode ?? 'unknown'}`); 172 + return []; 173 + } 174 + 175 + // The agent calls carry_assert tools during the turn, so data is already written. 176 + // We just return an empty array here since the tool calls themselves do the writing. 177 + // In a more elaborate version, we'd collect the tool call args. 178 + return []; 179 + } 180 + 181 + async function main(): Promise<void> { 182 + const args = process.argv.slice(2); 183 + const dryRun = args.includes('--dry-run'); 184 + const force = args.includes('--force'); 185 + const noteArg = args.find((a) => a.startsWith('--note=')); 186 + const noteName = noteArg ? noteArg.split('=')[1] : null; 187 + const limitArg = args.find((a) => a.startsWith('--limit=')); 188 + const limit = limitArg ? Number(limitArg.split('=')[1]) : Infinity; 189 + 190 + let notes = readVaultNotes(); 191 + 192 + if (noteName) { 193 + const match = notes.find((n) => n === noteName || n === `${noteName}.md`); 194 + if (!match) { 195 + console.error(`Note not found: ${noteName}`); 196 + process.exit(1); 197 + } 198 + notes = [match]; 199 + } 200 + 201 + if (!force) { 202 + notes = notes.filter((n) => !isAlreadyProcessed(n)); 203 + } 204 + 205 + notes = notes.slice(0, limit); 206 + 207 + if (notes.length === 0) { 208 + console.log('No notes to process.'); 209 + return; 210 + } 211 + 212 + console.log(`Processing ${notes.length} vault notes${dryRun ? ' (dry-run)' : ''}...`); 213 + 214 + // Create a fresh agent with the carry_assert tool 215 + const agentId = await createAgent({ 216 + persona: 'You are a research assistant that extracts structured entities, citations, and connections from Obsidian vault notes. You are thorough and precise, extracting every named entity and relationship. You always use the carry_assert tool to record your extractions.', 217 + human: 'vault-analyzer', 218 + tools: [carryAssertTool], 219 + permissionMode: 'bypassPermissions', 220 + memfs: false, 221 + systemInfoReminder: false, 222 + }); 223 + 224 + console.log(`Created agent: ${agentId}`); 225 + 226 + const session = createSession(agentId, { 227 + memfsStartup: 'skip', 228 + }); 229 + 230 + await session.initialize(); 231 + 232 + let processed = 0; 233 + let errors = 0; 234 + 235 + for (const filename of notes) { 236 + const title = filename.replace(/\.md$/, ''); 237 + console.log(`\nProcessing: ${title}`); 238 + 239 + try { 240 + const items = await processNote(session, filename, dryRun); 241 + if (!dryRun) { 242 + saveProvenance(filename, items); 243 + } 244 + processed++; 245 + } catch (err: any) { 246 + console.error(` Error: ${err?.message ?? err}`); 247 + errors++; 248 + } 249 + } 250 + 251 + console.log(`\nDone. Processed: ${processed}, Errors: ${errors}`); 252 + 253 + // Clean up session 254 + await session[Symbol.asyncDispose](); 255 + } 256 + 257 + main().catch((err) => { 258 + console.error('Fatal:', err instanceof Error ? err.message : String(err)); 259 + process.exit(1); 260 + });