Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 193 additions & 8 deletions build.agents.examples.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ const SDK_METHODS = [
'PushChain.utils.helpers.formatUnits',
'PushChain.utils.account.toUniversal',
'PushChain.utils.account.toChainAgnostic',
'PushChain.utils.account.fromChainAgnostic',
'PushChain.utils.account.deriveExecutorAccount',
'PushChain.utils.account.resolveControllerAccount',
'PushChain.utils.chains.getChainNamespace',
'PushChain.utils.chains.getChainName',
'PushChain.utils.chains.getSupportedChains',
'PushChain.utils.chains.getSupportedChainsByName',
'PushChain.utils.helpers.encodeTxData',
'PushChain.utils.tokens.getMoveableTokens',
'PushChain.utils.tokens.getPayableTokens',
Expand All @@ -59,8 +61,19 @@ const SDK_METHODS = [
'PushChain.utils.conversion.slippageToMinAmount',
];

// Match a method name only when it is followed by a non-identifier character,
// so a prefix never matches its longer sibling: `getChainName` must not fire
// on `getChainNamespace(`, `toUniversal` not on `toUniversalFromKeypair(`.
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const SDK_METHOD_PATTERNS = SDK_METHODS.map((m) => ({
method: m,
pattern: new RegExp(`${escapeRegExp(m)}(?![A-Za-z0-9_$])`),
}));

function detectSdkMethods(code) {
return SDK_METHODS.filter((m) => code.includes(m));
return SDK_METHOD_PATTERNS.filter(({ pattern }) => pattern.test(code)).map(
({ method }) => method
);
}

// ─── Helpers ──────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -120,6 +133,34 @@ function cleanCode(raw) {
.replace(/\\`/g, '`');
}

/**
* Fenced code blocks of an example markdown file, joined. Detection must run
* on code only: every example ends with an auto-generated "## SDK Methods Used"
* footer that lists method names in backticks, so scanning the whole file would
* re-detect whatever the footer already says — including past false positives.
*/
function codeBlocksOf(markdown) {
const blocks = [];
const re = /```[^\n]*\n([\s\S]*?)```/g;
let m;
while ((m = re.exec(markdown)) !== null) blocks.push(m[1]);
return blocks.join('\n');
}

function sdkMethodsFooter(methods) {
const list = methods.length
? methods.map((m) => `- \`${m}\``).join('\n')
: '- See code above';
return `## SDK Methods Used\n\n${list}\n`;
}

/** Rewrite the trailing "## SDK Methods Used" section to match `methods`. */
function syncSdkMethodsFooter(markdown, methods) {
const idx = markdown.lastIndexOf('## SDK Methods Used');
if (idx === -1) return markdown;
return markdown.slice(0, idx) + sdkMethodsFooter(methods);
}

// ─── Extraction: SDK pages ────────────────────────────────────────────────────

/**
Expand Down Expand Up @@ -510,13 +551,157 @@ export const buildAgentsExamples = async () => {
}
}

// Persist updated index
if (newEntries.length > 0) {
const merged = [...existingIndex, ...newEntries];
const payload = indexWrapper
? { ...indexWrapper, examples: merged }
: merged;
await fs.writeFile(INDEX_PATH, JSON.stringify(payload, null, 2), 'utf-8');
// ── Backfill: re-detect sdk_methods_used for already-registered entries ──
// The extraction loop skips registered IDs, so an empty sdk_methods_used
// (the retrieval key) would otherwise stay empty forever. Entries with no
// detectable SDK method (e.g. pure ethers/viem read examples) stay empty.
let backfilled = 0;
let pruned = 0;
for (const entry of existingIndex) {
// Tombstones (status: 'removed') document a deleted API; advertising that
// API under sdk_methods_used would surface a removed method as usable.
if (entry.status === 'removed') continue;

if (
Array.isArray(entry.sdk_methods_used) &&
entry.sdk_methods_used.length > 0
) {
// Prune substring false positives left by the old `includes()` matcher:
// a method is dropped only when it is a strict prefix of another method
// in the same list AND the boundary-aware detector cannot find it.
const listed = entry.sdk_methods_used;
const suspects = listed.filter((m) =>
listed.some((other) => other !== m && other.startsWith(m))
);
if (suspects.length === 0) continue;
try {
const md = await fs.readFile(
path.join(AGENTS_EXAMPLES_DIR, entry.file),
'utf-8'
);
const detected = new Set(detectSdkMethods(codeBlocksOf(md)));
const kept = listed.filter(
(m) => !suspects.includes(m) || detected.has(m)
);
if (kept.length !== listed.length) {
entry.sdk_methods_used = kept;
await fs.writeFile(
path.join(AGENTS_EXAMPLES_DIR, entry.file),
syncSdkMethodsFooter(md, kept),
'utf-8'
);
pruned++;
console.log(
chalk.yellow(` ✂ ${entry.id}`) +
chalk.gray(
` dropped [${listed.filter((m) => !kept.includes(m)).join(', ')}]`
)
);
}
} catch {
// example file missing on disk — leave the entry untouched
}
continue;
}
try {
const md = await fs.readFile(
path.join(AGENTS_EXAMPLES_DIR, entry.file),
'utf-8'
);
const detected = detectSdkMethods(codeBlocksOf(md));
if (detected.length > 0) {
entry.sdk_methods_used = detected;
await fs.writeFile(
path.join(AGENTS_EXAMPLES_DIR, entry.file),
syncSdkMethodsFooter(md, detected),
'utf-8'
);
backfilled++;
console.log(
chalk.green(` ✚ ${entry.id}`) +
chalk.gray(` sdk_methods_used ← [${detected.join(', ')}]`)
);
}
} catch {
// example file missing on disk — leave the entry untouched
}
}
// ── Footer drift check: every example's "## SDK Methods Used" section must
// mirror its index entry, regardless of whether this run changed the entry
// (entries backfilled by an earlier run predate the footer sync).
let footersSynced = 0;
for (const entry of existingIndex) {
if (entry.status === 'removed') continue; // hand-written migration stubs
if (!Array.isArray(entry.sdk_methods_used)) continue;
try {
const filePath = path.join(AGENTS_EXAMPLES_DIR, entry.file);
const md = await fs.readFile(filePath, 'utf-8');
const synced = syncSdkMethodsFooter(md, entry.sdk_methods_used);
if (synced !== md) {
await fs.writeFile(filePath, synced, 'utf-8');
footersSynced++;
console.log(chalk.green(` ≡ ${entry.id}`) + chalk.gray(' footer synced'));
}
} catch {
// example file missing on disk — nothing to sync
}
}
if (footersSynced > 0) {
console.log(
chalk.cyan(`\n≡ Synced "SDK Methods Used" footers on ${footersSynced} files`)
);
}

if (backfilled > 0 || pruned > 0) {
console.log(
chalk.cyan(
`\n🔁 sdk_methods_used: backfilled ${backfilled}, pruned ${pruned}`
)
);
}

// ── Refresh wrapper metadata from the installed SDK ──────────────────────
// The wrapper used to be carried over verbatim ({...indexWrapper}), which
// froze current_sdk_version/generated at their 2026-07-03 values forever.
if (indexWrapper) {
try {
const corePkg = JSON.parse(
await fs.readFile(
path.join(__dirname, 'node_modules/@pushchain/core/package.json'),
'utf-8'
)
);
if (
corePkg.version &&
indexWrapper.current_sdk_version !== corePkg.version
) {
indexWrapper.current_sdk_version = corePkg.version;
}
} catch {
// SDK not installed — keep the recorded version
}
}

// Persist updated index (only when content actually changed, so repeated
// runs stay byte-identical and don't churn git or prettier).
const merged = [...existingIndex, ...newEntries];
const payload = indexWrapper ? { ...indexWrapper, examples: merged } : merged;
const serialized = JSON.stringify(payload, null, 2) + '\n';
let onDisk = null;
try {
onDisk = await fs.readFile(INDEX_PATH, 'utf-8');
} catch {
// no index yet
}
if (serialized !== onDisk) {
if (indexWrapper) {
payload.generated = new Date().toISOString();
}
await fs.writeFile(
INDEX_PATH,
JSON.stringify(payload, null, 2) + '\n',
'utf-8'
);
console.log(
chalk.cyan(
`\n📖 Updated examples/index.json → ${merged.length} total entries`
Expand Down
52 changes: 40 additions & 12 deletions build.agents.llms.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ const BASE_URL = 'https://push.org';
const MAX_BLOG_POSTS = 5;

const SDK_VERSIONS = {
core: '6.0.19',
uiKit: '6.0.18',
core: '6.0.24',
uiKit: '6.0.24',
};
const AGENT_LAYER_VERSION = '1.0.25';
const AGENT_LAYER_DATE = '2026-07-15';
const AGENT_LAYER_VERSION = '1.0.26';
const AGENT_LAYER_DATE = '2026-09-02';
const ROUTES_PATH = path.join(AGENTS_DIR, 'routes.json');

const WORKFLOW_CATEGORIES = [
Expand Down Expand Up @@ -179,7 +179,13 @@ const gatherBlogPosts = async () => {
};

// Build llms.txt — static sections hardcoded, workflows, skills, resources and blog posts dynamic
const buildLlmsTxt = async (workflows, skills, resources, routes, blogPosts) => {
const buildLlmsTxt = async (
workflows,
skills,
resources,
routes,
blogPosts
) => {
const lines = [];

// ── Header ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -221,6 +227,12 @@ const buildLlmsTxt = async (workflows, skills, resources, routes, blogPosts) =>
lines.push(
'- **Universal Transaction**: A single SDK call that routes funds and execution from any origin chain to Push Chain or an external target.'
);
lines.push(
'- **PRC-20**: A token born on an *external* chain, mirrored inward as a synthetic on Push Chain (`USDC.eth`, `pETH`). Static SDK table, sync lookup via `getPRC20Address`. Move it with a `MoveableToken` accessor in `tx.funds`.'
);
lines.push(
'- **PC-20**: A token born *on* Push Chain, mirrored outward as a wrapper on external chains. Dynamic — lives in UniversalCore’s on-chain registry, async lookup via `getPC20Address`. Move it with a `{ chain, address }` reference in `tx.funds` (never add `symbol`). One letter apart from PRC-20 and the opposite direction — canonical definition: https://push.org/docs/chain/important-concepts/#token-types-on-push-chain'
);
// Route prose pulled from agents/routes.json (single source of truth).
// Falls back to inline strings only if the JSON failed to load.
if (routes.length > 0) {
Expand Down Expand Up @@ -346,8 +358,8 @@ const buildLlmsTxt = async (workflows, skills, resources, routes, blogPosts) =>
const href = s.external
? s.url
: s.file
? `${BASE_URL}/${s.file}`
: s.url;
? `${BASE_URL}/${s.file}`
: s.url;
if (s.external) {
const brief =
s.id === 'push-pusd'
Expand Down Expand Up @@ -564,6 +576,15 @@ const buildLlmsTxt = async (workflows, skills, resources, routes, blogPosts) =>
lines.push(
'| Tx fails with "insufficient funds" even though origin wallet has balance | UEA on Push Chain has no PC token balance \u2014 gas abstraction requires a funded UEA | Fund the UEA address on Push Chain via https://faucet.push.org or transfer PC tokens to it |'
);
lines.push(
'| PC-20 transfer throws `PC20_TOKEN_CHAIN_MISMATCH` | `funds.token.chain` was set to the destination chain | `funds.token.chain` = where the tokens sit RIGHT NOW; the destination goes in `to.chain` |'
);
lines.push(
'| PC-20 transfer misroutes or throws with a `symbol` field present | A `symbol` was added to a PC-20 reference \u2014 the SDK detects PC-20 vs MoveableToken by the ABSENCE of `symbol` | A PC-20 reference is exactly `{ chain, address }`; resolve it first with `PushChain.utils.tokens.getPC20Address()` |'
);
lines.push(
'| PC-20 wrapper address is empty after an export | Wrapper read from `receipt.externalAssetAddr`, a best-effort mirror that is `undefined` while the outbound is still in flight | Resolve wrappers from `getPC20Address(...).registry` \u2014 the authoritative record |'
);
lines.push('');

// ── Canonical Workflows (grouped by category) ─────────────────────────────
Expand Down Expand Up @@ -634,7 +655,10 @@ const buildLlmsTxt = async (workflows, skills, resources, routes, blogPosts) =>
);
lines.push('');
lines.push(
`- **${AGENT_LAYER_DATE} v${AGENT_LAYER_VERSION}** \u2014 Launched the push.org docs **MCP server** at \`https://mcp.push.org/api\` (Streamable HTTP, spec revision 2025-11-25; stateless, read-only, no API key). Four tools: \`search_docs\` (ranked full-text search over the indexed docs), \`get_page\` (full page as clean markdown with title/url/section/lastUpdated), \`list_sections\` (hierarchical docs tree), \`get_agent_resource\` (raw JSON of \`capabilities\`, \`errors\`, \`contract-addresses\`, \`supported-chains\`, \`sdk-capabilities\`, or \`changelog\` \u2014 snapshotted at site build time). Docs pages and the six agent files are also exposed as MCP resources under their canonical URLs. Artifacts are generated at site build time by a Docusaurus postBuild plugin (MiniSearch index, per-page markdown, manifest with build hash); pages containing raw i18n placeholder keys are excluded from the index and logged to \`build/mcp/skipped.json\`. Discovery document at \`/.well-known/mcp.json\`. Updated the \`mcp-candidates.json\` description \u2014 docs access is now a supported tool server; SDK-operation candidates (send_universal_transaction, sign_universal_message, etc.) remain reference definitions to adapt per framework.`
`- **${AGENT_LAYER_DATE} v${AGENT_LAYER_VERSION}** \u2014 **PC-20 agent-layer propagation** (the follow-up deferred from the PC-20 docs PR) plus \`@pushchain/core\` 6.0.19 \u2192 6.0.24 and \`@pushchain/ui-kit\` 6.0.18 \u2192 6.0.24. PC-20 = a token born ON Push Chain, mirrored outward as wrappers on external chains via UniversalCore's on-chain registry (async \`getPC20Address\`); PRC-20 = a token born on an external chain, mirrored inward as a synthetic (static table, sync \`getPRC20Address\`) \u2014 one letter apart, opposite directions. \`errors.json\` gained the full typed PC-20 error family (base \`PC20Error\` + 14 concrete classes with stable \`PC20_*\` codes, curated context fields, and remediation hints). \`sdk-capabilities.json\` gained \`PushChain.utils.tokens.getPC20Address\`; \`capabilities.json\` \`tx.funds\` now documents both token forms (MoveableToken accessor vs \`{ chain, address }\` PC-20 reference \u2014 never add \`symbol\`; \`funds.token.chain\` = where the tokens sit now, \`to.chain\` = destination). New \`choose_token_standard\` decision tree, \`pc20_token_movement\` feature-matrix row, \`routes.json\` \`shared.funds_token_forms\`, a PC-20 retrieval-map entry, and a read-only \`get_pc20_address\` MCP candidate (16 total). Unfroze \`examples/index.json\` regeneration (wrapper metadata was carried over verbatim since 2026-07-03; the builder now refreshes \`current_sdk_version\` from the installed SDK, stamps \`generated\` only on real changes, writes a trailing newline, and backfills empty \`sdk_methods_used\` \u2014 10 entries backfilled, 2 removed-API tombstones deliberately left empty, boundary-aware method matching so prefixes like \`getChainName\` no longer match \`getChainNamespace\`, \`fromChainAgnostic\` + \`getSupportedChainsByName\` added to detection). Skills and workflows gained the same coverage: push-backend SKILL.md has a full "Moving Tokens with tx.funds - PRC-20 vs PC-20" section plus a \`getPC20Address\` utility entry and four new Common Mistakes rows, push-frontend a compact PC-20 send section, and the send-universal-transaction / use-utility-functions workflows the step-by-step PC-20 form (push-backend and push-frontend frontmatter pins refreshed to 6.0.24; push-contracts untouched \u2014 no contract-side PC-20 interface is documented yet). Fixed 8 \`source-freshness.json\` paths broken by the docs renumbering. Naming pass completed: "Get PRC-20 Address" hyphenated in prose everywhere.`
);
lines.push(
`- **2026-07-15 v1.0.25** \u2014 Launched the push.org docs **MCP server** at \`https://mcp.push.org/api\` (Streamable HTTP, spec revision 2025-11-25; stateless, read-only, no API key). Four tools: \`search_docs\` (ranked full-text search over the indexed docs), \`get_page\` (full page as clean markdown with title/url/section/lastUpdated), \`list_sections\` (hierarchical docs tree), \`get_agent_resource\` (raw JSON of \`capabilities\`, \`errors\`, \`contract-addresses\`, \`supported-chains\`, \`sdk-capabilities\`, or \`changelog\` \u2014 snapshotted at site build time). Docs pages and the six agent files are also exposed as MCP resources under their canonical URLs. Artifacts are generated at site build time by a Docusaurus postBuild plugin (MiniSearch index, per-page markdown, manifest with build hash); pages containing raw i18n placeholder keys are excluded from the index and logged to \`build/mcp/skipped.json\`. Discovery document at \`/.well-known/mcp.json\`. Updated the \`mcp-candidates.json\` description \u2014 docs access is now a supported tool server; SDK-operation candidates (send_universal_transaction, sign_universal_message, etc.) remain reference definitions to adapt per framework.`
);
lines.push(
`- **2026-07-03 v1.0.24** \u2014 \`@pushchain/core\` 6.0.16 \u2192 6.0.19 and \`@pushchain/ui-kit\` 6.0.16 \u2192 6.0.18 (versions now intentionally unequal). Core headline: **EIP-7702 atomic batching for native Push Chain EOAs** \u2014 a multicall from a Push-native EOA now executes as ONE type-4 (SetCode) transaction delegating to \`PushBatchExecutor\` (Donut: \`0x0106BF2F9B02f32203A83a3bDaD79fE8818f3796\`) when the signer can sign authorizations (auto-wired for ethers v6 Wallet and viem local accounts; browser JSON-RPC wallets and ethers v5 fall back safely, pre-broadcast, to the legacy sequential loop). New additive \`atomic: boolean\` response field (\`false\` only on that fallback); inner multicall entries with a zero \`to\` now reject with \`PushChainExecutionError\`; \`UniversalSigner\` gains optional \`signAuthorization\`. Corrected the long-stale "Push-native senders cannot use multicall" claim across the skill/workflow/docs surfaces. Also: automatic **archive-RPC fallback** on Donut for pruned history (\`https://archive.evm.donut.rpc.push.org/\`), wait-stage progress markers now carry \`pushTxHash\` (no event ID changes), normalized user-facing revert messages, and a gas-swap preflight fix that reports real shortfalls instead of Uniswap \`STF\` reverts. ui-kit 6.0.18 is internal-only (viem/wagmi refresh, no public prop changes).`
Expand Down Expand Up @@ -739,17 +763,21 @@ const generateLlmsTxt = async () => {

const routes = await loadRoutes();
console.log(
chalk.gray(
` Loaded ${routes.length} routes from agents/routes.json`
)
chalk.gray(` Loaded ${routes.length} routes from agents/routes.json`)
);

const blogPosts = await gatherBlogPosts();
console.log(chalk.gray(` Found ${blogPosts.length} recent blog posts`));

await fs.mkdir(STATIC_DIR, { recursive: true });

const content = await buildLlmsTxt(workflows, skills, resources, routes, blogPosts);
const content = await buildLlmsTxt(
workflows,
skills,
resources,
routes,
blogPosts
);
await fs.writeFile(OUTPUT_PATH, content, 'utf-8');

console.log(chalk.green('✅ Generated static/llms.txt'));
Expand Down
Loading
Loading