diff --git a/README.md b/README.md index 9a54184..c96651f 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,9 @@ bun run security:audit # 4) Bootstrap selected skills ./bootstrap.sh --only aelf-node-skill --skip-install + +# 5) Check hub/catalog update drift (non-blocking) +bun run update:check ``` ## Bootstrap CLI @@ -91,6 +94,22 @@ Defaults: 3. health check enabled 4. `skills-catalog.json` as catalog source +## Update Self-Check + +`aelf-skills` includes built-in update reminders for `bootstrap`, `health:check`, and `catalog:generate`. +Checks are non-blocking and cache-backed (default TTL 24h). +Reminder output is throttled to once per TTL window by `lastNotifiedAt`. + +Commands: +1. `bun run update:check` +2. `bun run update:check -- --force` +3. `bun run update:check:json` + +Environment variables: +1. `AELF_SKILLS_UPDATE_CHECK=0|1` (default `1`) +2. `AELF_SKILLS_UPDATE_TTL_HOURS=24` (default `24`) +3. `AELF_SKILLS_UPDATE_CACHE_PATH=` (default `~/.aelf-skills/update-check-cache.json`) + ## Generated Catalog `skills-catalog.json` is the stable machine interface. @@ -130,13 +149,13 @@ This section is auto-synced by `bun run catalog:generate`. | ID | npm Package | Version | OpenClaw Tools | Description | |---|---|---:|---:|---| -| aelf-node-skill | @blockchain-forever/aelf-node-skill | 0.1.0 | 11 | AElf node querying and contract execution skill for agents. | -| aelfscan-skill | @aelfscan/agent-skills | 0.2.0 | 61 | AelfScan explorer data retrieval and analytics skill for agents. | -| awaken-agent-skills | @awaken-finance/agent-kit | 1.2.1 | 11 | Awaken DEX trading and market data operations for agents. | -| eforest-agent-skills | @eforest-finance/agent-skills | 0.4.0 | 48 | eForest symbol and forest NFT operations for agent workflows. | -| portkey-ca-agent-skills | @portkey/ca-agent-skills | 1.1.2 | 28 | Portkey CA wallet registration/auth/guardian/transfer operations for agents. | -| portkey-eoa-agent-skills | @portkey/eoa-agent-skills | 1.2.1 | 21 | Portkey EOA wallet and asset operations for aelf agents. | -| tomorrowdao-agent-skills | @tomorrowdao/agent-skills | 0.1.0 | 41 | TomorrowDAO governance, BP, and resource operations for agents. | +| aelf-node-skill | @blockchain-forever/aelf-node-skill | 0.1.3 | 11 | AElf node querying and contract execution skill for agents. | +| aelfscan-skill | @aelfscan/agent-skills | 0.2.2 | 61 | AelfScan explorer data retrieval and analytics skill for agents. | +| awaken-agent-skills | @awaken-finance/agent-kit | 1.2.4 | 11 | Awaken DEX trading and market data operations for agents. | +| eforest-agent-skills | @eforest-finance/agent-skills | 0.4.3 | 48 | eForest symbol and forest NFT operations for agent workflows. | +| portkey-ca-agent-skills | @portkey/ca-agent-skills | 1.1.5 | 28 | Portkey CA wallet registration/auth/guardian/transfer operations for agents. | +| portkey-eoa-agent-skills | @portkey/eoa-agent-skills | 1.2.4 | 21 | Portkey EOA wallet and asset operations for aelf agents. | +| tomorrowdao-agent-skills | @tomorrowdao/agent-skills | 0.1.4 | 41 | TomorrowDAO governance, BP, and resource operations for agents. | ## Health Check diff --git a/README.zh-CN.md b/README.zh-CN.md index f9a908f..462d329 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -77,6 +77,9 @@ bun run security:audit # 4) 拉起指定 skill ./bootstrap.sh --only aelf-node-skill --skip-install + +# 5) 检查 hub/catalog 版本漂移(非阻塞) +bun run update:check ``` ## Bootstrap 命令 @@ -91,6 +94,22 @@ bun run security:audit 3. 默认执行 health 4. 默认使用 `skills-catalog.json` +## 更新自检 + +`aelf-skills` 内置了更新提醒,会在 `bootstrap`、`health:check`、`catalog:generate` 运行时做非阻塞检测。 +检测结果使用本地缓存(默认 TTL 24 小时),不会阻塞主流程。 +提醒输出会基于 `lastNotifiedAt` 做节流:默认 24 小时内最多提示一次。 + +命令: +1. `bun run update:check` +2. `bun run update:check -- --force` +3. `bun run update:check:json` + +环境变量: +1. `AELF_SKILLS_UPDATE_CHECK=0|1`(默认 `1`) +2. `AELF_SKILLS_UPDATE_TTL_HOURS=24`(默认 `24`) +3. `AELF_SKILLS_UPDATE_CACHE_PATH=`(默认 `~/.aelf-skills/update-check-cache.json`) + ## 机器清单说明 `skills-catalog.json` 是稳定机器接口。 @@ -130,13 +149,13 @@ Schema 演进规则: | ID | npm 包名 | 版本 | OpenClaw 工具数 | 描述 | |---|---|---:|---:|---| -| aelf-node-skill | @blockchain-forever/aelf-node-skill | 0.1.0 | 11 | AElf 节点查询与合约调用技能。 | -| aelfscan-skill | @aelfscan/agent-skills | 0.2.0 | 61 | AelfScan 浏览器数据检索与分析技能。 | -| awaken-agent-skills | @awaken-finance/agent-kit | 1.2.1 | 11 | Awaken DEX 交易与行情数据技能。 | -| eforest-agent-skills | @eforest-finance/agent-skills | 0.4.0 | 48 | eForest 代币与 NFT 市场操作技能。 | -| portkey-ca-agent-skills | @portkey/ca-agent-skills | 1.1.2 | 28 | Portkey CA 钱包注册、认证、Guardian 与转账技能。 | -| portkey-eoa-agent-skills | @portkey/eoa-agent-skills | 1.2.1 | 21 | Portkey EOA 钱包与资产操作技能。 | -| tomorrowdao-agent-skills | @tomorrowdao/agent-skills | 0.1.0 | 41 | TomorrowDAO 治理、BP 与资源操作技能。 | +| aelf-node-skill | @blockchain-forever/aelf-node-skill | 0.1.3 | 11 | AElf 节点查询与合约调用技能。 | +| aelfscan-skill | @aelfscan/agent-skills | 0.2.2 | 61 | AelfScan 浏览器数据检索与分析技能。 | +| awaken-agent-skills | @awaken-finance/agent-kit | 1.2.4 | 11 | Awaken DEX 交易与行情数据技能。 | +| eforest-agent-skills | @eforest-finance/agent-skills | 0.4.3 | 48 | eForest 代币与 NFT 市场操作技能。 | +| portkey-ca-agent-skills | @portkey/ca-agent-skills | 1.1.5 | 28 | Portkey CA 钱包注册、认证、Guardian 与转账技能。 | +| portkey-eoa-agent-skills | @portkey/eoa-agent-skills | 1.2.4 | 21 | Portkey EOA 钱包与资产操作技能。 | +| tomorrowdao-agent-skills | @tomorrowdao/agent-skills | 0.1.4 | 41 | TomorrowDAO 治理、BP 与资源操作技能。 | ## 健康检查 diff --git a/package.json b/package.json index 536aff4..d517afb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@blockchain-forever/aelf-skills", - "version": "0.1.1", + "version": "0.1.2", "description": "Discovery, download, and configuration hub for the aelf agent skill ecosystem.", "type": "module", "license": "MIT", @@ -26,6 +26,8 @@ "catalog:generate:local": "bun run scripts/generate-catalog.ts --include-local-paths --output skills-catalog.local.json --no-readme-sync", "bootstrap": "bun run scripts/bootstrap.ts", "health:check": "bun run scripts/health-check.ts", + "update:check": "bun run scripts/update-check.ts", + "update:check:json": "bun run scripts/update-check.ts --json", "type-debt:report": "bun run scripts/type-debt-report.ts", "test:signer-chain": "bun run scripts/test-signer-chain.ts", "readme:check": "bun run scripts/check-readme-sync.ts", diff --git a/scripts/bootstrap.ts b/scripts/bootstrap.ts index ce32256..2a533e8 100644 --- a/scripts/bootstrap.ts +++ b/scripts/bootstrap.ts @@ -5,6 +5,7 @@ import type { SkillCatalogEntry, SkillsCatalog } from './lib/types.ts'; import { parseCsv, readJsonFile, requireCommand, runCommand } from './lib/utils.ts'; import { buildCatalog, writeCatalogArtifacts } from './lib/catalog.ts'; import { printHealthReport, runHealthCheck } from './lib/health.ts'; +import { maybePrintUpdateReminder } from './lib/update-check.ts'; type SourceMode = 'auto' | 'npm' | 'github' | 'local'; @@ -372,8 +373,10 @@ function printBootstrapSummary(results: SkillRunResult[]): void { } } -function main(): void { +async function main(): Promise { try { + await maybePrintUpdateReminder(); + const options = parseArgs(); validatePrerequisites(options.source, options.skipInstall); @@ -437,4 +440,4 @@ function main(): void { } } -main(); +void main(); diff --git a/scripts/generate-catalog.ts b/scripts/generate-catalog.ts index 8cd7445..d942a58 100644 --- a/scripts/generate-catalog.ts +++ b/scripts/generate-catalog.ts @@ -1,5 +1,6 @@ import path from 'node:path'; import { buildCatalog, writeCatalogArtifacts } from './lib/catalog.ts'; +import { maybePrintUpdateReminder } from './lib/update-check.ts'; interface CliOptions { workspacePath?: string; @@ -27,8 +28,10 @@ function parseArgs(): CliOptions { }; } -function main(): void { +async function main(): Promise { try { + await maybePrintUpdateReminder(); + const options = parseArgs(); const rootDir = process.cwd(); @@ -66,4 +69,4 @@ function main(): void { } } -main(); +void main(); diff --git a/scripts/health-check.ts b/scripts/health-check.ts index 529ec67..ede1dfd 100644 --- a/scripts/health-check.ts +++ b/scripts/health-check.ts @@ -4,6 +4,7 @@ import type { SkillsCatalog } from './lib/types.ts'; import { readJsonFile, parseCsv, writeJsonFile } from './lib/utils.ts'; import { buildCatalog, writeCatalogArtifacts } from './lib/catalog.ts'; import { printHealthReport, runHealthCheck } from './lib/health.ts'; +import { maybePrintUpdateReminder } from './lib/update-check.ts'; interface CliOptions { catalogPath: string; @@ -58,8 +59,10 @@ function ensureCatalog(catalogPath: string): void { writeJsonFile(catalogPath, catalog); } -function main(): void { +async function main(): Promise { try { + await maybePrintUpdateReminder(); + const options = parseArgs(); ensureCatalog(options.catalogPath); @@ -97,4 +100,4 @@ function main(): void { } } -main(); +void main(); diff --git a/scripts/lib/update-check.test.ts b/scripts/lib/update-check.test.ts new file mode 100644 index 0000000..0094288 --- /dev/null +++ b/scripts/lib/update-check.test.ts @@ -0,0 +1,498 @@ +import { afterEach, describe, expect, test } from 'bun:test'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { + checkForUpdates, + compareSemver, + getVersionDiffLevel, + isCacheFresh, + maybePrintUpdateReminder, + renderHumanSummary, + renderReminderLines, +} from './update-check.ts'; +import type { UpdateCheckResult } from './update-types.ts'; + +function createTempDir(): string { + return mkdtempSync(path.join(os.tmpdir(), 'aelf-skills-update-check-')); +} + +function createMockFetch(routes: Record): typeof fetch { + return (async (input: string | URL) => { + const key = String(input); + const route = routes[key]; + if (!route) { + return new Response(JSON.stringify({ error: `unmocked route ${key}` }), { status: 404 }); + } + return new Response(JSON.stringify(route.body), { + status: route.status, + headers: { 'content-type': 'application/json' }, + }); + }) as typeof fetch; +} + +function setEnv(name: string, value?: string): void { + if (value === undefined) { + delete process.env[name]; + return; + } + process.env[name] = value; +} + +afterEach(() => { + delete process.env.AELF_SKILLS_UPDATE_CHECK; +}); + +describe('update-check semver helpers', () => { + test('compares semver correctly', () => { + expect(compareSemver('1.2.3', '1.2.4')).toBeLessThan(0); + expect(compareSemver('2.0.0', '1.9.9')).toBeGreaterThan(0); + expect(compareSemver('1.2.3', '1.2.3')).toBe(0); + }); + + test('detects diff level', () => { + expect(getVersionDiffLevel('1.2.3', '2.0.0')).toBe('major'); + expect(getVersionDiffLevel('1.2.3', '1.3.0')).toBe('minor'); + expect(getVersionDiffLevel('1.2.3', '1.2.4')).toBe('patch'); + expect(getVersionDiffLevel('1.2.3', '1.2.3')).toBe('none'); + expect(getVersionDiffLevel('1.2.3-alpha.1', 'invalid')).toBe('unknown'); + }); + + test('cache freshness respects ttl', () => { + const checkedAt = '2026-03-05T00:00:00.000Z'; + expect(isCacheFresh(checkedAt, 24, new Date('2026-03-05T23:59:59.000Z'))).toBeTrue(); + expect(isCacheFresh(checkedAt, 24, new Date('2026-03-06T00:00:00.001Z'))).toBeFalse(); + }); +}); + +describe('update-check live flow', () => { + test('uses github compare fallback when release tag is missing', async () => { + const tempDir = createTempDir(); + try { + const packageJsonPath = path.join(tempDir, 'package.json'); + const catalogPath = path.join(tempDir, 'skills-catalog.json'); + const cachePath = path.join(tempDir, 'cache.json'); + + writeFileSync( + packageJsonPath, + JSON.stringify( + { + name: '@blockchain-forever/aelf-skills', + version: '0.1.1', + repository: { url: 'https://github.com/AElfProject/aelf-skills.git' }, + }, + null, + 2, + ), + ); + + writeFileSync( + catalogPath, + JSON.stringify( + { + schemaVersion: '1.2.0', + generatedAt: '2026-03-05T00:00:00.000Z', + source: 'test', + warnings: [], + skills: [ + { + id: 'awaken-agent-skills', + displayName: 'Awaken', + npm: { + name: '@awaken-finance/agent-kit', + version: '1.2.3', + }, + repository: { + https: 'https://github.com/Awaken-Finance/awaken-agent-skills', + }, + description: 'test', + capabilities: ['swap'], + artifacts: { + skillMd: true, + mcpServer: true, + openclaw: true, + }, + setupCommands: { + install: 'bun install', + }, + clientSupport: { + openclaw: 'native', + cursor: 'native-setup', + claude_desktop: 'native-setup', + claude_code: 'manual', + codex: 'manual', + }, + openclawToolCount: 1, + }, + ], + }, + null, + 2, + ), + ); + + const mockFetch = createMockFetch({ + 'https://registry.npmjs.org/%40blockchain-forever%2Faelf-skills': { + status: 200, + body: { 'dist-tags': { latest: '0.1.2' } }, + }, + 'https://registry.npmjs.org/%40awaken-finance%2Fagent-kit': { + status: 200, + body: { 'dist-tags': { latest: '2.0.0' } }, + }, + 'https://api.github.com/repos/AElfProject/aelf-skills/releases/tags/v0.1.2': { + status: 404, + body: {}, + }, + 'https://api.github.com/repos/AElfProject/aelf-skills/releases/tags/0.1.2': { + status: 404, + body: {}, + }, + 'https://api.github.com/repos/Awaken-Finance/awaken-agent-skills/releases/tags/v2.0.0': { + status: 404, + body: {}, + }, + 'https://api.github.com/repos/Awaken-Finance/awaken-agent-skills/releases/tags/2.0.0': { + status: 404, + body: {}, + }, + }); + + const result = await checkForUpdates({ + force: true, + allowWhenDisabled: true, + packageJsonPath, + catalogPath, + cachePath, + ttlHours: 24, + now: () => new Date('2026-03-05T01:00:00.000Z'), + fetchImpl: mockFetch, + }); + + expect(result).not.toBeNull(); + if (!result) return; + + expect(result.hasUpdates).toBeTrue(); + expect(result.hub.outdated).toBeTrue(); + expect(result.hub.latestVersion).toBe('0.1.2'); + expect(result.catalogOutdated.length).toBe(1); + expect(result.catalogOutdated[0].diffLevel).toBe('major'); + expect(result.catalogOutdated[0].release?.source).toBe('github-compare'); + expect(result.catalogOutdated[0].release?.url).toContain('/compare/v1.2.3...v2.0.0'); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test('falls back to cache on catastrophic read failure', async () => { + const tempDir = createTempDir(); + try { + const cachePath = path.join(tempDir, 'cache.json'); + const cached: UpdateCheckResult = { + checkedAt: '2026-03-05T00:00:00.000Z', + fromCache: false, + hasUpdates: true, + hub: { + packageName: '@blockchain-forever/aelf-skills', + currentVersion: '0.1.1', + latestVersion: '0.1.2', + outdated: true, + diffLevel: 'minor', + }, + catalogOutdated: [], + notesDigest: [], + sourceStatus: { + npm: 'ok', + github: 'ok', + errors: [], + }, + }; + writeFileSync(cachePath, JSON.stringify(cached, null, 2)); + + const result = await checkForUpdates({ + force: true, + allowWhenDisabled: true, + packageJsonPath: path.join(tempDir, 'missing-package.json'), + cachePath, + }); + + expect(result).not.toBeNull(); + if (!result) return; + expect(result.fromCache).toBeTrue(); + expect(result.hasUpdates).toBeTrue(); + expect(result.sourceStatus.errors.join('\n')).toContain('fallback to cache'); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test('hub up-to-date with mixed catalog versions only returns outdated skills', async () => { + const tempDir = createTempDir(); + try { + const packageJsonPath = path.join(tempDir, 'package.json'); + const catalogPath = path.join(tempDir, 'skills-catalog.json'); + const cachePath = path.join(tempDir, 'cache.json'); + + writeFileSync( + packageJsonPath, + JSON.stringify( + { + name: '@blockchain-forever/aelf-skills', + version: '0.1.1', + repository: { url: 'https://github.com/AElfProject/aelf-skills.git' }, + }, + null, + 2, + ), + ); + + writeFileSync( + catalogPath, + JSON.stringify( + { + schemaVersion: '1.2.0', + generatedAt: '2026-03-05T00:00:00.000Z', + source: 'test', + warnings: [], + skills: [ + { + id: 'skill-old', + displayName: 'Skill Old', + npm: { name: '@demo/skill-old', version: '1.0.0' }, + repository: { https: 'https://github.com/demo/skill-old' }, + description: 'old', + capabilities: ['x'], + artifacts: { skillMd: true, mcpServer: true, openclaw: true }, + setupCommands: { install: 'bun install' }, + clientSupport: { + openclaw: 'native', + cursor: 'native-setup', + claude_desktop: 'native-setup', + claude_code: 'manual', + codex: 'manual', + }, + openclawToolCount: 1, + }, + { + id: 'skill-ok', + displayName: 'Skill Ok', + npm: { name: '@demo/skill-ok', version: '2.1.0' }, + repository: { https: 'https://github.com/demo/skill-ok' }, + description: 'ok', + capabilities: ['y'], + artifacts: { skillMd: true, mcpServer: true, openclaw: true }, + setupCommands: { install: 'bun install' }, + clientSupport: { + openclaw: 'native', + cursor: 'native-setup', + claude_desktop: 'native-setup', + claude_code: 'manual', + codex: 'manual', + }, + openclawToolCount: 1, + }, + ], + }, + null, + 2, + ), + ); + + const mockFetch = createMockFetch({ + 'https://registry.npmjs.org/%40blockchain-forever%2Faelf-skills': { + status: 200, + body: { 'dist-tags': { latest: '0.1.1' } }, + }, + 'https://registry.npmjs.org/%40demo%2Fskill-old': { + status: 200, + body: { 'dist-tags': { latest: '1.1.0' } }, + }, + 'https://registry.npmjs.org/%40demo%2Fskill-ok': { + status: 200, + body: { 'dist-tags': { latest: '2.1.0' } }, + }, + 'https://api.github.com/repos/demo/skill-old/releases/tags/v1.1.0': { + status: 404, + body: {}, + }, + 'https://api.github.com/repos/demo/skill-old/releases/tags/1.1.0': { + status: 404, + body: {}, + }, + }); + + const result = await checkForUpdates({ + force: true, + allowWhenDisabled: true, + packageJsonPath, + catalogPath, + cachePath, + fetchImpl: mockFetch, + }); + + expect(result).not.toBeNull(); + if (!result) return; + expect(result.hub.outdated).toBeFalse(); + expect(result.hasUpdates).toBeTrue(); + expect(result.catalogOutdated.length).toBe(1); + expect(result.catalogOutdated[0].id).toBe('skill-old'); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test('returns null when update check is disabled by env', async () => { + const tempDir = createTempDir(); + try { + const packageJsonPath = path.join(tempDir, 'package.json'); + writeFileSync( + packageJsonPath, + JSON.stringify({ name: '@blockchain-forever/aelf-skills', version: '0.1.1' }, null, 2), + ); + setEnv('AELF_SKILLS_UPDATE_CHECK', '0'); + + const result = await checkForUpdates({ + packageJsonPath, + }); + + expect(result).toBeNull(); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); +}); + +describe('update-check rendering', () => { + test('renders human summary and reminder lines', () => { + const result: UpdateCheckResult = { + checkedAt: '2026-03-05T00:00:00.000Z', + fromCache: false, + hasUpdates: true, + hub: { + packageName: '@blockchain-forever/aelf-skills', + currentVersion: '0.1.1', + latestVersion: '0.1.2', + outdated: true, + diffLevel: 'minor', + release: { + source: 'github-release', + summary: 'Hub update summary', + url: 'https://github.com/AElfProject/aelf-skills/releases/tag/v0.1.2', + }, + }, + catalogOutdated: [ + { + id: 'awaken-agent-skills', + packageName: '@awaken-finance/agent-kit', + currentVersion: '1.2.3', + latestVersion: '1.5.0', + diffLevel: 'minor', + release: { + source: 'github-compare', + summary: '1.2.3 -> 1.5.0', + url: 'https://github.com/Awaken-Finance/awaken-agent-skills/compare/v1.2.3...v1.5.0', + }, + }, + ], + notesDigest: [], + sourceStatus: { + npm: 'ok', + github: 'ok', + errors: [], + }, + }; + + const summary = renderHumanSummary(result); + expect(summary).toContain('Hub: @blockchain-forever/aelf-skills 0.1.1 -> 0.1.2'); + expect(summary).toContain('Outdated skill packages: 1'); + expect(summary).toContain('awaken-agent-skills'); + + const reminder = renderReminderLines(result); + expect(reminder.length).toBeGreaterThan(0); + expect(reminder.join('\n')).toContain('aelf-skills hub update available'); + expect(reminder.join('\n')).toContain('./bootstrap.sh --source npm'); + }); + + test('suppresses reminder when already notified within ttl window', async () => { + const tempDir = createTempDir(); + try { + const cachePath = path.join(tempDir, 'cache.json'); + const cached: UpdateCheckResult = { + checkedAt: '2026-03-05T00:00:00.000Z', + fromCache: false, + hasUpdates: true, + lastNotifiedAt: '2026-03-05T10:00:00.000Z', + hub: { + packageName: '@blockchain-forever/aelf-skills', + currentVersion: '0.1.1', + latestVersion: '0.1.2', + outdated: true, + diffLevel: 'minor', + }, + catalogOutdated: [], + notesDigest: [], + sourceStatus: { + npm: 'ok', + github: 'ok', + errors: [], + }, + }; + writeFileSync(cachePath, JSON.stringify(cached, null, 2)); + + const logs: string[] = []; + const rawLog = console.log; + console.log = (...args: unknown[]) => { + logs.push(args.map(v => String(v)).join(' ')); + }; + try { + await maybePrintUpdateReminder({ + allowWhenDisabled: true, + cachePath, + ttlHours: 24, + now: () => new Date('2026-03-05T12:00:00.000Z'), + }); + } finally { + console.log = rawLog; + } + + expect(logs.length).toBe(0); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test('renders reminder when hub is up-to-date but catalog has outdated skills', () => { + const result: UpdateCheckResult = { + checkedAt: '2026-03-05T00:00:00.000Z', + fromCache: false, + hasUpdates: true, + hub: { + packageName: '@blockchain-forever/aelf-skills', + currentVersion: '0.1.1', + latestVersion: '0.1.1', + outdated: false, + diffLevel: 'none', + }, + catalogOutdated: [ + { + id: 'awaken-agent-skills', + packageName: '@awaken-finance/agent-kit', + currentVersion: '1.2.3', + latestVersion: '1.5.0', + diffLevel: 'minor', + }, + ], + notesDigest: [], + sourceStatus: { + npm: 'ok', + github: 'ok', + errors: [], + }, + }; + + const lines = renderReminderLines(result); + expect(lines.join('\n')).not.toContain('hub update available'); + expect(lines.join('\n')).toContain('1 skill package update(s)'); + expect(lines.join('\n')).toContain('./bootstrap.sh --source npm'); + }); +}); diff --git a/scripts/lib/update-check.ts b/scripts/lib/update-check.ts new file mode 100644 index 0000000..6803bc3 --- /dev/null +++ b/scripts/lib/update-check.ts @@ -0,0 +1,539 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import type { SkillsCatalog } from './types.ts'; +import { normalizeRepoUrlToHttps, readJsonFile } from './utils.ts'; +import type { + HubUpdateInfo, + OutdatedSkillItem, + ReleaseDigest, + UpdateCheckOptions, + UpdateCheckResult, + UpdateSourceStatus, + VersionDiffLevel, +} from './update-types.ts'; + +const DEFAULT_TTL_HOURS = 24; +const DEFAULT_CACHE_RELATIVE_PATH = path.join('.aelf-skills', 'update-check-cache.json'); +const ENABLE_ENV = 'AELF_SKILLS_UPDATE_CHECK'; +const TTL_ENV = 'AELF_SKILLS_UPDATE_TTL_HOURS'; +const CACHE_PATH_ENV = 'AELF_SKILLS_UPDATE_CACHE_PATH'; + +interface LocalPackageInfo { + name: string; + version: string; + repositoryUrl?: string; +} + +function getDefaultPackageJsonPath(): string { + return path.resolve(import.meta.dir, '..', '..', 'package.json'); +} + +function getDefaultCatalogPath(): string { + return path.resolve(process.cwd(), 'skills-catalog.json'); +} + +function getDefaultCachePath(): string { + return path.resolve(os.homedir(), DEFAULT_CACHE_RELATIVE_PATH); +} + +function envToBoolean(raw: string | undefined, defaultValue: boolean): boolean { + if (!raw) return defaultValue; + const normalized = raw.trim().toLowerCase(); + if (['0', 'false', 'no', 'off'].includes(normalized)) return false; + if (['1', 'true', 'yes', 'on'].includes(normalized)) return true; + return defaultValue; +} + +function parseNumber(value: string | undefined, fallback: number): number { + if (!value) return fallback; + const n = Number(value); + if (!Number.isFinite(n) || n <= 0) return fallback; + return n; +} + +function resolveTtlHours(options?: UpdateCheckOptions): number { + return options?.ttlHours || parseNumber(process.env[TTL_ENV], DEFAULT_TTL_HOURS); +} + +function resolveCachePath(options?: UpdateCheckOptions): string { + return path.resolve(options?.cachePath || process.env[CACHE_PATH_ENV] || getDefaultCachePath()); +} + +function isEnabled(options?: UpdateCheckOptions): boolean { + if (options?.allowWhenDisabled) return true; + return envToBoolean(process.env[ENABLE_ENV], true); +} + +function readLocalPackageInfo(packageJsonPath: string): LocalPackageInfo { + const pkg = readJsonFile>(packageJsonPath); + const name = typeof pkg.name === 'string' ? pkg.name : '@blockchain-forever/aelf-skills'; + const version = typeof pkg.version === 'string' ? pkg.version : '0.0.0'; + const repository = + typeof pkg.repository === 'object' && pkg.repository + ? (pkg.repository as Record) + : {}; + const repositoryUrl = typeof repository.url === 'string' ? repository.url : undefined; + return { name, version, repositoryUrl }; +} + +interface SemverCore { + major: number; + minor: number; + patch: number; +} + +function parseSemverCore(version: string): SemverCore | null { + const normalized = version.trim().replace(/^v/, ''); + const match = normalized.match(/^(\d+)\.(\d+)\.(\d+)/); + if (!match) return null; + return { + major: Number(match[1]), + minor: Number(match[2]), + patch: Number(match[3]), + }; +} + +export function compareSemver(currentVersion: string, latestVersion: string): number { + const a = parseSemverCore(currentVersion); + const b = parseSemverCore(latestVersion); + if (!a || !b) { + if (currentVersion === latestVersion) return 0; + return currentVersion < latestVersion ? -1 : 1; + } + + if (a.major !== b.major) return a.major < b.major ? -1 : 1; + if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1; + if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1; + return 0; +} + +export function getVersionDiffLevel(currentVersion: string, latestVersion: string): VersionDiffLevel { + const a = parseSemverCore(currentVersion); + const b = parseSemverCore(latestVersion); + if (!a || !b) return 'unknown'; + if (a.major !== b.major) return 'major'; + if (a.minor !== b.minor) return 'minor'; + if (a.patch !== b.patch) return 'patch'; + return 'none'; +} + +export function isCacheFresh(checkedAt: string, ttlHours: number, now: Date): boolean { + const checkedAtMs = Date.parse(checkedAt); + if (!Number.isFinite(checkedAtMs)) return false; + const maxAgeMs = ttlHours * 60 * 60 * 1000; + return now.getTime() - checkedAtMs < maxAgeMs; +} + +function readCache(cachePath: string): UpdateCheckResult | null { + if (!existsSync(cachePath)) return null; + try { + const cached = readJsonFile(cachePath); + if (!cached || typeof cached.checkedAt !== 'string') return null; + return cached; + } catch { + return null; + } +} + +function writeCache(cachePath: string, result: UpdateCheckResult): void { + const dir = path.dirname(cachePath); + mkdirSync(dir, { recursive: true }); + writeFileSync(cachePath, `${JSON.stringify(result, null, 2)}\n`, 'utf8'); +} + +async function fetchJson(url: string, fetchImpl: typeof fetch): Promise> { + const response = await fetchImpl(url, { + headers: { + Accept: 'application/json', + 'User-Agent': 'aelf-skills-update-check', + }, + }); + if (!response.ok) { + throw new Error(`HTTP ${response.status} for ${url}`); + } + return (await response.json()) as Record; +} + +async function fetchNpmLatestVersion(packageName: string, fetchImpl: typeof fetch): Promise { + const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`; + const payload = await fetchJson(registryUrl, fetchImpl); + const latest = payload?.['dist-tags']?.latest; + if (typeof latest !== 'string' || !latest.trim()) { + throw new Error(`npm latest version not found for ${packageName}`); + } + return latest.trim(); +} + +function toGitHubRepo(repositoryUrl?: string): { owner: string; repo: string; httpsUrl: string } | null { + const normalized = normalizeRepoUrlToHttps(repositoryUrl); + if (!normalized) return null; + const cleaned = normalized.replace(/\.git$/, ''); + const match = cleaned.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)$/i); + if (!match) return null; + return { + owner: match[1], + repo: match[2], + httpsUrl: `https://github.com/${match[1]}/${match[2]}`, + }; +} + +function firstMeaningfulLine(markdown: string): string | null { + const lines = markdown.split(/\r?\n/).map(v => v.trim()); + for (const line of lines) { + if (!line) continue; + if (line.startsWith('#')) continue; + return line; + } + return null; +} + +function truncate(text: string, maxChars = 180): string { + if (text.length <= maxChars) return text; + return `${text.slice(0, maxChars - 1)}…`; +} + +async function fetchGitHubReleaseDigest( + repositoryUrl: string | undefined, + currentVersion: string, + latestVersion: string, + fetchImpl: typeof fetch, +): Promise { + const repo = toGitHubRepo(repositoryUrl); + if (!repo) { + return { + source: 'none', + summary: `${currentVersion} -> ${latestVersion}`, + }; + } + + const tagCandidates = [`v${latestVersion}`, latestVersion]; + + for (const tag of tagCandidates) { + const apiUrl = `https://api.github.com/repos/${repo.owner}/${repo.repo}/releases/tags/${encodeURIComponent(tag)}`; + const response = await fetchImpl(apiUrl, { + headers: { + Accept: 'application/vnd.github+json', + 'User-Agent': 'aelf-skills-update-check', + }, + }); + + if (response.status === 404) { + continue; + } + if (!response.ok) { + throw new Error(`GitHub release API ${response.status} for ${repo.owner}/${repo.repo}`); + } + + const release = (await response.json()) as Record; + const body = typeof release.body === 'string' ? release.body : ''; + const title = typeof release.name === 'string' ? release.name : ''; + const summary = firstMeaningfulLine(body) || title || `${currentVersion} -> ${latestVersion}`; + const releaseUrl = + typeof release.html_url === 'string' && release.html_url + ? release.html_url + : `${repo.httpsUrl}/releases/tag/${tag}`; + + return { + source: 'github-release', + summary: truncate(summary), + url: releaseUrl, + }; + } + + return { + source: 'github-compare', + summary: `${currentVersion} -> ${latestVersion}`, + url: `${repo.httpsUrl}/compare/v${currentVersion}...v${latestVersion}`, + }; +} + +function buildEmptyHub(packageName: string, currentVersion: string): HubUpdateInfo { + return { + packageName, + currentVersion, + latestVersion: currentVersion, + outdated: false, + diffLevel: 'none', + }; +} + +function buildResultSkeleton(localPackage: LocalPackageInfo, checkedAt: string): UpdateCheckResult { + return { + checkedAt, + fromCache: false, + hasUpdates: false, + hub: buildEmptyHub(localPackage.name, localPackage.version), + catalogOutdated: [], + notesDigest: [], + sourceStatus: { + npm: 'ok', + github: 'skipped', + errors: [], + }, + }; +} + +function toNotesDigest(result: UpdateCheckResult): ReleaseDigest[] { + const notes: ReleaseDigest[] = []; + if (result.hub.outdated && result.hub.release) { + notes.push(result.hub.release); + } + for (const skill of result.catalogOutdated.slice(0, 5)) { + if (skill.release) { + notes.push(skill.release); + } + } + return notes; +} + +async function buildLiveResult(options: UpdateCheckOptions = {}): Promise { + const fetchImpl = options.fetchImpl || fetch; + const now = (options.now || (() => new Date()))(); + const packageJsonPath = path.resolve(options.packageJsonPath || getDefaultPackageJsonPath()); + const catalogPath = path.resolve(options.catalogPath || getDefaultCatalogPath()); + const localPackage = readLocalPackageInfo(packageJsonPath); + const result = buildResultSkeleton(localPackage, now.toISOString()); + + try { + const latestHubVersion = await fetchNpmLatestVersion(localPackage.name, fetchImpl); + const hubCmp = compareSemver(localPackage.version, latestHubVersion); + const hubOutdated = hubCmp < 0; + result.hub = { + packageName: localPackage.name, + currentVersion: localPackage.version, + latestVersion: latestHubVersion, + outdated: hubOutdated, + diffLevel: getVersionDiffLevel(localPackage.version, latestHubVersion), + }; + if (hubOutdated) { + try { + result.hub.release = await fetchGitHubReleaseDigest( + localPackage.repositoryUrl, + localPackage.version, + latestHubVersion, + fetchImpl, + ); + result.sourceStatus.github = 'ok'; + } catch (error) { + result.sourceStatus.github = 'error'; + result.sourceStatus.errors.push( + `hub release digest failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + } catch (error) { + result.sourceStatus.npm = 'error'; + result.sourceStatus.errors.push(`hub npm check failed: ${error instanceof Error ? error.message : String(error)}`); + } + + if (!existsSync(catalogPath)) { + result.sourceStatus.errors.push(`catalog not found: ${catalogPath}`); + result.hasUpdates = result.hub.outdated; + result.notesDigest = toNotesDigest(result); + return result; + } + + const catalog = readJsonFile(catalogPath); + const outdated: OutdatedSkillItem[] = []; + + for (const skill of catalog.skills) { + try { + const latestVersion = await fetchNpmLatestVersion(skill.npm.name, fetchImpl); + if (compareSemver(skill.npm.version, latestVersion) >= 0) { + continue; + } + + const item: OutdatedSkillItem = { + id: skill.id, + packageName: skill.npm.name, + currentVersion: skill.npm.version, + latestVersion, + diffLevel: getVersionDiffLevel(skill.npm.version, latestVersion), + repositoryUrl: skill.repository.https, + }; + + try { + item.release = await fetchGitHubReleaseDigest( + skill.repository.https, + skill.npm.version, + latestVersion, + fetchImpl, + ); + if (result.sourceStatus.github !== 'error') { + result.sourceStatus.github = 'ok'; + } + } catch (error) { + if (result.sourceStatus.github !== 'error') { + result.sourceStatus.github = 'error'; + } + result.sourceStatus.errors.push( + `${skill.id} release digest failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + outdated.push(item); + } catch (error) { + if (result.sourceStatus.npm !== 'error') { + result.sourceStatus.npm = 'error'; + } + result.sourceStatus.errors.push( + `${skill.id} npm check failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + if (result.sourceStatus.github === 'skipped' && outdated.length > 0) { + // outdated exists but no github notes were retrieved successfully. + result.sourceStatus.github = 'error'; + } + + result.catalogOutdated = outdated; + result.hasUpdates = result.hub.outdated || outdated.length > 0; + result.notesDigest = toNotesDigest(result); + return result; +} + +function withCacheFlag(result: UpdateCheckResult, fromCache: boolean): UpdateCheckResult { + return { + ...result, + fromCache, + }; +} + +export async function checkForUpdates(options: UpdateCheckOptions = {}): Promise { + if (!isEnabled(options)) { + return null; + } + + const now = (options.now || (() => new Date()))(); + const ttlHours = resolveTtlHours(options); + const cachePath = resolveCachePath(options); + const cached = readCache(cachePath); + + if (!options.force && cached && isCacheFresh(cached.checkedAt, ttlHours, now)) { + return withCacheFlag(cached, true); + } + + try { + const live = await buildLiveResult(options); + if (cached?.lastNotifiedAt) { + live.lastNotifiedAt = cached.lastNotifiedAt; + } + writeCache(cachePath, live); + return withCacheFlag(live, false); + } catch (error) { + if (cached) { + const fallback = withCacheFlag(cached, true); + fallback.sourceStatus.errors = [ + ...fallback.sourceStatus.errors, + `live check failed, fallback to cache: ${error instanceof Error ? error.message : String(error)}`, + ]; + return fallback; + } + throw error; + } +} + +export function renderReminderLines(result: UpdateCheckResult): string[] { + if (!result.hasUpdates) return []; + const lines: string[] = []; + + if (result.hub.outdated) { + lines.push( + `[UPDATE] aelf-skills hub update available: ${result.hub.currentVersion} -> ${result.hub.latestVersion}`, + ); + } + + if (result.catalogOutdated.length > 0) { + const sample = result.catalogOutdated + .slice(0, 2) + .map(item => `${item.id} ${item.currentVersion}->${item.latestVersion}`) + .join(', '); + lines.push(`[UPDATE] ${result.catalogOutdated.length} skill package update(s): ${sample}`); + } + + lines.push('[ACTION] Use pinned release flow: ./bootstrap.sh --source npm'); + return lines; +} + +export function renderHumanSummary(result: UpdateCheckResult): string { + const lines: string[] = []; + const hubCmp = compareSemver(result.hub.currentVersion, result.hub.latestVersion); + const hubStatus = hubCmp < 0 ? 'outdated' : hubCmp > 0 ? 'ahead' : 'ok'; + + lines.push('[Update Check]'); + lines.push(`Checked at: ${result.checkedAt}${result.fromCache ? ' (cache)' : ''}`); + lines.push( + `Hub: ${result.hub.packageName} ${result.hub.currentVersion} -> ${result.hub.latestVersion} [${hubStatus}]`, + ); + + if (result.hub.outdated && result.hub.release) { + lines.push(`Hub notes: ${result.hub.release.summary}`); + if (result.hub.release.url) { + lines.push(` ${result.hub.release.url}`); + } + } + + if (result.catalogOutdated.length > 0) { + lines.push(`Outdated skill packages: ${result.catalogOutdated.length}`); + for (const item of result.catalogOutdated) { + lines.push( + `- ${item.id}: ${item.currentVersion} -> ${item.latestVersion} [${item.diffLevel}]`, + ); + if (item.release?.summary) { + lines.push(` notes: ${item.release.summary}`); + } + if (item.release?.url) { + lines.push(` ${item.release.url}`); + } + } + } else { + lines.push('Outdated skill packages: 0'); + } + + if (result.sourceStatus.errors.length > 0) { + lines.push('Source status: degraded'); + for (const errorLine of result.sourceStatus.errors) { + lines.push(`- ${errorLine}`); + } + } + + if (result.hasUpdates) { + lines.push('[Action]'); + lines.push(`- Upgrade hub if needed: npm i -g ${result.hub.packageName}@latest`); + lines.push('- Keep release-locked install: ./bootstrap.sh --source npm'); + } else { + lines.push('[OK] Hub and catalog pinned versions are up to date.'); + } + + return lines.join('\n'); +} + +export async function maybePrintUpdateReminder(options: UpdateCheckOptions = {}): Promise { + try { + const now = (options.now || (() => new Date()))(); + const ttlHours = resolveTtlHours(options); + const cachePath = resolveCachePath(options); + const result = await checkForUpdates(options); + if (!result || !result.hasUpdates) return; + + const lastNotifiedAt = result.lastNotifiedAt; + if (lastNotifiedAt && isCacheFresh(lastNotifiedAt, ttlHours, now)) { + return; + } + + const lines = renderReminderLines(result); + if (lines.length === 0) return; + for (const line of lines) { + console.log(line); + } + + writeCache(cachePath, { + ...result, + lastNotifiedAt: now.toISOString(), + }); + } catch { + // Non-blocking reminder path: swallow all check errors. + } +} diff --git a/scripts/lib/update-types.ts b/scripts/lib/update-types.ts new file mode 100644 index 0000000..7b7a490 --- /dev/null +++ b/scripts/lib/update-types.ts @@ -0,0 +1,58 @@ +export type VersionDiffLevel = 'major' | 'minor' | 'patch' | 'none' | 'unknown'; + +export type ReleaseDigestSource = 'github-release' | 'github-compare' | 'none'; + +export interface ReleaseDigest { + source: ReleaseDigestSource; + summary: string; + url?: string; +} + +export interface HubUpdateInfo { + packageName: string; + currentVersion: string; + latestVersion: string; + outdated: boolean; + diffLevel: VersionDiffLevel; + release?: ReleaseDigest; +} + +export interface OutdatedSkillItem { + id: string; + packageName: string; + currentVersion: string; + latestVersion: string; + diffLevel: VersionDiffLevel; + repositoryUrl?: string; + release?: ReleaseDigest; +} + +export interface UpdateSourceStatus { + npm: 'ok' | 'error' | 'skipped'; + github: 'ok' | 'error' | 'skipped'; + errors: string[]; +} + +export interface UpdateCheckResult { + checkedAt: string; + fromCache: boolean; + hasUpdates: boolean; + lastNotifiedAt?: string; + hub: HubUpdateInfo; + catalogOutdated: OutdatedSkillItem[]; + notesDigest: ReleaseDigest[]; + sourceStatus: UpdateSourceStatus; +} + +export interface UpdateCheckOptions { + force?: boolean; + quiet?: boolean; + json?: boolean; + allowWhenDisabled?: boolean; + ttlHours?: number; + cachePath?: string; + catalogPath?: string; + packageJsonPath?: string; + now?: () => Date; + fetchImpl?: typeof fetch; +} diff --git a/scripts/update-check.ts b/scripts/update-check.ts new file mode 100644 index 0000000..0e2a3e9 --- /dev/null +++ b/scripts/update-check.ts @@ -0,0 +1,50 @@ +import { checkForUpdates, renderHumanSummary } from './lib/update-check.ts'; + +interface CliOptions { + force: boolean; + quiet: boolean; + json: boolean; +} + +function parseArgs(): CliOptions { + const args = process.argv.slice(2); + return { + force: args.includes('--force'), + quiet: args.includes('--quiet'), + json: args.includes('--json'), + }; +} + +async function main(): Promise { + try { + const options = parseArgs(); + const result = await checkForUpdates({ + force: options.force, + allowWhenDisabled: true, + }); + + if (!result) { + if (!options.quiet) { + console.log('[INFO] Update check disabled by environment configuration.'); + } + return; + } + + if (options.json) { + console.log(JSON.stringify(result, null, 2)); + return; + } + + if (options.quiet && !result.hasUpdates) { + return; + } + + console.log(renderHumanSummary(result)); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(message.startsWith('[FAIL]') ? message : `[FAIL] ${message}`); + process.exitCode = 1; + } +} + +void main(); diff --git a/skills-catalog.json b/skills-catalog.json index 4afebee..93a991a 100644 --- a/skills-catalog.json +++ b/skills-catalog.json @@ -1,6 +1,6 @@ { "schemaVersion": "1.2.0", - "generatedAt": "2026-02-27T15:24:17.210Z", + "generatedAt": "2026-03-04T18:01:08.274Z", "source": "workspace.json", "skills": [ { @@ -8,7 +8,7 @@ "displayName": "AElf Node Skill", "npm": { "name": "@blockchain-forever/aelf-node-skill", - "version": "0.1.0" + "version": "0.1.3" }, "repository": { "https": "https://github.com/AElfProject/aelf-node-skill.git" @@ -19,6 +19,7 @@ "Chain reads: status, block, transaction result, metadata", "Contract operations: view call and transaction sending", "Node registry import/list with REST-first and SDK fallback strategy", + "Shared signer resolution for write operations: `explicit -> context -> env`", "Supports SDK, CLI, MCP, and OpenClaw integration from one codebase." ], "artifacts": { @@ -47,7 +48,7 @@ "displayName": "AelfScan Skill", "npm": { "name": "@aelfscan/agent-skills", - "version": "0.2.0" + "version": "0.2.2" }, "repository": { "https": "https://github.com/AelfScanProject/aelfscan-skill.git" @@ -86,7 +87,7 @@ "displayName": "Awaken Agent Skill", "npm": { "name": "@awaken-finance/agent-kit", - "version": "1.2.1" + "version": "1.2.4" }, "repository": { "https": "https://github.com/Awaken-Finance/awaken-agent-skills.git" @@ -97,6 +98,7 @@ "Read operations: quote, pair info, balances, allowance, positions", "Write operations: swap, add/remove liquidity, approve", "SignalR-based K-line retrieval and interval discovery", + "Shared signer resolution for write tools: `explicit -> context -> env`", "Supports SDK, CLI, MCP, and OpenClaw integration from one codebase." ], "artifacts": { @@ -125,7 +127,7 @@ "displayName": "eForest Agent Skill", "npm": { "name": "@eforest-finance/agent-skills", - "version": "0.4.0" + "version": "0.4.3" }, "repository": { "https": "https://github.com/eforest-finance/eforest-agent-skills.git" @@ -136,6 +138,7 @@ "Symbol-market operations: buy seed, create token, issue token", "Forest operations: collection/item creation, listing, offer, trade", "Config-first service gating and graceful degradation", + "Shared signer resolution for write operations: `explicit -> context -> env`", "Supports SDK, CLI, MCP, and OpenClaw integration from one codebase." ], "artifacts": { @@ -164,7 +167,7 @@ "displayName": "Portkey CA Agent Skill", "npm": { "name": "@portkey/ca-agent-skills", - "version": "1.1.2" + "version": "1.1.5" }, "repository": { "https": "https://github.com/Portkey-Wallet/ca-agent-skills.git" @@ -175,6 +178,7 @@ "Auth operations: verifier, email code, register, recover, status", "Query operations: account, guardian, assets, chain config", "Tx operations: transfer, contract call, approvals, keystore workflows", + "Shared wallet context: auto-set active CA profile for cross-skill signer resolution", "Supports SDK, CLI, MCP, and OpenClaw integration from one codebase." ], "artifacts": { @@ -203,7 +207,7 @@ "displayName": "Portkey EOA Agent Skill", "npm": { "name": "@portkey/eoa-agent-skills", - "version": "1.2.1" + "version": "1.2.4" }, "repository": { "https": "https://github.com/Portkey-Wallet/eoa-agent-skills.git" @@ -212,6 +216,7 @@ "description_zh": "Portkey EOA 钱包与资产操作技能。", "capabilities": [ "Wallet lifecycle: create, import, list, backup, delete", + "Shared wallet context: auto-set active wallet for cross-skill signer resolution", "Asset/query operations: token balances, NFTs, history, prices", "Transfer and contract execution via CLI/MCP/SDK adapters", "Supports SDK, CLI, MCP, and OpenClaw integration from one codebase." @@ -242,7 +247,7 @@ "displayName": "TomorrowDAO Agent Skill", "npm": { "name": "@tomorrowdao/agent-skills", - "version": "0.1.0" + "version": "0.1.4" }, "repository": { "https": "https://github.com/TomorrowDAOProject/tomorrowDAO-skill.git" @@ -253,6 +258,7 @@ "DAO domain: create/update/proposal/discussion operations", "Network governance and BP election operation set", "Resource token trading with unified ToolResult/TxReceipt outputs", + "Shared signer resolution for send mode: `explicit -> context -> env`", "Supports SDK, CLI, MCP, and OpenClaw integration from one codebase." ], "artifacts": { @@ -278,6 +284,7 @@ } ], "warnings": [ - "[WARN] portkey/agent-skills-e2e: SKILL.md not found, project skipped" + "[WARN] portkey/agent-skills-e2e: SKILL.md not found, project skipped", + "[WARN] vibeCoding/aelf-skill-prompt-pilot: SKILL.md not found, project skipped" ] }