diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index fb3aa7a5..3004ba9f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ai-devkit", - "version": "0.21.0", + "version": "0.21.1", "description": "Structured AI-assisted development with phase workflows, persistent memory, and reusable skills", "author": { "name": "Hoang Nguyen", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 00000000..7924e448 --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,26 @@ +{ + "name": "ai-devkit", + "version": "0.21.1", + "description": "AI-assisted development toolkit for Codex with structured workflows, reusable commands, persistent memory, and skills", + "author": { + "name": "Hoang Nguyen", + "email": "hoang@codeaholicguy.com" + }, + "homepage": "https://ai-devkit.com/", + "repository": "https://github.com/codeaholicguy/ai-devkit", + "license": "MIT", + "keywords": [ + "ai", + "development", + "codex", + "workflow", + "memory", + "skills", + "commands", + "testing", + "code-review", + "debugging" + ], + "commands": "./commands/", + "skills": "./skills/" +} diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 942ed394..0534946e 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ai-devkit", - "version": "0.21.0", + "version": "0.21.1", "description": "AI-assisted development toolkit with structured SDLC workflows, persistent memory, and reusable skills", "author": { "name": "Hoang Nguyen", diff --git a/.github/workflows/publish-channel-connector.yml b/.github/workflows/publish-channel-connector.yml new file mode 100644 index 00000000..3cdd4e90 --- /dev/null +++ b/.github/workflows/publish-channel-connector.yml @@ -0,0 +1,38 @@ +name: Publish Channel Connector Package + +on: + release: + types: [created] + workflow_dispatch: + inputs: + tag: + description: 'Package version tag (e.g., channel-connector-v0.1.0)' + required: true + +jobs: + publish: + name: Publish @ai-devkit/channel-connector to NPM + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build all packages + run: npm run build + + - name: Publish to NPM + working-directory: ./packages/channel-connector + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..ab4c1191 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +npm run lint +npm run test diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 00000000..e69de29b diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ff480e..c94436fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.21.0] - 2026-03-14 +## [Unreleased] + +### Added + +- **Channel Connector Package** - Added new `@ai-devkit/channel-connector` package with channel management, config storage, and Telegram adapter support, plus CLI commands for channel operations (#53). +- **Channel Connector Release Workflow** - Added publish workflow and package ignore rules for releasing the channel connector package. +- **Codex Plugin Config** - Added `.codex-plugin/plugin.json` configuration. +- **Dev Lifecycle Guide** - Added web documentation for the dev-lifecycle skill. +- **Shopify Toolkit Registry Entry** - Added the Shopify toolkit skill to the registry. +- **Git Hooks** - Added Husky `pre-commit` and `pre-push` hooks. + +### Changed + +- **Memory Skill Workflow** - Improved the memory skill workflow and updated the OpenAI agent configuration used by the skill. +- **Dev Lifecycle Brainstorming** - Updated the dev-lifecycle `new-requirement` guidance to include brainstorming refinements. +- **Code Review Guidance** - Refreshed code review instructions across command templates and dev-lifecycle references. +- **CLI Environment Support** - Added Amp Code environment configuration support and updated related CLI expectations. + +## [0.21.1] - 2026-04-04 + +### Fixed + +- **Interactive Skill Add Parsing** - Fixed `ai-devkit skill add ` so it no longer requires `skill-name` and can open the interactive skill selection flow. + +## [0.21.0] - 2026-04-04 ### Added diff --git a/README.md b/README.md index 9e37a29b..cd30fe0e 100644 --- a/README.md +++ b/README.md @@ -18,19 +18,19 @@ This launches an interactive setup wizard that configures your project for AI-as ## Supported Agents -| Agent | Status | -|-------|--------| -| [Claude Code](https://www.anthropic.com/claude-code) | ✅ Supported | -| [GitHub Copilot](https://code.visualstudio.com/) | ✅ Supported | -| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✅ Supported | -| [Cursor](https://cursor.sh/) | ✅ Supported | -| [opencode](https://opencode.ai/) | ✅ Supported | -| [Antigravity](https://antigravity.google/) | ✅ Supported | -| [Codex CLI](https://github.com/openai/codex) | ✅ Supported | -| [Windsurf](https://windsurf.com/) | 🚧 Testing | -| [Kilo Code](https://github.com/Kilo-Org/kilocode) | 🚧 Testing | -| [Roo Code](https://roocode.com/) | 🚧 Testing | -| [Amp](https://ampcode.com/) | 🚧 Testing | +| Agent | Agent Setup Support | Agent Control Support | +|-------|---------------------|-----------------------| +| [Claude Code](https://www.anthropic.com/claude-code) | ✅ Supported | ✅ Ready | +| [GitHub Copilot](https://code.visualstudio.com/) | ✅ Supported | ❌ Not Ready | +| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✅ Supported | ❌ Not Ready | +| [Cursor](https://cursor.sh/) | ✅ Supported | ❌ Not Ready | +| [opencode](https://opencode.ai/) | ✅ Supported | ❌ Not Ready | +| [Antigravity](https://antigravity.google/) | ✅ Supported | ❌ Not Ready | +| [Codex CLI](https://github.com/openai/codex) | ✅ Supported | ✅ Ready | +| [Windsurf](https://windsurf.com/) | 🚧 Testing | ❌ Not Ready | +| [Kilo Code](https://github.com/Kilo-Org/kilocode) | 🚧 Testing | ❌ Not Ready | +| [Roo Code](https://roocode.com/) | 🚧 Testing | ❌ Not Ready | +| [Amp](https://ampcode.com/) | 🚧 Testing | ❌ Not Ready | ## Documentation diff --git a/commands/code-review.md b/commands/code-review.md index 13d73611..5287768d 100644 --- a/commands/code-review.md +++ b/commands/code-review.md @@ -2,13 +2,20 @@ description: Pre-push code review against design docs. --- -Perform a local code review **before** pushing changes. +Perform a **holistic** local code review **before** pushing changes. Go beyond the diff — review how changes integrate with the broader codebase. -1. **Gather Context** — If not already provided, ask for: feature/branch description, list of modified files, relevant design doc(s) (e.g., `docs/ai/design/feature-{name}.md`), known constraints or risky areas, and which tests have been run. Also review the latest diff via `git status` and `git diff --stat`. +1. **Gather Context** — If not already provided, ask for: feature/branch description, relevant design doc(s) (e.g., `docs/ai/design/feature-{name}.md`), known constraints or risky areas, and which tests have been run. Run `git status` and `git diff --stat` to identify modified files. 2. **Use Memory for Context** — Search memory for project review standards and recurring pitfalls: `npx ai-devkit@latest memory search --query "code review checklist project conventions"`. 3. **Understand Design Alignment** — For each design doc, summarize architectural intent and critical constraints. -4. **File-by-File Review** — For every modified file: check alignment with design/requirements and flag deviations, spot logic issues/edge cases/redundant code, flag security concerns (input validation, secrets, auth, data handling), check error handling/performance/observability, and identify missing or outdated tests. -5. **Cross-Cutting Concerns** — Verify naming consistency and project conventions. Confirm docs/comments updated where behavior changed. Identify missing tests (unit, integration, E2E). Check for needed configuration/migration updates. -6. **Store Reusable Knowledge** — Save durable review findings/checklists with `npx ai-devkit@latest memory store ...`. -7. **Summarize Findings** — Categorize each finding as **blocking**, **important**, or **nice-to-have** with: file, issue, impact, recommendation, and design reference. -8. **Next Command Guidance** — If blocking issues remain, return to `/execute-plan` (code fixes) or `/writing-test` (test gaps); if clean, proceed with push/PR workflow. +4. **Holistic Codebase Review** — For each modified file, use targeted grep/glob on exported names (functions, types, constants) to trace callers and dependents. Read only relevant sections (signatures, call sites, type defs) — skip files with no shared interface. Then check: + - **Consistency**: Scan 1–2 similar modules to verify the change follows established patterns. + - **Duplication**: Search for existing utilities the new code could reuse or now duplicates. + - **Contract integrity**: Verify type signatures, API contracts, and config/DB schemas remain consistent at integration boundaries. + - **Dependency health**: Check for circular dependencies or version conflicts from new/changed imports. + - **Breaking changes**: Are public APIs, CLI flags, env vars, or config keys changed in ways that break existing consumers? Check downstream dependents. + - **Rollback safety**: Can this change be safely reverted? Flag irreversible migrations, one-way data format changes, or state transitions that cannot be undone. +5. **File-by-File Review** — For every modified file: check alignment with design/requirements and flag deviations, spot logic issues/edge cases/redundant code, flag security concerns (input validation, secrets, auth, data handling), check error handling/performance/observability, and identify missing or outdated tests. +6. **Cross-Cutting Concerns** — Verify naming consistency and project conventions. Confirm docs/comments updated where behavior changed. Identify missing tests (unit, integration, E2E). Check for needed configuration/migration updates. +7. **Store Reusable Knowledge** — Save durable review findings/checklists with `npx ai-devkit@latest memory store ...`. +8. **Summarize Findings** — Categorize each finding as **blocking**, **important**, or **nice-to-have** with: file, issue, impact, recommendation, and design reference. Include findings from both the diff and the broader codebase analysis. +9. **Next Command Guidance** — If blocking issues remain, return to `/execute-plan` (code fixes) or `/writing-test` (test gaps); if clean, proceed with push/PR workflow. diff --git a/commands/new-requirement.md b/commands/new-requirement.md index 4ef4da4b..4d8f7aa7 100644 --- a/commands/new-requirement.md +++ b/commands/new-requirement.md @@ -4,8 +4,8 @@ description: Scaffold feature documentation from requirements through planning. Guide me through adding a new feature, from requirements documentation to implementation readiness. -1. **Capture Requirement** — If not already provided, ask for: feature name (kebab-case, e.g., `user-authentication`), what problem it solves and who will use it, and key user stories. -2. **Use Memory for Context** — Before asking repetitive clarification questions, search memory for related decisions or conventions via `npx ai-devkit@latest memory search --query ""` and reuse relevant context. +1. **Use AI DevKit Memory for Context** — Search AI DevKit memory (not built-in memory) for related decisions or conventions via `npx ai-devkit@latest memory search --query ""` and reuse relevant context. If unfamiliar, check the AI DevKit memory skill first. +2. **Capture Requirement** — Ask for: feature name (kebab-case, e.g., `user-authentication`), problem it solves, who uses it, key user stories. **Brainstorm**: ask clarifying questions as needed, explore alternatives to confirm this is the right thing to build, then present 2–3 approaches with one-line trade-offs and a recommendation. 3. **Create Feature Documentation Structure** — Copy each template's content (preserving YAML frontmatter and section headings) into feature-specific files: - `docs/ai/requirements/README.md` → `docs/ai/requirements/feature-{name}.md` - `docs/ai/design/README.md` → `docs/ai/design/feature-{name}.md` diff --git a/docs/ai/design/feature-channel-connector.md b/docs/ai/design/feature-channel-connector.md new file mode 100644 index 00000000..206e4340 --- /dev/null +++ b/docs/ai/design/feature-channel-connector.md @@ -0,0 +1,305 @@ +--- +phase: design +title: "Channel Connector: System Design" +description: Technical architecture for the channel-connector package as a generic messaging bridge +--- + +# System Design: Channel Connector + +## Architecture Overview + +The key architectural principle: **channel-connector is a pure message pipe**. It knows nothing about agents. CLI is the orchestrator that connects channel-connector with agent-manager. + +```mermaid +graph TD + subgraph Telegram + TG_USER[Developer on Phone] + TG_API[Telegram Bot API] + end + + subgraph "@ai-devkit/channel-connector" + CM[ChannelManager] + TA[TelegramAdapter] + CA[ChannelAdapter Interface] + CS[ConfigStore] + end + + subgraph "@ai-devkit/cli" + CMD[channel commands] + INPUT_HANDLER[Input Handler] + OUTPUT_LOOP[Output Polling Loop] + end + + subgraph "@ai-devkit/agent-manager" + AM[AgentManager] + TW[TtyWriter] + GC[getConversation] + end + + subgraph Running Agent + A1[Claude Code] + SF[Session JSONL File] + end + + TG_USER -->|send message| TG_API + TG_API -->|long polling| TA + TA -->|incoming msg| INPUT_HANDLER + INPUT_HANDLER -->|fire-and-forget| TW + TW -->|keyboard input| A1 + + A1 -->|writes output| SF + OUTPUT_LOOP -->|poll getConversation| GC + GC -->|read| SF + OUTPUT_LOOP -->|new assistant msgs| TA + TA -->|sendMessage| TG_API + TG_API -->|push| TG_USER + + CMD -->|configure| CS + CMD -->|create & start| CM +``` + +### Key Separation of Concerns + +| Layer | Package | Responsibility | +|-------|---------|---------------| +| **Channel** | `channel-connector` | Connect to messaging platforms, receive/send text. No agent knowledge. | +| **Orchestration** | `cli` | Wire channel-connector to agent-manager. Provide message handler. | +| **Agent** | `agent-manager` | Detect agents, send input (TtyWriter), read conversation history. | + +### Key Components (channel-connector only) + +| Component | Responsibility | +|-----------|---------------| +| `ChannelManager` | Registers adapters, manages lifecycle, routes messages to/from handler callback | +| `ChannelAdapter` | Interface for messaging platforms (Telegram, future: Slack, WhatsApp) | +| `TelegramAdapter` | Telegram Bot API integration via long polling | +| `ConfigStore` | Persists channel configurations (tokens, chat IDs, preferences) | + +### Technology Choices +- **Telegram library**: `telegraf` (mature, TypeScript-native, active maintenance) +- **Config storage**: JSON file at `~/.ai-devkit/channels.json` +- **Process model**: Foreground process via `ai-devkit channel start` + +## Data Models + +### ChannelConfig +```typescript +interface ChannelConfig { + channels: Record; +} + +interface ChannelEntry { + type: 'telegram' | 'slack' | 'whatsapp'; + enabled: boolean; + createdAt: string; + config: TelegramConfig; // extend with union for future channel types +} + +interface TelegramConfig { + botToken: string; + botUsername: string; + authorizedChatId?: number; // auto-set from first user to message the bot +} +``` + +### Message Types (generic, no agent concepts) +```typescript +interface IncomingMessage { + channelType: string; + chatId: string; + userId: string; + text: string; + timestamp: Date; + metadata?: Record; +} + +/** Handler function provided by the consumer (CLI). Fire-and-forget — returns void. */ +type MessageHandler = (message: IncomingMessage) => Promise; +``` + +### Authorization Model +- First user to message the bot is auto-authorized: their `chatId` is stored in config via `ConfigStore` +- Subsequent messages from other chat IDs are rejected +- CLI tracks the authorized `chatId` in-memory for the output polling loop (captured from first incoming message) + +## API Design + +### ChannelAdapter Interface +```typescript +interface ChannelAdapter { + readonly type: string; + + /** Start listening for messages */ + start(): Promise; + + /** Stop listening */ + stop(): Promise; + + /** Send a message to a specific chat. Automatically chunks messages exceeding platform limits (e.g., 4096 chars for Telegram), splitting at newline boundaries. */ + sendMessage(chatId: string, text: string): Promise; + + /** Register handler for incoming text messages (fire-and-forget, responses sent via sendMessage) */ + onMessage(handler: (msg: IncomingMessage) => Promise): void; + + /** Check if adapter is connected and healthy */ + isHealthy(): Promise; +} +``` + +### ChannelManager API +```typescript +class ChannelManager { + registerAdapter(adapter: ChannelAdapter): void; + getAdapter(type: string): ChannelAdapter | undefined; + startAll(): Promise; + stopAll(): Promise; +} +``` + +### ConfigStore API +```typescript +class ConfigStore { + constructor(configPath?: string); // defaults to ~/.ai-devkit/channels.json + + getConfig(): Promise; + saveChannel(name: string, entry: ChannelEntry): Promise; + removeChannel(name: string): Promise; + getChannel(name: string): Promise; +} +``` + +### CLI Integration Pattern +```typescript +// In CLI's `channel start --agent ` command +import { ChannelManager, TelegramAdapter, ConfigStore } from '@ai-devkit/channel-connector'; +import { AgentManager, ClaudeCodeAdapter, CodexAdapter, TtyWriter } from '@ai-devkit/agent-manager'; + +// 1. Set up agent-manager and resolve target agent by name +const agentManager = new AgentManager(); +agentManager.registerAdapter(new ClaudeCodeAdapter()); +agentManager.registerAdapter(new CodexAdapter()); +const agents = await agentManager.listAgents(); +const agent = agents.find(a => a.name === targetAgentName); + +// 2. Set up channel-connector +const config = await configStore.getChannel('telegram'); +const telegram = new TelegramAdapter({ botToken: config.botToken }); + +// 3. Track chatId from first incoming message +let activeChatId: string | null = null; + +// 4. Register message handler (fire-and-forget to agent, capture chatId) +telegram.onMessage(async (msg) => { + if (!activeChatId) { + activeChatId = msg.chatId; // auto-authorize first user + } + if (msg.chatId !== activeChatId) return; // reject unauthorized + await ttyWriter.write(agent.pid, msg.text); // send to agent, don't wait +}); + +// 5. Start agent output observation loop (polls and pushes to Telegram) +let lastMessageCount = 0; +const pollInterval = setInterval(() => { + if (!activeChatId) return; // no user connected yet + const conversation = agentAdapter.getConversation(agent.sessionFilePath); + const newMessages = conversation.slice(lastMessageCount) + .filter(m => m.role === 'assistant'); + for (const msg of newMessages) { + telegram.sendMessage(activeChatId, msg.content); // auto-chunks at 4096 chars + } + lastMessageCount = conversation.length; +}, 2000); + +// 5. Start channel +const manager = new ChannelManager(); +manager.registerAdapter(telegram); +await manager.startAll(); +``` + +### CLI Commands +``` +ai-devkit channel connect telegram # Interactive setup (bot token prompt) +ai-devkit channel list # Show configured channels +ai-devkit channel disconnect telegram # Remove channel config +ai-devkit channel start --agent # Start bridge to specific agent +ai-devkit channel stop # Stop running bridge +ai-devkit channel status # Show bridge process status +``` + +## Component Breakdown + +### Package: `@ai-devkit/channel-connector` +``` +packages/channel-connector/ + src/ + index.ts # Public exports + ChannelManager.ts # Adapter registry and lifecycle + ConfigStore.ts # Config persistence (~/.ai-devkit/) + adapters/ + ChannelAdapter.ts # Interface definition + TelegramAdapter.ts # Telegram Bot API implementation + types.ts # Shared type definitions + __tests__/ + ChannelManager.test.ts + ConfigStore.test.ts + adapters/ + TelegramAdapter.test.ts + package.json + tsconfig.json + tsconfig.build.json + jest.config.ts +``` + +### CLI Integration (in `@ai-devkit/cli`) +``` +packages/cli/src/commands/ + channel.ts # channel connect/list/disconnect/start/stop/status +``` + +## Design Decisions + +### 1. Channel-connector has zero agent knowledge +**Decision**: No dependency on agent-manager. The package is a pure messaging bridge. +**Why**: Clean separation of concerns. Channel-connector can be used independently of agents (e.g., for notifications, other integrations). CLI is the natural integration point since it already depends on both packages. + +### 2. Async fire-and-forget message handling +**Decision**: Consumer registers a `MessageHandler` callback that returns `void`. Responses are sent separately via `sendMessage()`. +**Why**: Agent responses are inherently async (can take seconds to minutes). Decoupling input and output avoids blocking. CLI runs a separate polling loop that observes agent conversation via `getConversation()` and pushes new assistant messages to the channel. This also naturally handles unsolicited agent output (errors, completions). + +### 3. One agent per session (v1) +**Decision**: `channel start --agent ` binds one channel to one agent. +**Why**: Simplest mental model. No agent-routing logic needed in channel-connector. Developer explicitly chooses which agent to bridge. Can evolve to multi-agent in CLI later without changing channel-connector. + +### 4. Adapter pattern (consistent with agent-manager) +**Decision**: Use pluggable adapter interface for channel implementations. +**Why**: Proven pattern in this codebase. Makes adding Slack/WhatsApp straightforward. + +### 5. Long polling for Telegram (not webhooks) +**Decision**: Use Telegram's long polling via telegraf. +**Why**: No need for a public server/URL. Works behind firewalls and NAT. Simpler for CLI tool users. + +### 6. Single config file at `~/.ai-devkit/` +**Decision**: Store channel configs globally, not per-project. +**Why**: A Telegram bot bridges to agents across projects. Global config avoids re-setup per project. + +## Non-Functional Requirements + +### Performance +- Message delivery latency: < 3 seconds (Telegram → handler → Telegram) +- Memory footprint: < 50MB for the bridge process + +### Reliability +- Auto-reconnect on network failure with exponential backoff +- Graceful shutdown on SIGINT/SIGTERM +- Message queue for offline/reconnecting scenarios (in-memory, bounded) + +### Security +- Bot token stored with file permissions 0600 +- Only authorized chat IDs can interact with the bot +- No sensitive data logged or exposed in error messages +- Token validation on connect before persisting + +### Scalability +- v1 targets single-user, single-machine use +- Adapter interface supports future multi-channel, multi-user scenarios +- Handler pattern allows CLI to evolve routing without channel-connector changes diff --git a/docs/ai/implementation/feature-channel-connector.md b/docs/ai/implementation/feature-channel-connector.md new file mode 100644 index 00000000..6d2d6310 --- /dev/null +++ b/docs/ai/implementation/feature-channel-connector.md @@ -0,0 +1,108 @@ +--- +phase: implementation +title: "Channel Connector: Implementation Guide" +description: Technical implementation details for the channel-connector package (pure messaging bridge) +--- + +# Implementation Guide: Channel Connector + +## Development Setup + +### Prerequisites +- Node.js >= 20.20.0 +- npm (workspace-aware) +- Telegram account + bot created via BotFather + +### Package Setup +```bash +# Package lives at packages/channel-connector/ +# Build: tsc (consistent with agent-manager) +# Test: jest with ts-jest +``` + +## Code Structure + +``` +packages/channel-connector/ + src/ + index.ts # Public API exports + ChannelManager.ts # Adapter registry + lifecycle + ConfigStore.ts # ~/.ai-devkit/channels.json persistence + adapters/ + ChannelAdapter.ts # Interface definition + TelegramAdapter.ts # Telegraf-based implementation + types.ts # Shared types (no agent concepts) + __tests__/ + ChannelManager.test.ts + ConfigStore.test.ts + adapters/ + TelegramAdapter.test.ts +``` + +## Implementation Notes + +### Core Features + +**ChannelAdapter interface**: Generic messaging contract. Methods: `start()`, `stop()`, `sendMessage()`, `onMessage()`, `isHealthy()`. No agent-specific methods. + +**ChannelManager**: Holds registered adapters, manages their lifecycle (startAll/stopAll). Simple registry pattern consistent with AgentManager. + +**TelegramAdapter**: Wraps `telegraf` library. Uses long polling. On incoming text message: calls registered `MessageHandler` (fire-and-forget, handler returns void). `sendMessage()` allows the consumer (CLI) to push agent responses and notifications proactively. + +**ConfigStore**: Simple JSON file read/write at `~/.ai-devkit/channels.json`. Creates directory if needed. Sets file permissions to 0600 for token security. + +### Patterns & Best Practices + +- Follow existing patterns from agent-manager (adapter registration, type exports) +- Use same tsconfig, jest config patterns as sibling packages +- Keep telegraf as the only external dependency for the Telegram adapter +- All async operations return Promises (no callbacks except MessageHandler) +- No agent-manager imports — channel-connector is a standalone package + +## Integration Points + +### Consumer Pattern (CLI wires both packages) +```typescript +// In CLI — this is the ONLY place where both packages meet +import { ChannelManager, TelegramAdapter, ConfigStore } from '@ai-devkit/channel-connector'; +import { AgentManager, ClaudeCodeAdapter, TtyWriter } from '@ai-devkit/agent-manager'; + +const telegram = new TelegramAdapter({ botToken }); + +// Input: fire-and-forget to agent +telegram.onMessage(async (msg) => { + await ttyWriter.write(agent.pid, msg.text); // no waiting +}); + +// Output: polling loop pushes agent responses to Telegram +let lastCount = 0; +setInterval(() => { + const msgs = adapter.getConversation(agent.sessionFilePath); + const newAssistant = msgs.slice(lastCount).filter(m => m.role === 'assistant'); + for (const m of newAssistant) { + telegram.sendMessage(chatId, m.content); + } + lastCount = msgs.length; +}, 2000); + +await telegram.start(); +``` + +### Key Principle +- Channel-connector exposes: `onMessage(handler)` + `sendMessage(chatId, text)` +- CLI provides: the input handler (fire-and-forget) + the output polling loop +- Input and output are fully decoupled — agent can take any amount of time to respond + +## Error Handling + +- Invalid bot token → validate on connect (ConfigStore), clear error message +- Network failure → telegraf auto-reconnect + exponential backoff in TelegramAdapter +- Handler throws → catch in adapter, send error message to user in Telegram +- Config file corruption → backup and recreate with warning + +## Security Notes + +- Bot tokens stored at `~/.ai-devkit/channels.json` with 0600 permissions +- Chat ID allowlist prevents unauthorized users from interacting +- No secrets logged or exposed in error messages +- Token validated against Telegram API before persisting diff --git a/docs/ai/planning/feature-channel-connector.md b/docs/ai/planning/feature-channel-connector.md new file mode 100644 index 00000000..adb9c205 --- /dev/null +++ b/docs/ai/planning/feature-channel-connector.md @@ -0,0 +1,105 @@ +--- +phase: planning +title: "Channel Connector: Planning & Task Breakdown" +description: Implementation plan for the channel-connector package +--- + +# Planning: Channel Connector + +## Milestones + +- [x] Milestone 1: Package scaffold and core abstractions +- [x] Milestone 2: Telegram adapter (send/receive messages via callback) +- [x] Milestone 3: CLI channel commands and agent bridge +- [ ] Milestone 4: End-to-end flow working + +## Task Breakdown + +### Phase 1: Package Foundation +- [x] Task 1.1: Scaffold `packages/channel-connector` package (package.json, tsconfig, jest config, nx project config) +- [x] Task 1.2: Define core types (`IncomingMessage`, `MessageHandler`, `ChannelConfig`, `ChannelEntry`, `TelegramConfig`) +- [x] Task 1.3: Implement `ChannelAdapter` interface +- [x] Task 1.4: Implement `ChannelManager` (adapter registration, lifecycle start/stop) +- [x] Task 1.5: Implement `ConfigStore` (read/write `~/.ai-devkit/channels.json`, file permissions) +- [x] Task 1.6: Add `index.ts` public exports + +### Phase 2: Telegram Adapter +- [x] Task 2.1: Add `telegraf` dependency, implement `TelegramAdapter` (connect, long polling, map telegraf context to `IncomingMessage`) +- [x] Task 2.2: Implement `onMessage` — call registered handler (fire-and-forget, void return) +- [x] Task 2.3: Implement `sendMessage` with message chunking — split at 4096 chars preferring newline boundaries +- [x] Task 2.4: Implement chat authorization (auto-authorize first user, reject others) +- [ ] Task 2.5: Implement auto-reconnect with exponential backoff (deferred — telegraf handles basic reconnect) +- [x] Task 2.6: Implement graceful shutdown (SIGINT/SIGTERM handling in CLI) + +### Phase 3: CLI Integration +- [x] Task 3.1: Create `channel connect telegram` command (interactive bot token setup, validation, persist via ConfigStore) +- [x] Task 3.2: Create `channel list` command (show configured channels with status) +- [x] Task 3.3: Create `channel disconnect telegram` command (remove config) +- [x] Task 3.4: Create `channel start --agent ` command — resolve agent by name, instantiate channel-connector, wire input handler and output loop +- [x] Task 3.5: Implement input handler in CLI — capture chatId from first message, fire-and-forget to agent via TtyWriter +- [x] Task 3.6: Implement output polling loop in CLI — poll `getConversation()` from agent-manager, detect new assistant messages, push to tracked chatId via `sendMessage()` +- [x] Task 3.7: Create `channel status` command +- [x] Task 3.8: Register channel commands in CLI entry point (`cli.ts`) + +### Phase 4: Integration & Polish +- [ ] Task 4.1: End-to-end testing — connect Telegram, send message, verify agent receives and responds +- [x] Task 4.2: Handle edge cases — unauthorized user rejection, invalid token validation, handler errors +- [x] Task 4.3: Update root workspace config (package.json workspaces — already included via packages/*) + +## Dependencies + +```mermaid +graph LR + T1_1[1.1 Scaffold] --> T1_2[1.2 Types] + T1_2 --> T1_3[1.3 Adapter Interface] + T1_2 --> T1_5[1.5 ConfigStore] + T1_3 --> T1_4[1.4 ChannelManager] + T1_3 --> T2_1[2.1 TelegramAdapter] + T1_4 --> T1_6[1.6 Exports] + T2_1 --> T2_2[2.2 onMessage] + T2_1 --> T2_3[2.3 sendMessage] + T2_1 --> T2_4[2.4 Auth] + T2_2 --> T2_5[2.5 Reconnect] + T2_5 --> T2_6[2.6 Shutdown] + T1_5 --> T3_1[3.1 connect cmd] + T3_1 --> T3_2[3.2 list cmd] + T3_1 --> T3_3[3.3 disconnect cmd] + T1_6 --> T3_4[3.4 start cmd] + T2_2 --> T3_5[3.5 Input handler] + T3_4 --> T3_5 + T3_5 --> T3_6[3.6 Output polling loop] + T3_6 --> T3_7[3.7 stop/status] + T3_7 --> T3_8[3.8 Register CLI] + T3_8 --> T4_1[4.1 E2E] + T4_1 --> T4_2[4.2 Edge Cases] + T4_2 --> T4_3[4.3 Workspace Config] +``` + +### External Dependencies +- `telegraf` — Telegram Bot API library (channel-connector package) +- `@ai-devkit/agent-manager` — agent discovery and terminal I/O (CLI only, NOT in channel-connector) +- `@ai-devkit/channel-connector` — messaging bridge (CLI dependency) + +## Timeline & Estimates + +| Phase | Tasks | Effort | +|-------|-------|--------| +| Phase 1: Foundation | 6 tasks | Small | +| Phase 2: Telegram | 6 tasks | Medium | +| Phase 3: CLI | 8 tasks | Medium | +| Phase 4: Polish | 3 tasks | Small | + +## Risks & Mitigation + +| Risk | Impact | Mitigation | +|------|--------|------------| +| Agent response capture is unreliable (CLI responsibility) | Core feature broken | Evaluate agent-manager's `getConversation()` first; fallback to terminal monitoring | +| Telegraf API changes | Build breaks | Pin telegraf version, use stable API surface | +| TtyWriter limitations | Can't send to all agent types | Start with Claude Code (best supported), document limitations | +| Long-polling reliability | Missed messages | Implement reconnect with backoff in TelegramAdapter | + +## Resources Needed + +- **NPM package**: `telegraf` (Telegram Bot API) +- **Telegram Bot**: Created via BotFather (developer provides token) +- **Existing packages**: `@ai-devkit/agent-manager` (used by CLI, not by channel-connector) diff --git a/docs/ai/requirements/feature-channel-connector.md b/docs/ai/requirements/feature-channel-connector.md new file mode 100644 index 00000000..3db2ffe6 --- /dev/null +++ b/docs/ai/requirements/feature-channel-connector.md @@ -0,0 +1,110 @@ +--- +phase: requirements +title: "Channel Connector: Generic Messaging Bridge" +description: A generic package for connecting to messaging platforms (Telegram, Slack, WhatsApp) with callback-based message handling +--- + +# Requirements: Channel Connector + +## Problem Statement + +Developers using ai-devkit can only interact with their AI agents through the terminal. When away from their computer (commuting, in meetings, on mobile), they lose visibility into agent activity and cannot provide input. There is no mechanism to bridge the gap between a running agent and external messaging platforms. + +**Who is affected?** Developers who run long-lived agent sessions and need to monitor or interact with them remotely. + +**Current workaround:** Developers must return to their terminal or use remote desktop/SSH to check agent status and respond to prompts. + +## Goals & Objectives + +### Primary Goals +- Build a generic `@ai-devkit/channel-connector` package that provides a clean messaging abstraction for external platforms +- The package is a **pure message pipe** — receives messages, calls a handler, sends back responses. No knowledge of agents. +- Implement Telegram adapter as the first channel (via Bot API) +- CLI integrates channel-connector with agent-manager to bridge agents and messaging platforms + +### Secondary Goals +- Design the adapter interface to support future channels (Slack, WhatsApp) without breaking changes +- Keep the package independently useful beyond ai-devkit (generic messaging bridge) + +### Non-Goals (Out of Scope for v1) +- Slack and WhatsApp adapters (architecture supports them, but only Telegram is implemented) +- Rich media support (images, files, voice) — text-only for v1 +- Multi-user access control (single developer per bot) +- Web dashboard or custom UI +- End-to-end encryption beyond what Telegram provides natively +- Agent-specific logic inside channel-connector (no agent-manager dependency) + +## User Stories & Use Cases + +### US-1: Connect Telegram +> As a developer, I want to connect my Telegram bot to ai-devkit so that I can interact with my agents from my phone. + +**Flow:** `ai-devkit channel connect telegram` → prompts for bot token → validates token → stores config → confirms connection. + +### US-2: Start Channel Bridge with Agent +> As a developer, I want to start the channel bridge targeting a specific agent so that all Telegram messages are forwarded to that agent. + +**Flow:** `ai-devkit channel start --agent ` → resolves agent by name via agent-manager → starts Telegram bot → starts two concurrent loops: (1) incoming messages forwarded to agent via TtyWriter, (2) polling loop observes agent conversation and pushes new output to Telegram. + +### US-3: Chat with an Agent via Telegram +> As a developer, I want to send a message in Telegram and have it forwarded to my agent, then receive the agent's response back in Telegram. + +**Flow (async, non-blocking):** +1. Developer sends text in Telegram → channel-connector passes it to CLI-provided handler → handler sends to agent via TtyWriter (fire-and-forget, no waiting for response) +2. Separate polling loop in CLI: polls `getConversation()` from agent-manager → detects new assistant messages → calls `connector.sendMessage()` to push response to Telegram + +The two directions (input and output) are decoupled. This avoids blocking when agents take time to respond. + +### US-4: List Connected Channels +> As a developer, I want to run `ai-devkit channel list` to see all configured channels and their status. + +### US-5: Disconnect a Channel +> As a developer, I want to run `ai-devkit channel disconnect telegram` to remove a channel configuration. + +### US-6: Receive Agent Output +> As a developer, I want to see all agent output (responses, task completion, errors, prompts for input) in Telegram as they happen, without having to ask. + +**Flow:** CLI runs a continuous polling loop that reads the agent's conversation via `getConversation()` from agent-manager. When new assistant messages are detected (by tracking message count/timestamps), CLI calls `connector.sendMessage()` to push them to Telegram. This is the same loop that delivers responses for US-3 — all agent output flows through this single observation mechanism. + +### Edge Cases +- Agent terminates while user is chatting → CLI detects via agent-manager, sends notification through channel-connector +- Bot token is invalid or revoked → clear error message on connect and in Telegram +- Multiple Telegram users message the same bot → reject unauthorized users (only bot owner) +- Network interruption → reconnect with backoff, queue messages +- No agent specified at start → CLI shows error with available agents + +## Success Criteria + +1. Developer can set up Telegram connection in under 2 minutes via CLI +2. Messages round-trip (Telegram → agent → Telegram) in under 5 seconds on stable network +3. `@ai-devkit/channel-connector` has zero dependency on `@ai-devkit/agent-manager` +4. Package follows pluggable adapter pattern for extensibility +5. CLI commands (`channel connect/list/disconnect/start`) work consistently +6. Bot handles disconnections gracefully with automatic reconnection + +## Constraints & Assumptions + +### Constraints +- Must use Telegram Bot API (not Telegram client API / user accounts) +- `@ai-devkit/channel-connector` must NOT depend on `@ai-devkit/agent-manager` — all integration happens in CLI +- Must follow existing monorepo patterns (Nx, TypeScript, CommonJS, Jest) +- Package must be independently publishable as `@ai-devkit/channel-connector` + +### Assumptions +- Developer has a Telegram account and can create a bot via BotFather +- Developer runs ai-devkit on a machine with internet access +- CLI provides the message handler that bridges channel-connector to agent-manager +- One channel session connects to one agent (specified via `--agent ` flag, agent identified by `name` field from AgentInfo) + +## Resolved Decisions + +1. **Output capture**: CLI polls agent conversation via `getConversation(sessionFilePath)` from agent-manager. Tracks last seen message count/timestamp. Detects new assistant messages and pushes to channel via `sendMessage()`. No terminal monitoring needed. +2. **Config storage**: Global at `~/.ai-devkit/channels.json`. Channels are machine-wide, not per-project. +3. **Long-running process**: Foreground process via `ai-devkit channel start` for v1. Background daemon deferred. +4. **Rate limiting**: Skip for v1. Single-user use case. +5. **Agent identification**: By `name` field from `AgentInfo` (e.g., "ai-devkit"). PID is unique for disambiguation if needed. +6. **Message flow**: Fully async/non-blocking. Incoming messages fire-and-forget to agent. Separate polling loop observes agent output and pushes to channel. Handler signature is `Promise`, not `Promise`. + +## Open Items + +- None blocking for v1. diff --git a/docs/ai/testing/feature-channel-connector.md b/docs/ai/testing/feature-channel-connector.md new file mode 100644 index 00000000..8d269a9d --- /dev/null +++ b/docs/ai/testing/feature-channel-connector.md @@ -0,0 +1,92 @@ +--- +phase: testing +title: "Channel Connector: Testing Strategy" +description: Test plan for the channel-connector package (pure messaging bridge) +--- + +# Testing Strategy: Channel Connector + +## Test Coverage Goals + +- Unit test coverage target: 100% of new code +- Integration tests: Core message flow paths + error handling +- E2E tests: Manual verification of Telegram round-trip with agent + +## Coverage Results + +``` +---------------------|---------|----------|---------|---------| +File | % Stmts | % Branch | % Funcs | % Lines | +---------------------|---------|----------|---------|---------| +All files | 100 | 100 | 100 | 100 | + ChannelManager.ts | 100 | 100 | 100 | 100 | + ConfigStore.ts | 100 | 100 | 100 | 100 | + TelegramAdapter.ts | 100 | 100 | 100 | 100 | +---------------------|---------|----------|---------|---------| +``` + +**34 tests, all passing.** + +## Test Files + +- `packages/channel-connector/src/__tests__/ChannelManager.test.ts` (8 tests) +- `packages/channel-connector/src/__tests__/ConfigStore.test.ts` (12 tests) +- `packages/channel-connector/src/__tests__/adapters/TelegramAdapter.test.ts` (14 tests) + +## Unit Tests + +### ChannelManager (8 tests) +- [x] Register adapter and retrieve by type +- [x] startAll() calls start() on all registered adapters +- [x] stopAll() calls stop() on all registered adapters +- [x] Duplicate adapter type registration throws error +- [x] getAdapter() returns undefined for unregistered type +- [x] startAll() works with no adapters +- [x] stopAll() works with no adapters +- [x] Returns registered adapter by type + +### ConfigStore (12 tests) +- [x] Uses default path when no configPath provided +- [x] Write config creates file with correct permissions (0600) +- [x] Read config returns parsed JSON +- [x] Read missing config returns default empty config +- [x] Creates parent directory if missing +- [x] Handles corrupted JSON gracefully +- [x] saveChannel() adds entry +- [x] saveChannel() preserves existing channels +- [x] removeChannel() removes entry +- [x] removeChannel() handles non-existent channel +- [x] getChannel() returns entry +- [x] getChannel() returns undefined for non-existent channel + +### TelegramAdapter (14 tests) +- [x] Returns type "telegram" +- [x] Starts telegraf bot with correct token +- [x] Stops bot cleanly +- [x] Silently ignores messages when no handler registered +- [x] Maps telegraf message to IncomingMessage +- [x] Calls registered MessageHandler on incoming text (fire-and-forget) +- [x] Handles handler errors gracefully (Error instance) +- [x] Handles handler errors gracefully (non-Error thrown) +- [x] isHealthy() returns true after start +- [x] isHealthy() returns false before start +- [x] isHealthy() returns false after stop +- [x] sendMessage() sends text to specified chatId +- [x] sendMessage() chunks messages exceeding 4096 chars at newline boundaries +- [x] sendMessage() handles messages with no newlines (hard split at 4096) + +### CLI Channel Commands +CLI channel commands are integration-tested via manual E2E testing (requires running agent and Telegram bot). + +## Manual Testing + +- [ ] Create Telegram bot via BotFather +- [ ] Run `ai-devkit channel connect telegram` with token +- [ ] Run `ai-devkit channel list` — verify telegram shown +- [ ] Start an agent (e.g., Claude Code) +- [ ] Run `ai-devkit channel start --agent ` +- [ ] Send message in Telegram — verify agent receives input +- [ ] Verify agent response appears in Telegram +- [ ] Kill agent — verify error message in Telegram +- [ ] Test reconnection after network interruption +- [ ] Run `ai-devkit channel disconnect telegram` — verify removal diff --git a/package-lock.json b/package-lock.json index 1fed7c78..27a9ae9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ai-devkit", - "version": "0.21.0", + "version": "0.21.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ai-devkit", - "version": "0.21.0", + "version": "0.21.1", "license": "MIT", "workspaces": [ "apps/*", @@ -14,6 +14,7 @@ ], "devDependencies": { "@nx/js": "^22.4.0", + "husky": "^9.1.7", "nx": "^22.4.0" }, "engines": { @@ -24,6 +25,10 @@ "resolved": "packages/agent-manager", "link": true }, + "node_modules/@ai-devkit/channel-connector": { + "resolved": "packages/channel-connector", + "link": true + }, "node_modules/@ai-devkit/memory": { "resolved": "packages/memory", "link": true @@ -2091,9 +2096,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.12", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.12.tgz", - "integrity": "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw==", + "version": "1.19.13", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", + "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -3291,13 +3296,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@ltd/j-toml": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@ltd/j-toml/-/j-toml-1.38.0.tgz", - "integrity": "sha512-lYtBcmvHustHQtg4X7TXUu1Xa/tbLC3p2wLvgQI+fWVySguVZJF60Snxijw5EiohumxZbR10kWYFFebh1zotiw==", - "dev": true, - "license": "LGPL-3.0" - }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", @@ -3734,14 +3732,14 @@ } }, "node_modules/@nx/devkit": { - "version": "22.6.3", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-22.6.3.tgz", - "integrity": "sha512-GUGQGU1XcNHLQcUEq/JqNqTGikfdJQAgiyauwKr5z2dUNWK+OmUJE9J0tqANbPBZO5wtwMpRNXtVWtxQqgX8nQ==", + "version": "22.6.5", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-22.6.5.tgz", + "integrity": "sha512-9kvAI+kk2pfEXLqS8OyjI9XvWmp+Gdn7jPfxDAz8BOqxMyPy3p5hYl+jc4TIsLOWunAFl8azqrcYsHzEpaWCIA==", "dev": true, "license": "MIT", "dependencies": { "@zkochan/js-yaml": "0.0.7", - "ejs": "^3.1.7", + "ejs": "5.0.1", "enquirer": "~2.3.6", "minimatch": "10.2.4", "semver": "^7.6.3", @@ -3792,9 +3790,9 @@ } }, "node_modules/@nx/js": { - "version": "22.6.3", - "resolved": "https://registry.npmjs.org/@nx/js/-/js-22.6.3.tgz", - "integrity": "sha512-KKgN6TydaadkZFRu3XuOkC3aV+49pDzfiOad93nkHMXScevnbGFdyhqXXxc9FqaKW3ocrsNc6T7I4MjQ7ZUNaw==", + "version": "22.6.5", + "resolved": "https://registry.npmjs.org/@nx/js/-/js-22.6.5.tgz", + "integrity": "sha512-bmikz6qaBHfuAgsqPB/TfLIKfvI4g+EKIRAiU2FHnEtVWOKDAmSQXHFwE3rMS49jl2JLgxkdNjZHpg4g/OLy0g==", "dev": true, "license": "MIT", "dependencies": { @@ -3805,8 +3803,8 @@ "@babel/preset-env": "^7.23.2", "@babel/preset-typescript": "^7.22.5", "@babel/runtime": "^7.22.6", - "@nx/devkit": "22.6.3", - "@nx/workspace": "22.6.3", + "@nx/devkit": "22.6.5", + "@nx/workspace": "22.6.5", "@zkochan/js-yaml": "0.0.7", "babel-plugin-const-enum": "^1.0.1", "babel-plugin-macros": "^3.1.0", @@ -3819,7 +3817,7 @@ "jsonc-parser": "3.2.0", "npm-run-path": "^4.0.1", "picocolors": "^1.1.0", - "picomatch": "4.0.2", + "picomatch": "4.0.4", "semver": "^7.6.3", "source-map-support": "0.5.19", "tinyglobby": "^0.2.12", @@ -3835,9 +3833,9 @@ } }, "node_modules/@nx/js/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -3859,9 +3857,9 @@ } }, "node_modules/@nx/nx-darwin-arm64": { - "version": "22.6.3", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-22.6.3.tgz", - "integrity": "sha512-m8hEp2WufqUJzrl2uI5OItkPqIo8+0lbOBEKI7yZN9uoL6FKzP5LF6WlMFPJ8FlajtjBzQqaoDwp04+bkuXeaw==", + "version": "22.6.5", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-22.6.5.tgz", + "integrity": "sha512-qT77Omkg5xQuL2+pDbneX2tI+XW5ZeayMylu7UUgK8OhTrAkJLKjpuYRH4xT5XBipxbDtlxmO3aLS3Ib1pKzJQ==", "cpu": [ "arm64" ], @@ -3873,9 +3871,9 @@ ] }, "node_modules/@nx/nx-darwin-x64": { - "version": "22.6.3", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-22.6.3.tgz", - "integrity": "sha512-biPybnU2qlNuP7ytBYmRuusrU5TWXqVKMHr7Kxrqlin87iJR5MosXSZ+Pjr8H+0zFrB4rGf/9yro3s/dYG40Yw==", + "version": "22.6.5", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-22.6.5.tgz", + "integrity": "sha512-9jICxb7vfJ56y/7Yuh3b/n1QJqWxO9xnXKYEs6SO8xPoW/KomVckILGc1C6RQSs6/3ixVJC7k1Dh1wm5tKPFrg==", "cpu": [ "x64" ], @@ -3887,9 +3885,9 @@ ] }, "node_modules/@nx/nx-freebsd-x64": { - "version": "22.6.3", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.6.3.tgz", - "integrity": "sha512-8C6hhvVuqPwnvjHMPAA77DeEZ/WSY6AxuuIiyRje9uKF2B5F26sV89lRjBoEiWnV1dmLdy5YY5HJZEjwqjifAQ==", + "version": "22.6.5", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.6.5.tgz", + "integrity": "sha512-6B1wEKpqz5dI3AGMqttAVnA6M3DB/besAtuGyQiymK9ROlta1iuWgCcIYwcCQyhLn2Rx7vqj447KKcgCa8HlVw==", "cpu": [ "x64" ], @@ -3901,9 +3899,9 @@ ] }, "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "22.6.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.6.3.tgz", - "integrity": "sha512-8gWDhe4lY3pegmKx5/z7z/h4adlmL+3wuPXMUlBtMkhJ5TX1z94PkVtHRprEsHuQHO7PsSFaOJdsIZbr/sx7SQ==", + "version": "22.6.5", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.6.5.tgz", + "integrity": "sha512-xV50B8mnDPboct7JkAHftajI02s+8FszA8WTzhore+YGR+lEKHTLpucwGEaQuMlSdLplH7pQix4B4uK5pcMhZw==", "cpu": [ "arm" ], @@ -3915,9 +3913,9 @@ ] }, "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "22.6.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.6.3.tgz", - "integrity": "sha512-ZRP5qf4lsk0HFuvhhSJc+t3a0NKc+WXElKPXTEK9DGOluY327lUogeZrSSJfxGf+dBTtpuRIO8rOIrnZOf5Xww==", + "version": "22.6.5", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.6.5.tgz", + "integrity": "sha512-2JkWuMGj+HpW6oPAvU5VdAx1afTnEbiM10Y3YOrl3fipWV4BiP5VDx762QTrfCraP4hl6yqTgvTe7F9xaby+jQ==", "cpu": [ "arm64" ], @@ -3929,9 +3927,9 @@ ] }, "node_modules/@nx/nx-linux-arm64-musl": { - "version": "22.6.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.6.3.tgz", - "integrity": "sha512-AcOf/5UJD7Fyc2ujHYajxLw+ajJ8C1IhHoCQyLwBpd/15lu3pii9Z9G4cNBm0ejKnnzofzRmhv2xka9qqCtpXQ==", + "version": "22.6.5", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.6.5.tgz", + "integrity": "sha512-Z/zMqFClnEyqDXouJKEPoWVhMQIif5F0YuECWBYjd3ZLwQsXGTItoh+6Wm3XF/nGMA2uLOHyTq/X7iFXQY3RzA==", "cpu": [ "arm64" ], @@ -3943,9 +3941,9 @@ ] }, "node_modules/@nx/nx-linux-x64-gnu": { - "version": "22.6.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.6.3.tgz", - "integrity": "sha512-KxSdUCGOt2GGXzgggp9sSLJacWj7AAI410UPOEGw5F6GS5148e+kiy3piULF/0NE5/q40IK7gyS43HY99qgAqQ==", + "version": "22.6.5", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.6.5.tgz", + "integrity": "sha512-FlotSyqNnaXSn0K+yWw+hRdYBwusABrPgKLyixfJIYRzsy+xPKN6pON6vZfqGwzuWF/9mEGReRz+iM8PiW0XSg==", "cpu": [ "x64" ], @@ -3957,9 +3955,9 @@ ] }, "node_modules/@nx/nx-linux-x64-musl": { - "version": "22.6.3", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.6.3.tgz", - "integrity": "sha512-Tvlw6XvTj+5IQRkprV3AdCKnlQFYh2OJYn0wgHrvQWeV1Eks/RaCoRChfHXdAyE4S64YrBA6NAOxfXANh3yLTg==", + "version": "22.6.5", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.6.5.tgz", + "integrity": "sha512-RVOe2qcwhoIx6mxQURPjUfAW5SEOmT2gdhewvdcvX9ICq1hj5B2VarmkhTg0qroO7xiyqOqwq26mCzoV2I3NgQ==", "cpu": [ "x64" ], @@ -3971,9 +3969,9 @@ ] }, "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "22.6.3", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.6.3.tgz", - "integrity": "sha512-9yRRuoVeQdV52GJtHo+vH6+es2PNF8skWlUa74jyWRsoZM9Ew8JmRZruRfhkUmhjJTrguqJLj9koa/NXgS0yeg==", + "version": "22.6.5", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.6.5.tgz", + "integrity": "sha512-ZqurqI8VuYnsr2Kn4K4t+Gx6j/BZdf6qz/6Tv4A7XQQ6oNYVQgTqoNEFj+CCkVaIe6aIdCWpousFLqs+ZgBqYQ==", "cpu": [ "arm64" ], @@ -3985,9 +3983,9 @@ ] }, "node_modules/@nx/nx-win32-x64-msvc": { - "version": "22.6.3", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.6.3.tgz", - "integrity": "sha512-21wjiUSV5hMa1oj8UfpfMTxpROksWrr/minAv8ejmGFwUSoztSzAkNf5i4PESPsbYNytjKooDzzAiQMLo6b0kg==", + "version": "22.6.5", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.6.5.tgz", + "integrity": "sha512-i2QFBJIuaYg9BHxrrnBV4O7W9rVL2k0pSIdk/rRp3EYJEU93iUng+qbZiY9wh1xvmXuUCE2G7TRd+8/SG/RFKg==", "cpu": [ "x64" ], @@ -3999,27 +3997,27 @@ ] }, "node_modules/@nx/workspace": { - "version": "22.6.3", - "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-22.6.3.tgz", - "integrity": "sha512-Tdu3K6mpRAU0LI2gpi61WLY7mPR61nj1Szvhwj4T88PhGQXSlN6PuUhb5nMPHnZG/bDmUFeTSv/+MCgbEm563A==", + "version": "22.6.5", + "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-22.6.5.tgz", + "integrity": "sha512-/CZtv1ESSfZ1MVqSlCsmnBWysU1z5VdNlwANlqL6BV2X6RUHKDPVj4YuNPvCK+0LsqyzfJdUt3pcnBYxnT5TIg==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "22.6.3", + "@nx/devkit": "22.6.5", "@zkochan/js-yaml": "0.0.7", "chalk": "^4.1.0", "enquirer": "~2.3.6", - "nx": "22.6.3", - "picomatch": "4.0.2", + "nx": "22.6.5", + "picomatch": "4.0.4", "semver": "^7.6.3", "tslib": "^2.3.0", "yargs-parser": "21.1.1" } }, "node_modules/@nx/workspace/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -4381,6 +4379,12 @@ "node": ">=14.16" } }, + "node_modules/@telegraf/types": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@telegraf/types/-/types-7.1.0.tgz", + "integrity": "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==", + "license": "MIT" + }, "node_modules/@tokenizer/inflate": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", @@ -5117,6 +5121,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -5356,13 +5372,6 @@ "node": ">=8" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5371,9 +5380,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", - "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5878,6 +5887,22 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "license": "MIT" + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -5888,6 +5913,12 @@ "node": "*" } }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "license": "MIT" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6688,19 +6719,16 @@ "license": "MIT" }, "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.1.tgz", + "integrity": "sha512-COqBPFMxuPTPspXl2DkVYaDS3HtrD1GpzOGkNTJ1IYkifq/r9h8SVEFrjA3D9/VJGOEoMQcrlhpntcSUrM8k6A==", "dev": true, "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, "bin": { "ejs": "bin/cli.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.12.18" } }, "node_modules/electron-to-chromium": { @@ -7042,6 +7070,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/events-universal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", @@ -7395,29 +7432,6 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, - "node_modules/filelist": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/filename-reserved-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", @@ -8062,9 +8076,9 @@ } }, "node_modules/hono": { - "version": "4.12.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", - "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", + "version": "4.12.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", + "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -8128,6 +8142,22 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", @@ -8562,24 +8592,6 @@ "node": ">=8" } }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -10160,9 +10172,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.debounce": { @@ -10396,6 +10408,15 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10449,6 +10470,26 @@ "node": ">=10" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -10500,25 +10541,24 @@ } }, "node_modules/nx": { - "version": "22.6.3", - "resolved": "https://registry.npmjs.org/nx/-/nx-22.6.3.tgz", - "integrity": "sha512-8eIkEAlvkTvR2zY+yjhuTxMD6z4AtM1SumSBbwMmUMEXMtXE88fH0RL59T5V6MLjaov1exUM3lhUqPE3IyuBPg==", + "version": "22.6.5", + "resolved": "https://registry.npmjs.org/nx/-/nx-22.6.5.tgz", + "integrity": "sha512-VRKhDAt684dXNSz9MNjE7MekkCfQF41P2PSx5jEWQjDEP1Z4jFZbyeygWs5ZyOroG7/n0MoWAJTe6ftvIcBOAg==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "@ltd/j-toml": "^1.38.0", "@napi-rs/wasm-runtime": "0.2.4", "@yarnpkg/lockfile": "^1.1.0", "@yarnpkg/parsers": "3.0.2", "@zkochan/js-yaml": "0.0.7", - "axios": "^1.12.0", + "axios": "1.15.0", "cli-cursor": "3.1.0", "cli-spinners": "2.6.1", "cliui": "^8.0.1", "dotenv": "~16.4.5", "dotenv-expand": "~11.0.6", - "ejs": "^3.1.7", + "ejs": "5.0.1", "enquirer": "~2.3.6", "figures": "3.2.0", "flat": "^5.0.2", @@ -10534,6 +10574,7 @@ "picocolors": "^1.1.0", "resolve.exports": "2.0.3", "semver": "^7.6.3", + "smol-toml": "1.6.1", "string-width": "^4.2.3", "tar-stream": "~2.2.0", "tmp": "~0.2.1", @@ -10549,16 +10590,16 @@ "nx-cloud": "bin/nx-cloud.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "22.6.3", - "@nx/nx-darwin-x64": "22.6.3", - "@nx/nx-freebsd-x64": "22.6.3", - "@nx/nx-linux-arm-gnueabihf": "22.6.3", - "@nx/nx-linux-arm64-gnu": "22.6.3", - "@nx/nx-linux-arm64-musl": "22.6.3", - "@nx/nx-linux-x64-gnu": "22.6.3", - "@nx/nx-linux-x64-musl": "22.6.3", - "@nx/nx-win32-arm64-msvc": "22.6.3", - "@nx/nx-win32-x64-msvc": "22.6.3" + "@nx/nx-darwin-arm64": "22.6.5", + "@nx/nx-darwin-x64": "22.6.5", + "@nx/nx-freebsd-x64": "22.6.5", + "@nx/nx-linux-arm-gnueabihf": "22.6.5", + "@nx/nx-linux-arm64-gnu": "22.6.5", + "@nx/nx-linux-arm64-musl": "22.6.5", + "@nx/nx-linux-x64-gnu": "22.6.5", + "@nx/nx-linux-x64-musl": "22.6.5", + "@nx/nx-win32-arm64-msvc": "22.6.5", + "@nx/nx-win32-x64-msvc": "22.6.5" }, "peerDependencies": { "@swc-node/register": "^1.11.1", @@ -10826,6 +10867,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-timeout": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-4.1.0.tgz", + "integrity": "sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -11630,12 +11680,30 @@ ], "license": "MIT" }, + "node_modules/safe-compare": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-compare/-/safe-compare-1.1.4.tgz", + "integrity": "sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==", + "license": "MIT", + "dependencies": { + "buffer-alloc": "^1.2.0" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sandwich-stream": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sandwich-stream/-/sandwich-stream-2.0.2.tgz", + "integrity": "sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/section-matter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", @@ -11926,6 +11994,19 @@ "node": ">=8" } }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -12195,6 +12276,28 @@ "node": ">=6" } }, + "node_modules/telegraf": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.16.3.tgz", + "integrity": "sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==", + "license": "MIT", + "dependencies": { + "@telegraf/types": "^7.1.0", + "abort-controller": "^3.0.0", + "debug": "^4.3.4", + "mri": "^1.2.0", + "node-fetch": "^2.7.0", + "p-timeout": "^4.1.0", + "safe-compare": "^1.1.4", + "sandwich-stream": "^2.0.2" + }, + "bin": { + "telegraf": "lib/cli.mjs" + }, + "engines": { + "node": "^12.20.0 || >=14.13.1" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -12363,6 +12466,12 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -12831,6 +12940,22 @@ "defaults": "^1.0.3" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -13031,12 +13156,34 @@ "node": ">=20.20.0" } }, + "packages/channel-connector": { + "name": "@ai-devkit/channel-connector", + "version": "0.2.0", + "license": "MIT", + "dependencies": { + "telegraf": "^4.16.3" + }, + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "^20.11.5", + "@typescript-eslint/eslint-plugin": "^6.19.1", + "@typescript-eslint/parser": "^6.19.1", + "eslint": "^8.56.0", + "jest": "^29.7.0", + "ts-jest": "^29.4.5", + "typescript": "^5.3.3" + }, + "engines": { + "node": ">=20.20.0" + } + }, "packages/cli": { "name": "ai-devkit", - "version": "0.21.1", + "version": "0.22.0", "license": "MIT", "dependencies": { "@ai-devkit/agent-manager": "0.7.0", + "@ai-devkit/channel-connector": "0.2.0", "@ai-devkit/memory": "0.8.0", "chalk": "^4.1.2", "commander": "^11.1.0", diff --git a/package.json b/package.json index 549aeb68..7ce5cf0a 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "ai-devkit", - "version": "0.21.0", + "version": "0.21.1", "private": true, "description": "A CLI toolkit for AI-assisted software development with phase templates and environment setup", "scripts": { "nx": "nx", + "prepare": "husky", "build": "nx run-many -t build", "lint": "nx run-many -t lint", "test": "nx run-many -t test", @@ -28,6 +29,7 @@ "license": "MIT", "devDependencies": { "@nx/js": "^22.4.0", + "husky": "^9.1.7", "nx": "^22.4.0" }, "engines": { diff --git a/packages/channel-connector/.eslintrc.json b/packages/channel-connector/.eslintrc.json new file mode 100644 index 00000000..ddc47aaf --- /dev/null +++ b/packages/channel-connector/.eslintrc.json @@ -0,0 +1,31 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "plugins": ["@typescript-eslint"], + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "env": { + "node": true, + "es6": true, + "jest": true + }, + "rules": { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-var-requires": "error" + }, + "overrides": [ + { + "files": ["**/__tests__/**/*.ts", "**/*.test.ts", "**/*.spec.ts"], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off" + } + } + ] +} diff --git a/packages/channel-connector/.npmignore b/packages/channel-connector/.npmignore new file mode 100644 index 00000000..673c1503 --- /dev/null +++ b/packages/channel-connector/.npmignore @@ -0,0 +1,22 @@ +src/ +tsconfig.json +.eslintrc.json +*.test.ts +*.spec.ts +.DS_Store +node_modules/ +.git/ +.vscode/ +.idea/ +*.log +ai.plan.md +docs/ +.ai-devkit.json +.cursor/ +.github/ +.agent/ +.claude/ +.gemini/ +AGENTS.md +jest.config.js +project.json diff --git a/packages/channel-connector/jest.config.js b/packages/channel-connector/jest.config.js new file mode 100644 index 00000000..6e5e0508 --- /dev/null +++ b/packages/channel-connector/jest.config.js @@ -0,0 +1,21 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + testMatch: ['**/__tests__/**/*.test.ts', '**/?(*.)+(spec|test).ts'], + collectCoverageFrom: [ + 'src/**/*.{ts,js}', + '!src/**/*.d.ts', + '!src/index.ts' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + coverageThreshold: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80 + } + } +}; diff --git a/packages/channel-connector/package.json b/packages/channel-connector/package.json new file mode 100644 index 00000000..9e271c7e --- /dev/null +++ b/packages/channel-connector/package.json @@ -0,0 +1,46 @@ +{ + "name": "@ai-devkit/channel-connector", + "version": "0.2.0", + "description": "Generic messaging bridge for connecting to external communication platforms", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "test": "jest", + "test:coverage": "jest --coverage", + "lint": "eslint src --ext .ts", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist" + }, + "keywords": [ + "ai", + "channel", + "connector", + "telegram", + "messaging" + ], + "author": "", + "license": "MIT", + "dependencies": { + "telegraf": "^4.16.3" + }, + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "^20.11.5", + "@typescript-eslint/eslint-plugin": "^6.19.1", + "@typescript-eslint/parser": "^6.19.1", + "eslint": "^8.56.0", + "jest": "^29.7.0", + "ts-jest": "^29.4.5", + "typescript": "^5.3.3" + }, + "engines": { + "node": ">=20.20.0" + } +} diff --git a/packages/channel-connector/src/ChannelManager.ts b/packages/channel-connector/src/ChannelManager.ts new file mode 100644 index 00000000..4667e68d --- /dev/null +++ b/packages/channel-connector/src/ChannelManager.ts @@ -0,0 +1,43 @@ +import type { ChannelAdapter } from './adapters/ChannelAdapter'; + +/** + * Central registry for channel adapters. + * Manages adapter lifecycle (start/stop). + */ +export class ChannelManager { + private adapters: Map = new Map(); + + /** + * Register a channel adapter. + * @throws If an adapter for the same type is already registered. + */ + registerAdapter(adapter: ChannelAdapter): void { + if (this.adapters.has(adapter.type)) { + throw new Error(`Adapter for type "${adapter.type}" is already registered`); + } + this.adapters.set(adapter.type, adapter); + } + + /** + * Get a registered adapter by type. + */ + getAdapter(type: string): ChannelAdapter | undefined { + return this.adapters.get(type); + } + + /** + * Start all registered adapters. + */ + async startAll(): Promise { + const startPromises = Array.from(this.adapters.values()).map(a => a.start()); + await Promise.all(startPromises); + } + + /** + * Stop all registered adapters. + */ + async stopAll(): Promise { + const stopPromises = Array.from(this.adapters.values()).map(a => a.stop()); + await Promise.all(stopPromises); + } +} diff --git a/packages/channel-connector/src/ConfigStore.ts b/packages/channel-connector/src/ConfigStore.ts new file mode 100644 index 00000000..f79a7afe --- /dev/null +++ b/packages/channel-connector/src/ConfigStore.ts @@ -0,0 +1,64 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import type { ChannelConfig, ChannelEntry } from './types'; + +const DEFAULT_CONFIG_PATH = path.join(os.homedir(), '.ai-devkit', 'channels.json'); +const DEFAULT_CONFIG: ChannelConfig = { channels: {} }; + +/** + * Persists channel configurations to disk. + * Default location: ~/.ai-devkit/channels.json + * File permissions are set to 0600 to protect tokens. + */ +export class ConfigStore { + private configPath: string; + + constructor(configPath?: string) { + this.configPath = configPath ?? DEFAULT_CONFIG_PATH; + } + + /** + * Read the full config. Returns default empty config if file is missing or corrupt. + */ + async getConfig(): Promise { + try { + const raw = fs.readFileSync(this.configPath, 'utf-8'); + return JSON.parse(raw) as ChannelConfig; + } catch { + return { ...DEFAULT_CONFIG, channels: {} }; + } + } + + /** + * Save a channel entry. Creates the file and parent directory if needed. + */ + async saveChannel(name: string, entry: ChannelEntry): Promise { + const config = await this.getConfig(); + config.channels[name] = entry; + await this.writeConfig(config); + } + + /** + * Remove a channel entry by name. + */ + async removeChannel(name: string): Promise { + const config = await this.getConfig(); + delete config.channels[name]; + await this.writeConfig(config); + } + + /** + * Get a single channel entry by name. + */ + async getChannel(name: string): Promise { + const config = await this.getConfig(); + return config.channels[name]; + } + + private async writeConfig(config: ChannelConfig): Promise { + const dir = path.dirname(this.configPath); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), { mode: 0o600 }); + } +} diff --git a/packages/channel-connector/src/__tests__/ChannelManager.test.ts b/packages/channel-connector/src/__tests__/ChannelManager.test.ts new file mode 100644 index 00000000..abd6bac9 --- /dev/null +++ b/packages/channel-connector/src/__tests__/ChannelManager.test.ts @@ -0,0 +1,86 @@ +import { ChannelManager } from '../ChannelManager'; +import type { ChannelAdapter } from '../adapters/ChannelAdapter'; + +function createMockAdapter(type: string): jest.Mocked { + return { + type, + start: jest.fn().mockResolvedValue(undefined), + stop: jest.fn().mockResolvedValue(undefined), + sendMessage: jest.fn().mockResolvedValue(undefined), + onMessage: jest.fn(), + isHealthy: jest.fn().mockResolvedValue(true), + }; +} + +describe('ChannelManager', () => { + let manager: ChannelManager; + + beforeEach(() => { + manager = new ChannelManager(); + }); + + describe('registerAdapter', () => { + it('should register an adapter', () => { + const adapter = createMockAdapter('telegram'); + manager.registerAdapter(adapter); + expect(manager.getAdapter('telegram')).toBe(adapter); + }); + + it('should throw on duplicate adapter type', () => { + const adapter1 = createMockAdapter('telegram'); + const adapter2 = createMockAdapter('telegram'); + manager.registerAdapter(adapter1); + expect(() => manager.registerAdapter(adapter2)).toThrow( + 'Adapter for type "telegram" is already registered' + ); + }); + }); + + describe('getAdapter', () => { + it('should return undefined for unregistered type', () => { + expect(manager.getAdapter('slack')).toBeUndefined(); + }); + + it('should return the registered adapter', () => { + const adapter = createMockAdapter('telegram'); + manager.registerAdapter(adapter); + expect(manager.getAdapter('telegram')).toBe(adapter); + }); + }); + + describe('startAll', () => { + it('should call start() on all registered adapters', async () => { + const telegram = createMockAdapter('telegram'); + const slack = createMockAdapter('slack'); + manager.registerAdapter(telegram); + manager.registerAdapter(slack); + + await manager.startAll(); + + expect(telegram.start).toHaveBeenCalledTimes(1); + expect(slack.start).toHaveBeenCalledTimes(1); + }); + + it('should work with no adapters', async () => { + await expect(manager.startAll()).resolves.toBeUndefined(); + }); + }); + + describe('stopAll', () => { + it('should call stop() on all registered adapters', async () => { + const telegram = createMockAdapter('telegram'); + const slack = createMockAdapter('slack'); + manager.registerAdapter(telegram); + manager.registerAdapter(slack); + + await manager.stopAll(); + + expect(telegram.stop).toHaveBeenCalledTimes(1); + expect(slack.stop).toHaveBeenCalledTimes(1); + }); + + it('should work with no adapters', async () => { + await expect(manager.stopAll()).resolves.toBeUndefined(); + }); + }); +}); diff --git a/packages/channel-connector/src/__tests__/ConfigStore.test.ts b/packages/channel-connector/src/__tests__/ConfigStore.test.ts new file mode 100644 index 00000000..744408e6 --- /dev/null +++ b/packages/channel-connector/src/__tests__/ConfigStore.test.ts @@ -0,0 +1,126 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { ConfigStore } from '../ConfigStore'; +import type { ChannelEntry } from '../types'; + +describe('ConfigStore', () => { + let tmpDir: string; + let configPath: string; + let store: ConfigStore; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'channel-connector-test-')); + configPath = path.join(tmpDir, 'channels.json'); + store = new ConfigStore(configPath); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + const sampleEntry: ChannelEntry = { + type: 'telegram', + enabled: true, + createdAt: '2026-04-11T00:00:00Z', + config: { + botToken: 'test-token-123', + botUsername: 'test_bot', + }, + }; + + describe('constructor', () => { + it('should use default path when no configPath provided', async () => { + const defaultStore = new ConfigStore(); + // Should not throw — just uses default path + const config = await defaultStore.getConfig(); + expect(config).toBeDefined(); + }); + }); + + describe('getConfig', () => { + it('should return default empty config when file does not exist', async () => { + const config = await store.getConfig(); + expect(config).toEqual({ channels: {} }); + }); + + it('should return parsed config when file exists', async () => { + fs.writeFileSync(configPath, JSON.stringify({ + channels: { telegram: sampleEntry } + })); + + const config = await store.getConfig(); + expect(config.channels.telegram).toEqual(sampleEntry); + }); + + it('should handle corrupted JSON gracefully', async () => { + fs.writeFileSync(configPath, 'not valid json{{{'); + + const config = await store.getConfig(); + expect(config).toEqual({ channels: {} }); + }); + }); + + describe('saveChannel', () => { + it('should create config file with channel entry', async () => { + await store.saveChannel('telegram', sampleEntry); + + const raw = fs.readFileSync(configPath, 'utf-8'); + const config = JSON.parse(raw); + expect(config.channels.telegram).toEqual(sampleEntry); + }); + + it('should create parent directory if missing', async () => { + const nestedPath = path.join(tmpDir, 'nested', 'dir', 'channels.json'); + const nestedStore = new ConfigStore(nestedPath); + + await nestedStore.saveChannel('telegram', sampleEntry); + + expect(fs.existsSync(nestedPath)).toBe(true); + }); + + it('should set file permissions to 0600', async () => { + await store.saveChannel('telegram', sampleEntry); + + const stats = fs.statSync(configPath); + const mode = (stats.mode & 0o777).toString(8); + expect(mode).toBe('600'); + }); + + it('should preserve existing channels when adding a new one', async () => { + await store.saveChannel('telegram', sampleEntry); + await store.saveChannel('slack', { ...sampleEntry, type: 'slack' }); + + const config = await store.getConfig(); + expect(Object.keys(config.channels)).toEqual(['telegram', 'slack']); + }); + }); + + describe('removeChannel', () => { + it('should remove a channel entry', async () => { + await store.saveChannel('telegram', sampleEntry); + await store.removeChannel('telegram'); + + const config = await store.getConfig(); + expect(config.channels.telegram).toBeUndefined(); + }); + + it('should not throw when removing non-existent channel', async () => { + await expect(store.removeChannel('nonexistent')).resolves.toBeUndefined(); + }); + }); + + describe('getChannel', () => { + it('should return the channel entry', async () => { + await store.saveChannel('telegram', sampleEntry); + + const entry = await store.getChannel('telegram'); + expect(entry).toEqual(sampleEntry); + }); + + it('should return undefined for non-existent channel', async () => { + const entry = await store.getChannel('slack'); + expect(entry).toBeUndefined(); + }); + }); +}); diff --git a/packages/channel-connector/src/__tests__/adapters/TelegramAdapter.test.ts b/packages/channel-connector/src/__tests__/adapters/TelegramAdapter.test.ts new file mode 100644 index 00000000..90cfd059 --- /dev/null +++ b/packages/channel-connector/src/__tests__/adapters/TelegramAdapter.test.ts @@ -0,0 +1,192 @@ +import { TelegramAdapter } from '../../adapters/TelegramAdapter'; +import type { IncomingMessage } from '../../types'; + +// Mock telegraf +jest.mock('telegraf', () => { + const handlers: Record any> = {}; + const mockBot = { + launch: jest.fn().mockResolvedValue(undefined), + stop: jest.fn().mockResolvedValue(undefined), + on: jest.fn((event: string, handler: (...args: any[]) => any) => { + handlers[event] = handler; + }), + telegram: { + sendMessage: jest.fn().mockResolvedValue(undefined), + getMe: jest.fn().mockResolvedValue({ username: 'test_bot' }), + }, + _handlers: handlers, + _triggerText: async (chatId: number, userId: number, text: string) => { + const ctx = { + message: { + chat: { id: chatId }, + from: { id: userId }, + text, + date: Math.floor(Date.now() / 1000), + }, + reply: jest.fn().mockResolvedValue(undefined), + }; + if (handlers['text']) { + await handlers['text'](ctx); + } + return ctx; + }, + }; + return { + Telegraf: jest.fn(() => mockBot), + __mockBot: mockBot, + }; +}); + +function getMockBot() { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('telegraf').__mockBot; +} + +describe('TelegramAdapter', () => { + let adapter: TelegramAdapter; + + beforeEach(() => { + jest.clearAllMocks(); + adapter = new TelegramAdapter({ botToken: 'test-token-123' }); + }); + + describe('type', () => { + it('should return "telegram"', () => { + expect(adapter.type).toBe('telegram'); + }); + }); + + describe('start', () => { + it('should launch the telegraf bot', async () => { + const bot = getMockBot(); + await adapter.start(); + expect(bot.launch).toHaveBeenCalled(); + }); + }); + + describe('stop', () => { + it('should stop the telegraf bot', async () => { + const bot = getMockBot(); + await adapter.start(); + await adapter.stop(); + expect(bot.stop).toHaveBeenCalled(); + }); + }); + + describe('onMessage', () => { + it('should silently ignore messages when no handler is registered', async () => { + // Don't register a handler + await adapter.start(); + + const bot = getMockBot(); + const ctx = await bot._triggerText(12345, 67890, 'hello'); + + // Should not throw or reply + expect(ctx.reply).not.toHaveBeenCalled(); + }); + + it('should handle non-Error thrown by handler', async () => { + const handler = jest.fn().mockRejectedValue('string error'); + adapter.onMessage(handler); + await adapter.start(); + + const bot = getMockBot(); + const ctx = await bot._triggerText(12345, 67890, 'hello'); + + expect(ctx.reply).toHaveBeenCalledWith( + 'Error processing message: Unknown error' + ); + }); + + it('should call handler with IncomingMessage on incoming text', async () => { + const handler = jest.fn().mockResolvedValue(undefined); + adapter.onMessage(handler); + await adapter.start(); + + const bot = getMockBot(); + await bot._triggerText(12345, 67890, 'hello agent'); + + expect(handler).toHaveBeenCalledTimes(1); + const msg: IncomingMessage = handler.mock.calls[0][0]; + expect(msg.channelType).toBe('telegram'); + expect(msg.chatId).toBe('12345'); + expect(msg.userId).toBe('67890'); + expect(msg.text).toBe('hello agent'); + expect(msg.timestamp).toBeInstanceOf(Date); + }); + + it('should handle handler errors gracefully', async () => { + const handler = jest.fn().mockRejectedValue(new Error('handler failed')); + adapter.onMessage(handler); + await adapter.start(); + + const bot = getMockBot(); + const ctx = await bot._triggerText(12345, 67890, 'hello'); + + // Should not throw, and should reply with error + expect(ctx.reply).toHaveBeenCalledWith( + expect.stringContaining('Error processing message') + ); + }); + }); + + describe('sendMessage', () => { + it('should send text to the specified chat', async () => { + const bot = getMockBot(); + await adapter.sendMessage('12345', 'hello from bot'); + + expect(bot.telegram.sendMessage).toHaveBeenCalledWith('12345', 'hello from bot'); + }); + + it('should chunk messages exceeding 4096 chars at newline boundaries', async () => { + const bot = getMockBot(); + // Create a message with lines that total > 4096 chars + const line = 'A'.repeat(100) + '\n'; + const longMessage = line.repeat(50); // 50 * 101 = 5050 chars + + await adapter.sendMessage('12345', longMessage); + + // Should have been called multiple times (chunked) + expect(bot.telegram.sendMessage.mock.calls.length).toBeGreaterThan(1); + // Each chunk should be <= 4096 chars + for (const call of bot.telegram.sendMessage.mock.calls) { + expect(call[1].length).toBeLessThanOrEqual(4096); + } + }); + + it('should hard split at 4096 when no newlines available', async () => { + const bot = getMockBot(); + const longMessage = 'A'.repeat(5000); + + await adapter.sendMessage('12345', longMessage); + + expect(bot.telegram.sendMessage.mock.calls.length).toBe(2); + expect(bot.telegram.sendMessage.mock.calls[0][1].length).toBe(4096); + expect(bot.telegram.sendMessage.mock.calls[1][1].length).toBe(904); + }); + + it('should send short messages in a single call', async () => { + const bot = getMockBot(); + await adapter.sendMessage('12345', 'short message'); + + expect(bot.telegram.sendMessage).toHaveBeenCalledTimes(1); + }); + }); + + describe('isHealthy', () => { + it('should return true after start', async () => { + await adapter.start(); + expect(await adapter.isHealthy()).toBe(true); + }); + + it('should return false before start', async () => { + expect(await adapter.isHealthy()).toBe(false); + }); + + it('should return false after stop', async () => { + await adapter.start(); + await adapter.stop(); + expect(await adapter.isHealthy()).toBe(false); + }); + }); +}); diff --git a/packages/channel-connector/src/adapters/ChannelAdapter.ts b/packages/channel-connector/src/adapters/ChannelAdapter.ts new file mode 100644 index 00000000..a19a3351 --- /dev/null +++ b/packages/channel-connector/src/adapters/ChannelAdapter.ts @@ -0,0 +1,35 @@ +import type { IncomingMessage } from '../types'; + +/** + * Interface for messaging platform adapters. + * + * Implementations connect to a specific platform (Telegram, Slack, etc.) + * and provide a generic send/receive abstraction. + */ +export interface ChannelAdapter { + /** Identifier for this channel type (e.g., 'telegram') */ + readonly type: string; + + /** Start listening for incoming messages */ + start(): Promise; + + /** Stop listening and clean up resources */ + stop(): Promise; + + /** + * Send a message to a specific chat. + * Implementations should handle platform-specific limits + * (e.g., chunking at 4096 chars for Telegram). + */ + sendMessage(chatId: string, text: string): Promise; + + /** + * Register a handler for incoming text messages. + * Fire-and-forget — handler returns void. + * Responses are sent separately via sendMessage(). + */ + onMessage(handler: (msg: IncomingMessage) => Promise): void; + + /** Check if the adapter is connected and healthy */ + isHealthy(): Promise; +} diff --git a/packages/channel-connector/src/adapters/TelegramAdapter.ts b/packages/channel-connector/src/adapters/TelegramAdapter.ts new file mode 100644 index 00000000..9ad3e391 --- /dev/null +++ b/packages/channel-connector/src/adapters/TelegramAdapter.ts @@ -0,0 +1,110 @@ +import { Telegraf } from 'telegraf'; +import type { ChannelAdapter } from './ChannelAdapter'; +import type { IncomingMessage } from '../types'; + +export const TELEGRAM_CHANNEL_TYPE = 'telegram'; +export const TELEGRAM_MAX_MESSAGE_LENGTH = 4096; + +export interface TelegramAdapterOptions { + botToken: string; +} + +/** + * Telegram Bot API adapter using telegraf with long polling. + */ +export class TelegramAdapter implements ChannelAdapter { + readonly type = TELEGRAM_CHANNEL_TYPE; + + private bot: Telegraf; + private messageHandler: ((msg: IncomingMessage) => Promise) | null = null; + private running = false; + + constructor(options: TelegramAdapterOptions) { + this.bot = new Telegraf(options.botToken); + } + + async start(): Promise { + this.bot.on('text', async (ctx) => { + if (!this.messageHandler) return; + + const msg: IncomingMessage = { + channelType: TELEGRAM_CHANNEL_TYPE, + chatId: String(ctx.message.chat.id), + userId: String(ctx.message.from.id), + text: ctx.message.text, + timestamp: new Date(ctx.message.date * 1000), + }; + + try { + await this.messageHandler(msg); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + await ctx.reply(`Error processing message: ${errorMessage}`); + } + }); + + await this.bot.launch(); + this.running = true; + } + + async stop(): Promise { + this.running = false; + await this.bot.stop(); + } + + /** + * Send a message to a chat. Automatically chunks messages exceeding + * Telegram's 4096-char limit, preferring newline boundaries. + */ + async sendMessage(chatId: string, text: string): Promise { + const chunks = chunkMessage(text, TELEGRAM_MAX_MESSAGE_LENGTH); + for (const chunk of chunks) { + await this.bot.telegram.sendMessage(chatId, chunk); + } + } + + onMessage(handler: (msg: IncomingMessage) => Promise): void { + this.messageHandler = handler; + } + + async isHealthy(): Promise { + return this.running; + } +} + +/** + * Split text into chunks of maxLen or fewer characters, + * preferring to split at newline boundaries. + */ +function chunkMessage(text: string, maxLen: number): string[] { + if (text.length <= maxLen) { + return [text]; + } + + const chunks: string[] = []; + let remaining = text; + + while (remaining.length > 0) { + if (remaining.length <= maxLen) { + chunks.push(remaining); + break; + } + + // Find the last newline within the limit + const searchArea = remaining.slice(0, maxLen); + const lastNewline = searchArea.lastIndexOf('\n'); + + let splitAt: number; + if (lastNewline > 0) { + splitAt = lastNewline + 1; // include the newline in the current chunk + } else { + // No newline found — hard split at maxLen + splitAt = maxLen; + } + + chunks.push(remaining.slice(0, splitAt)); + remaining = remaining.slice(splitAt); + } + + return chunks; +} diff --git a/packages/channel-connector/src/index.ts b/packages/channel-connector/src/index.ts new file mode 100644 index 00000000..489feb20 --- /dev/null +++ b/packages/channel-connector/src/index.ts @@ -0,0 +1,15 @@ +export { ChannelManager } from './ChannelManager'; +export { ConfigStore } from './ConfigStore'; +export { TelegramAdapter, TELEGRAM_CHANNEL_TYPE, TELEGRAM_MAX_MESSAGE_LENGTH } from './adapters/TelegramAdapter'; +export type { TelegramAdapterOptions } from './adapters/TelegramAdapter'; + +export type { ChannelAdapter } from './adapters/ChannelAdapter'; + +export type { + IncomingMessage, + MessageHandler, + ChannelConfig, + ChannelEntry, + ChannelType, + TelegramConfig, +} from './types'; diff --git a/packages/channel-connector/src/types.ts b/packages/channel-connector/src/types.ts new file mode 100644 index 00000000..c05e5e79 --- /dev/null +++ b/packages/channel-connector/src/types.ts @@ -0,0 +1,49 @@ +/** + * An incoming message from a messaging platform. + * Generic — no agent-specific concepts. + */ +export interface IncomingMessage { + channelType: string; + chatId: string; + userId: string; + text: string; + timestamp: Date; + metadata?: Record; +} + +/** + * Handler function provided by the consumer (e.g., CLI). + * Fire-and-forget — returns void. Responses are sent separately via sendMessage(). + */ +export type MessageHandler = (message: IncomingMessage) => Promise; + +/** + * Root configuration for all channels. + */ +export interface ChannelConfig { + channels: Record; +} + +/** + * Configuration entry for a single channel. + */ +export interface ChannelEntry { + type: ChannelType; + enabled: boolean; + createdAt: string; + config: TelegramConfig; +} + +/** + * Supported channel types. + */ +export type ChannelType = 'telegram' | 'slack' | 'whatsapp'; + +/** + * Telegram-specific configuration. + */ +export interface TelegramConfig { + botToken: string; + botUsername: string; + authorizedChatId?: number; +} diff --git a/packages/channel-connector/tsconfig.json b/packages/channel-connector/tsconfig.json new file mode 100644 index 00000000..9f3762b6 --- /dev/null +++ b/packages/channel-connector/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node", + "rootDir": "./src", + "outDir": "./dist" + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "src/__tests__" + ] +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 9df15409..b1e158a3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "ai-devkit", - "version": "0.21.1", + "version": "0.22.0", "description": "A CLI toolkit for AI-assisted software development with phase templates and environment setup", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -28,6 +28,7 @@ "license": "MIT", "dependencies": { "@ai-devkit/agent-manager": "0.7.0", + "@ai-devkit/channel-connector": "0.2.0", "@ai-devkit/memory": "0.8.0", "chalk": "^4.1.2", "commander": "^11.1.0", diff --git a/packages/cli/src/__tests__/util/env.test.ts b/packages/cli/src/__tests__/util/env.test.ts index d6051d9b..5ba99830 100644 --- a/packages/cli/src/__tests__/util/env.test.ts +++ b/packages/cli/src/__tests__/util/env.test.ts @@ -357,7 +357,6 @@ describe('Environment Utilities', () => { expect(envCodes).not.toContain('gemini'); expect(envCodes).not.toContain('github'); expect(envCodes).not.toContain('kilocode'); - expect(envCodes).not.toContain('amp'); expect(envCodes).not.toContain('roo'); }); @@ -369,9 +368,10 @@ describe('Environment Utilities', () => { expect(envCodes).toContain('cursor'); expect(envCodes).toContain('claude'); expect(envCodes).toContain('codex'); + expect(envCodes).toContain('amp'); expect(envCodes).toContain('opencode'); expect(envCodes).toContain('antigravity'); - expect(skillEnvs).toHaveLength(5); + expect(skillEnvs).toHaveLength(6); }); }); diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 6ded0e2a..16841678 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -9,6 +9,7 @@ import { installCommand } from './commands/install'; import { registerMemoryCommand } from './commands/memory'; import { registerSkillCommand } from './commands/skill'; import { registerAgentCommand } from './commands/agent'; +import { registerChannelCommand } from './commands/channel'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { version } = require('../package.json') as { version: string }; @@ -57,5 +58,6 @@ program registerMemoryCommand(program); registerSkillCommand(program); registerAgentCommand(program); +registerChannelCommand(program); program.parse(); diff --git a/packages/cli/src/commands/channel.ts b/packages/cli/src/commands/channel.ts new file mode 100644 index 00000000..e9929b53 --- /dev/null +++ b/packages/cli/src/commands/channel.ts @@ -0,0 +1,380 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import { + AgentManager, + ClaudeCodeAdapter, + CodexAdapter, + TerminalFocusManager, + TtyWriter, + type AgentAdapter, + type AgentInfo, + type TerminalLocation, +} from '@ai-devkit/agent-manager'; +import { Telegraf } from 'telegraf'; +import { + ChannelManager, + TelegramAdapter, + TELEGRAM_CHANNEL_TYPE, + ConfigStore, + type ChannelEntry, + type TelegramConfig, +} from '@ai-devkit/channel-connector'; +import { ui } from '../util/terminal-ui'; + +const AGENT_POLL_INTERVAL_MS = 2000; + +function createAgentManager(): AgentManager { + const manager = new AgentManager(); + manager.registerAdapter(new ClaudeCodeAdapter()); + manager.registerAdapter(new CodexAdapter()); + return manager; +} + +function getAgentAdapter(agentType: string): AgentAdapter | null { + const adapters: Record = { + claude: new ClaudeCodeAdapter(), + codex: new CodexAdapter(), + }; + return adapters[agentType] ?? null; +} + +async function resolveTargetAgent(agentManager: AgentManager, agentName: string): Promise { + const agents = await agentManager.listAgents(); + + if (agents.length === 0) { + ui.error('No running agents detected.'); + return null; + } + + const resolved = agentManager.resolveAgent(agentName, agents); + if (!resolved) { + ui.error(`No agent found matching "${agentName}".`); + ui.info('Available agents:'); + agents.forEach(a => console.log(` - ${a.name}`)); + return null; + } + + if (Array.isArray(resolved)) { + const { selectedAgent } = await inquirer.prompt([{ + type: 'list', + name: 'selectedAgent', + message: 'Multiple agents match. Select one:', + choices: resolved.map(a => ({ + name: `${a.name} (PID: ${a.pid})`, + value: a, + })), + }]); + return selectedAgent; + } + + return resolved as AgentInfo; +} + +function setupInputHandler( + telegram: TelegramAdapter, + terminalLocation: TerminalLocation, + chatIdRef: { value: string | null }, +): void { + telegram.onMessage(async (msg) => { + if (!chatIdRef.value) { + chatIdRef.value = msg.chatId; + ui.info(`Authorized Telegram user (chat ID: ${msg.chatId})`); + } + + if (msg.chatId !== chatIdRef.value) { + await telegram.sendMessage(msg.chatId, 'Unauthorized. Only the first user is allowed.'); + return; + } + + try { + await TtyWriter.send(terminalLocation, msg.text); + } catch (error: any) { + ui.error(`Failed to send to agent: ${error.message}`); + await telegram.sendMessage(msg.chatId, `Failed to send to agent: ${error.message}`); + } + }); +} + +function startOutputPolling( + telegram: TelegramAdapter, + agentAdapter: AgentAdapter, + agent: AgentInfo, + chatIdRef: { value: string | null }, +): NodeJS.Timeout { + let lastMessageCount = 0; + + // Initialize with current conversation length to avoid sending history + if (agent.sessionFilePath) { + try { + const existing = agentAdapter.getConversation(agent.sessionFilePath); + lastMessageCount = existing.length; + } catch { + // Session file might not exist yet + } + } + + return setInterval(async () => { + if (!chatIdRef.value || !agent.sessionFilePath) return; + + try { + const conversation = agentAdapter.getConversation(agent.sessionFilePath); + const newMessages = conversation.slice(lastMessageCount); + lastMessageCount = conversation.length; + + for (const msg of newMessages) { + if (msg.role !== 'user' && msg.content) { + await telegram.sendMessage(chatIdRef.value, msg.content); + } + } + } catch { + // Agent may have terminated — check later + } + }, AGENT_POLL_INTERVAL_MS); +} + +function setupGracefulShutdown(manager: ChannelManager, pollInterval: NodeJS.Timeout): void { + const shutdown = async () => { + ui.info('\nShutting down...'); + clearInterval(pollInterval); + await manager.stopAll(); + ui.success('Channel bridge stopped.'); + process.exit(0); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); +} + +export function registerChannelCommand(program: Command): void { + const channelCommand = program + .command('channel') + .description('Connect agents with messaging channels'); + + channelCommand + .command('connect ') + .description('Connect a messaging channel (e.g., telegram)') + .action(async (type: string) => { + try { + if (type !== TELEGRAM_CHANNEL_TYPE) { + ui.error(`Unsupported channel type: ${type}. Supported: ${TELEGRAM_CHANNEL_TYPE}`); + return; + } + + const configStore = new ConfigStore(); + const existing = await configStore.getChannel(TELEGRAM_CHANNEL_TYPE); + if (existing) { + const { overwrite } = await inquirer.prompt([{ + type: 'confirm', + name: 'overwrite', + message: 'Telegram is already configured. Overwrite?', + default: false, + }]); + if (!overwrite) return; + } + + ui.info('To connect Telegram, you need a bot token from @BotFather.'); + ui.info('Open Telegram, search for @BotFather, and create a new bot.\n'); + + const { botToken } = await inquirer.prompt([{ + type: 'password', + name: 'botToken', + message: 'Enter your Telegram bot token:', + validate: (input: string) => { + if (!input.trim()) return 'Bot token is required'; + if (!input.includes(':')) return 'Invalid token format (expected number:hash)'; + return true; + }, + }]); + + // Validate token by calling getMe + const spinner = ui.spinner('Validating bot token...'); + spinner.start(); + + let botUsername: string; + try { + const bot = new Telegraf(botToken.trim()); + const me = await bot.telegram.getMe(); + botUsername = me.username; + spinner.succeed(`Connected to bot @${botUsername}`); + } catch (error: any) { + spinner.fail('Invalid bot token. Please check and try again.'); + return; + } + + const entry: ChannelEntry = { + type: TELEGRAM_CHANNEL_TYPE, + enabled: true, + createdAt: new Date().toISOString(), + config: { + botToken: botToken.trim(), + botUsername, + } as TelegramConfig, + }; + + await configStore.saveChannel(TELEGRAM_CHANNEL_TYPE, entry); + ui.success('Telegram channel configured successfully!'); + ui.info(`Bot: @${botUsername}`); + ui.info('Run "ai-devkit channel start --agent " to start the bridge.'); + + } catch (error: any) { + ui.error(`Failed to connect channel: ${error.message}`); + process.exit(1); + } + }); + + channelCommand + .command('list') + .description('List configured channels') + .action(async () => { + try { + const configStore = new ConfigStore(); + const config = await configStore.getConfig(); + const channels = Object.entries(config.channels); + + if (channels.length === 0) { + ui.info('No channels configured. Run "ai-devkit channel connect telegram" to set up.'); + return; + } + + ui.text('Configured Channels:', { breakline: true }); + + const rows = channels.map(([name, entry]) => { + const telegramConfig = entry.config as TelegramConfig; + return [ + name, + entry.type, + entry.enabled ? chalk.green('enabled') : chalk.dim('disabled'), + telegramConfig.botUsername ? `@${telegramConfig.botUsername}` : '-', + entry.createdAt ? new Date(entry.createdAt).toLocaleDateString() : '-', + ]; + }); + + ui.table({ + headers: ['Name', 'Type', 'Status', 'Bot', 'Created'], + rows, + }); + + } catch (error: any) { + ui.error(`Failed to list channels: ${error.message}`); + process.exit(1); + } + }); + + channelCommand + .command('disconnect ') + .description('Remove a channel configuration') + .action(async (type: string) => { + try { + const configStore = new ConfigStore(); + const existing = await configStore.getChannel(type); + + if (!existing) { + ui.info(`No ${type} channel configured.`); + return; + } + + const { confirm } = await inquirer.prompt([{ + type: 'confirm', + name: 'confirm', + message: `Remove ${type} channel configuration?`, + default: false, + }]); + + if (!confirm) return; + + await configStore.removeChannel(type); + ui.success(`${type} channel disconnected.`); + + } catch (error: any) { + ui.error(`Failed to disconnect channel: ${error.message}`); + process.exit(1); + } + }); + + channelCommand + .command('start') + .description('Start the channel bridge to a running agent') + .requiredOption('--agent ', 'Name of the agent to bridge') + .action(async (options) => { + try { + const configStore = new ConfigStore(); + const channelEntry = await configStore.getChannel(TELEGRAM_CHANNEL_TYPE); + + if (!channelEntry) { + ui.error('No Telegram channel configured. Run "ai-devkit channel connect telegram" first.'); + return; + } + + const telegramConfig = channelEntry.config as TelegramConfig; + + // Resolve agent + const agentManager = createAgentManager(); + const agent = await resolveTargetAgent(agentManager, options.agent); + if (!agent) return; + + // Get the adapter for reading conversation + const agentAdapter = getAgentAdapter(agent.type); + if (!agentAdapter) { + ui.error(`Unsupported agent type: ${agent.type}`); + return; + } + + // Find agent terminal + const focusManager = new TerminalFocusManager(); + const terminalLocation = await focusManager.findTerminal(agent.pid); + + if (!terminalLocation) { + ui.error(`Cannot find terminal for agent "${agent.name}" (PID: ${agent.pid}).`); + return; + } + + // Set up channel bridge + const telegram = new TelegramAdapter({ botToken: telegramConfig.botToken }); + const chatIdRef = { value: null as string | null }; + + setupInputHandler(telegram, terminalLocation, chatIdRef); + const pollInterval = startOutputPolling(telegram, agentAdapter, agent, chatIdRef); + + // Start the bot + const manager = new ChannelManager(); + manager.registerAdapter(telegram); + setupGracefulShutdown(manager, pollInterval); + + ui.success(`Bridge started: Telegram @${telegramConfig.botUsername} <-> Agent "${agent.name}" (PID: ${agent.pid})`); + ui.info('Send a message to your Telegram bot to start chatting.'); + ui.info('Press Ctrl+C to stop.\n'); + + await manager.startAll(); + + // Keep process running + await new Promise(() => {}); + } catch (error: any) { + ui.error(`Failed to start channel bridge: ${error.message}`); + process.exit(1); + } + }); + + channelCommand + .command('status') + .description('Show channel bridge status') + .action(async () => { + const configStore = new ConfigStore(); + const config = await configStore.getConfig(); + const channels = Object.entries(config.channels); + + if (channels.length === 0) { + ui.info('No channels configured.'); + return; + } + + for (const [name, entry] of channels) { + const telegramConfig = entry.config as TelegramConfig; + console.log(`${chalk.bold(name)} (${entry.type})`); + console.log(` Enabled: ${entry.enabled ? chalk.green('yes') : chalk.red('no')}`); + console.log(` Bot: @${telegramConfig.botUsername || 'unknown'}`); + console.log(` Configured: ${entry.createdAt || 'unknown'}`); + console.log(); + } + }); +} diff --git a/packages/cli/src/util/env.ts b/packages/cli/src/util/env.ts index a8a0cbb4..7892ef5b 100644 --- a/packages/cli/src/util/env.ts +++ b/packages/cli/src/util/env.ts @@ -58,6 +58,8 @@ export const ENVIRONMENT_DEFINITIONS: Record"` and reuse relevant context. +1. **Use AI DevKit Memory for Context** — Search AI DevKit memory (not built-in memory) for related decisions or conventions via `npx ai-devkit@latest memory search --query ""` and reuse relevant context. If unfamiliar, check the AI DevKit memory skill first. +2. **Capture Requirement** — Ask for: feature name (kebab-case, e.g., `user-authentication`), problem it solves, who uses it, key user stories. **Brainstorm**: ask clarifying questions as needed, explore alternatives to confirm this is the right thing to build, then present 2–3 approaches with one-line trade-offs and a recommendation. 3. **Create Feature Documentation Structure** — Copy each template's content (preserving YAML frontmatter and section headings) into feature-specific files: - `{{docsDir}}/requirements/README.md` → `{{docsDir}}/requirements/feature-{name}.md` - `{{docsDir}}/design/README.md` → `{{docsDir}}/design/feature-{name}.md` diff --git a/skills/dev-lifecycle/references/code-review.md b/skills/dev-lifecycle/references/code-review.md index 3291a0d6..78a20041 100644 --- a/skills/dev-lifecycle/references/code-review.md +++ b/skills/dev-lifecycle/references/code-review.md @@ -1,12 +1,19 @@ # Phase 8: Code Review -Final pre-push review. Check `git status -sb` and `git diff --stat`. +Final pre-push **holistic** review. Go beyond the diff — review how changes integrate with the broader codebase. Check `git status -sb` and `git diff --stat`. 1. **Gather context** — feature description, modified files, design docs, risky areas, tests already run. 2. **Verify design alignment** — summarize architectural intent, check implementation matches. -3. **File-by-file review** — correctness, logic/edge cases, redundancy, security, performance, error handling, test coverage. -4. **Cross-cutting** — naming conventions, documentation updates, missing tests, config/migration changes. -5. **Summarize** — blocking issues, important follow-ups, nice-to-haves. Per finding: file, issue, impact severity, recommendation. -6. **Final checklist** — design match, no logic gaps, security addressed, tests cover changes, docs updated. +3. **Holistic codebase review** — for each modified file, grep exported names (functions, types, constants) to trace callers and dependents. Read only relevant sections (signatures, call sites, type defs) — skip files with no shared interface. Then check: + - **Consistency**: scan 1–2 similar modules for pattern alignment. + - **Duplication**: search for existing utilities the new code could reuse or now duplicates. + - **Contract integrity**: verify type signatures, API contracts, config/DB schemas remain consistent at integration boundaries. + - **Dependency health**: check for circular dependencies or version conflicts from new imports. + - **Breaking changes**: public APIs, CLI flags, env vars, or config keys changed in ways that break existing consumers. + - **Rollback safety**: can this be safely reverted? Flag irreversible migrations or one-way data/state changes. +4. **File-by-file review** — correctness, logic/edge cases, redundancy, security, performance, error handling, test coverage. +5. **Cross-cutting** — naming conventions, documentation updates, missing tests, config/migration changes. +6. **Summarize** — blocking issues, important follow-ups, nice-to-haves. Per finding: file, issue, impact severity, recommendation. Include findings from both diff and broader codebase analysis. +7. **Final checklist** — design match, no logic gaps, security addressed, integration points verified, tests cover changes, docs updated. **Done**: If checklist passes, ready to push and create PR. If blocking issues → back to Phase 4 (fix code) or Phase 7 (add tests). diff --git a/skills/dev-lifecycle/references/new-requirement.md b/skills/dev-lifecycle/references/new-requirement.md index 0039fc9a..3cbae374 100644 --- a/skills/dev-lifecycle/references/new-requirement.md +++ b/skills/dev-lifecycle/references/new-requirement.md @@ -1,7 +1,7 @@ # Phase 1: New Requirement -1. **Search memory** for relevant past features or conventions. -2. **Ask** for: feature name (kebab-case), problem, target users, key user stories. Skip what memory already covers; store answers after. +1. **Search AI DevKit memory** (not built-in memory) for relevant past features or conventions via `npx ai-devkit@latest memory search --query ""`. If unfamiliar, check the AI DevKit memory skill first. +2. **Ask** for: feature name (kebab-case), problem, target users, key user stories. Skip what memory already covers; store answers after. **Brainstorm**: ask clarifying questions as needed, explore alternatives to confirm this is the right thing to build, then present 2–3 approaches for the chosen direction — one-line trade-offs + recommendation. 3. **Run shared setup first** using [worktree-setup.md](worktree-setup.md) with normalized ``: - Default: create and use `feature-` worktree - Optional fallback: no-worktree only when user explicitly requests it diff --git a/skills/index.json b/skills/index.json index 4c0d46e9..971b1b3a 100644 --- a/skills/index.json +++ b/skills/index.json @@ -1,77 +1,80 @@ { "meta": { "version": 1, - "createdAt": 1774743061332, - "updatedAt": 1774743061332, + "createdAt": 1775952745763, + "updatedAt": 1775952745763, "registryHeads": { - "anthropics/skills": "98669c11ca63e9c81c11501e1437e5c47b556621", - "vercel-labs/agent-skills": "64484e9a6022c81e3af59f5dcee6fb6d631bf53e", - "remotion-dev/skills": "d5d395582c6227249cec74f53ab79aca77a4ff16", - "supabase/agent-skills": "c7f83da3b60e198d2bed28cff4b56e8ac705e694", - "obra/superpowers": "eafe962b18f6c5dc70fb7c8cc7e83e61f4cdde06", + "anthropics/skills": "12ab35c2eb5668c95810e6a6066f40f4218adc39", + "vercel-labs/agent-skills": "73140fc5b3a214ad3222bcf557b397b3c02d11c1", + "remotion-dev/skills": "41a3b0685bc648c5f66b3c81d67c61589875eefb", + "supabase/agent-skills": "05a8f1a3a9f0977e192c44a85627532e0e69fe8f", + "obra/superpowers": "917e5f53b16b115b70a3a355ed5f4993b9f8b73d", "softaworks/agent-toolkit": "3027f20f3181758385a1bb8c022d4041dfb4de84", - "codeaholicguy/ai-devkit": "63eeefae03df8bade46f8a8f37927c0ed741b799", + "codeaholicguy/ai-devkit": "89ba9b31f3f7b53b1d7a32828a76f638fc55d634", "antfu/skills": "c35a5588a5158b5b404a14fb10469b2b6dc1952b", - "browser-use/browser-use": "eee98ff2725739b56fdd7ebf71ac04a5eab54cf7", - "microsoft/agent-skills": "c6a6d1a7164ea9ed28648beb8d81d94efe50007f", - "vercel-labs/skills": "d95d0cacfd0ebe9677cb96d51c66a8e9d5630f2c", - "vercel-labs/agent-browser": "dc26ff76679a1f3b0cf22651d06d79e40dfe88fe", - "coreyhaines31/marketingskills": "7c8c087486c29290b982820d719e1c4a556c0053", - "callstackincubator/agent-skills": "fa8694ceaa306a9442202feda37c4b5798e3512a", + "browser-use/browser-use": "1324a88445573da09bea4f19adac5c0a4731ec88", + "microsoft/agent-skills": "f246ec163617b1bd47b02f7c34ae644c36e6576b", + "vercel-labs/skills": "df0579f85cb8a360473c921e1343359006100d3c", + "vercel-labs/agent-browser": "fa043a496f7579680c78b22d0a5015f48dc99a4d", + "coreyhaines31/marketingskills": "2c7c108bd53efc35e3408ad9cd6f0921686e8205", + "callstackincubator/agent-skills": "ace14e40df8eebaeea4816c2a1da1f046676b37a", "hyf0/vue-skills": "c9d355ff23f654309dd02006be671859df0a134c", "napoleond/clawdirect": "b645ffdd610571af6dd0dd4911cb905c57b1091a", - "vercel/ai": "767e354108908f23ad730be3a9ee118ee980c709", + "vercel/ai": "6e706db5b98079311e85e8f77022602cb88ab58c", "subsy/ralph-tui": "fcea670a2e811f5ccf2e3987fd710fb6b692c9a2", - "atxp-dev/cli": "2d548dc4f413bf2736834ea31cc0620aab4bf056", - "giuseppe-trisciuoglio/developer-kit": "55966ee6d77735972f6a4bd9b191904e32e7ac33", - "vercel/turborepo": "cd2d25bbdbfd38278c80b0e8388c646cc82b2234", - "jimliu/baoyu-skills": "9eb032a22f2ed5b617b3e3874c1bf1fb62fd4354", - "google-labs-code/stitch-skills": "ad0b5cc5d5c3569e12a3105b0dee7409c3227e1b", - "jezweb/claude-skills": "936165298a4ef5e67b8ed922921a0dd3e5ea2377", - "firecrawl/cli": "6838c4b8d2d1294b8571d133aafeab6700b5dd50", + "atxp-dev/cli": "9685a861d3bb24e4bc29effc3ccff86320b8b02d", + "giuseppe-trisciuoglio/developer-kit": "618b176e027ac856389921662977d651bfcaf588", + "vercel/turborepo": "41ff958d893c472be7c7268540b93809194fd3d7", + "jimliu/baoyu-skills": "31b2929d1cc00b57dfd20571416ad2284145525f", + "google-labs-code/stitch-skills": "6c0cbdb909b7d256c8b9b3854c8c8f87aab2c140", + "jezweb/claude-skills": "bf917575ed6b65eb98307ced903f469020d8cd0a", + "firecrawl/cli": "cd2c3e1e013012ab8f08b0220afc08f18c824e71", "vercel-labs/next-skills": "038954e07bfc313e97fa5f6ff7caf87226e4a782", "inference-sh/skills": "2c19448d43ef280a4b29d5238afaf7e1c6f00c01", - "intellectronica/agent-skills": "9f7f750cdb158316fb57fcce755f7530fe1118e2", - "resend/react-email": "c1d019bb5de400aba88459e2c7009d7ed7009f17", - "onmax/nuxt-skills": "004e2460c21d1d07203743a2fd47a7d2321fc8b6", + "intellectronica/agent-skills": "3412af720dd499dc0d7306f63deaf3daa0a0c667", + "resend/react-email": "b4a91a67313d73668efbdf005a276cb240d2dc78", + "onmax/nuxt-skills": "00fb59d760c60e5e00fd14403fa2dd2cf3f0b35f", "forrestchang/andrej-karpathy-skills": "aa4467f0b33e1e80d11c7c043d4b27e7c79a73a3", "vuejs-ai/skills": "c9d355ff23f654309dd02006be671859df0a134c", "cloudai-x/threejs-skills": "b1c623076c661fc9b03dac19292e825a5d106823", "boristane/agent-skills": "8aa14dd16a1340a6049e6d7cd58e2ed52333a550", - "kepano/obsidian-skills": "bb9ec95e1b59c3471bd6fd77a78a4042430bfac3", - "sickn33/antigravity-awesome-skills": "e874770c0d2a9e45f2d48b9becf84c0d2ddb492e", + "kepano/obsidian-skills": "fa1e131a014576ff8f8919f191a7ca8d8fded39b", + "sickn33/antigravity-awesome-skills": "f2d80cea0b5a0f84500cbd0f0969dabf5d3f6bff", "zackkorman/skills": "7d77bd2f6305ddc150571930a8486c9d8078c845", "jeffallan/claude-skills": "5b761018cebd430edcecbeeb46bdde6150d22c65", "ibelick/ui-skills": "95d0e08524b9f287c32696760725ae32c0f59791", "brianlovin/claude-config": "1a9819ebf3fee811150fc76cbe177ea4e5f747ff", "waynesutton/convexskills": "8ef49c96675f760dd5569c0588c1abb04cd989dd", - "stripe/ai": "42954a7f0946d687285c820f5b85a4bcb8357e25", - "cloudflare/skills": "ed57ea9dcb5d520f6ed055d3b1418993ed1ae593", - "resciencelab/opc-skills": "87c96800c0affdcabf6174814951e40ac3954cac", + "stripe/ai": "fd912693173dcba346cc9739f0f89f76ab934c78", + "cloudflare/skills": "5ec03da67e230df52b698255c8e5979dc9b124b6", + "resciencelab/opc-skills": "04dda8299373ac70122e150782805f837205b20e", "adithya-s-k/manim_skill": "cef045011722d285692e3381d12d4d637da56e18", "analogjs/angular-skills": "610c90eb9490194bcff703f343f97fa0e00bdb2f", - "github/awesome-copilot": "445933628d2d32a57d614184597e19b02c249a47", - "simonwong/agent-skills": "667ea2ae7643de1e965b135ef38e2b502c39e462", + "github/awesome-copilot": "8395dce14cce763504aab2197effcc62c730a0f6", + "simonwong/agent-skills": "01b2f13fbd6e8e65ce047389b1b9b54b635431a9", "superdesigndev/superdesign-skill": "12a637f7782194256818d3e47c2aff2be41b2634", - "figma/mcp-server-guide": "512f1c770037f8bb4a86748df54e219716d64414", + "figma/mcp-server-guide": "9680714bad40503ef37a9f815fd1d2cd15150af4", "addyosmani/web-quality-skills": "fed9617111260e19f4f54b72a2874a3f3de8ff94", "vueuse/skills": "075b0d6d558cc5ca7d5ffe72a56b5fd92bbef2d1", "pluginagentmarketplace/custom-plugin-java": "51aa571c25436e667618e27b8ac8c1d500d62aa2", - "othmanadi/planning-with-files": "bb3a21ab0d3efbfb3f719124644fc2688a3373e4", - "SawyerHood/dev-browser": "94748ec1cab3a9a0f7ce758f6b253f222dc1a894", - "dgreenheck/webgpu-claude-skill": "4adcfe8ef2317eae0801dbf4a396844954f8e996", - "affaan-m/everything-claude-code": "f98207feea4c9f11f3c44d8c4ad248c624b835f6", + "othmanadi/planning-with-files": "32f3c1f9a93a67632b98e1461f2781732d6f7eca", + "SawyerHood/dev-browser": "9c85e469e213badb5c5ff96987553e6ff05df9f4", + "dgreenheck/webgpu-claude-skill": "af2319bd01bb7cc881267a9ef42cafdaf5e9029d", + "affaan-m/everything-claude-code": "125d5e619905d97b519a887d5bc7332dcc448a52", "CloudAI-X/claude-workflow-v2": "b6952dd3cd31b548d57ea88352c84692030e5480", - "muratcankoylan/Agent-Skills-for-Context-Engineering": "3ab8c948898908e4acc083c32f0390f0caafe3e4", + "muratcankoylan/Agent-Skills-for-Context-Engineering": "401f3dde9653c4fabb4f74d566969c0a6310ab6b", "itsmostafa/aws-agent-skills": "5df6da7060ce411e959312f07aa3cc1fad2eedd7", - "apify/agent-skills": "6d522216c99f7fa4e936b8f1c068fbbb0484258f", + "apify/agent-skills": "8e101a05bcf8518158af4435935cc84db9bea041", "WordPress/agent-skills": "c5c0697b120ec00e8fcf6a265f161c61dbc2581c", "lackeyjb/playwright-skill": "bb7e920d376022958214e349ef25498a2644e189", - "dbt-labs/dbt-agent-skills": "a21c3538ee3cc4b5a38b2da33522d11077998f30", + "dbt-labs/dbt-agent-skills": "56a4553b6b839ce673bbfc8adb5fc593355bb84d", "google-gemini/gemini-skills": "5b655a47ac6d03a2d96d6e64ca051b6a7fe62296", "HeyVincent-ai/agent-skills": "76ae07b11eca1b70f2565f55341a73dafca85d26", - "huggingface/skills": "7aa8697975c7efe07c2f93d3e5c0795a6597efe0", - "mcollina/skills": "317db7c0a9bf71a8d3a4c23ad599895f86d60503" + "huggingface/skills": "f89b940362495d2a65dbd88af9755dd5001f2acd", + "mcollina/skills": "3a5bd304cf5cdf75a997b03bb675121f4bbb365b", + "samber/cc-skills-golang": "9dfc70fb8c4a82fbe3bff052d8bddeeff5541ce4", + "addyosmani/agent-skills": "931310f4341e869e9b816a820b73312a6a213926", + "Shopify/Shopify-AI-Toolkit": "b1ba5a1746c9b1b18ad16b83ec75a117dcc23faa" } }, "skills": [ @@ -80,15911 +83,17710 @@ "registry": "anthropics/skills", "path": "skills/algorithmic-art", "description": "Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.", - "lastIndexed": 1774743050150 + "lastIndexed": 1775952737554 }, { "name": "brand-guidelines", "registry": "anthropics/skills", "path": "skills/brand-guidelines", "description": "Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.", - "lastIndexed": 1774743050138 + "lastIndexed": 1775952737585 }, { "name": "canvas-design", "registry": "anthropics/skills", "path": "skills/canvas-design", "description": "Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.", - "lastIndexed": 1774743050144 + "lastIndexed": 1775952737593 }, { "name": "claude-api", "registry": "anthropics/skills", "path": "skills/claude-api", - "description": "Build apps with the Claude API or Anthropic SDK. TRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`/`claude_agent_sdk`, or user asks to use Claude API, Anthropic SDKs, or Agent SDK. DO NOT TRIGGER when: code imports `openai`/other AI SDK, general programming, or ML/data-science tasks.", - "lastIndexed": 1774743050149 + "description": "No description available", + "lastIndexed": 1775952737619 }, { "name": "doc-coauthoring", "registry": "anthropics/skills", "path": "skills/doc-coauthoring", "description": "Guide users through a structured workflow for co-authoring documentation. Use when user wants to write documentation, proposals, technical specs, decision docs, or similar structured content. This workflow helps users efficiently transfer context, refine content through iteration, and verify the doc works for readers. Trigger when user mentions writing docs, creating proposals, drafting specs, or similar documentation tasks.", - "lastIndexed": 1774743050149 + "lastIndexed": 1775952737593 }, { "name": "docx", "registry": "anthropics/skills", "path": "skills/docx", "description": "Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files). Triggers include: any mention of 'Word doc', 'word document', '.docx', or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a 'report', 'memo', 'letter', 'template', or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation.", - "lastIndexed": 1774743050151 + "lastIndexed": 1775952737594 }, { "name": "frontend-design", "registry": "anthropics/skills", "path": "skills/frontend-design", "description": "Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.", - "lastIndexed": 1774743050140 + "lastIndexed": 1775952737568 }, { "name": "internal-comms", "registry": "anthropics/skills", "path": "skills/internal-comms", "description": "A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill whenever asked to write some sort of internal communications (status reports, leadership updates, 3P updates, company newsletters, FAQs, incident reports, project updates, etc.).", - "lastIndexed": 1774743050137 + "lastIndexed": 1775952737612 }, { "name": "mcp-builder", "registry": "anthropics/skills", "path": "skills/mcp-builder", "description": "Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).", - "lastIndexed": 1774743050140 + "lastIndexed": 1775952737593 }, { "name": "pdf", "registry": "anthropics/skills", "path": "skills/pdf", "description": "Use this skill whenever the user wants to do anything with PDF files. This includes reading or extracting text/tables from PDFs, combining or merging multiple PDFs into one, splitting PDFs apart, rotating pages, adding watermarks, creating new PDFs, filling PDF forms, encrypting/decrypting PDFs, extracting images, and OCR on scanned PDFs to make them searchable. If the user mentions a .pdf file or asks to produce one, use this skill.", - "lastIndexed": 1774743050145 + "lastIndexed": 1775952737587 }, { "name": "pptx", "registry": "anthropics/skills", "path": "skills/pptx", "description": "Use this skill any time a .pptx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx filename, regardless of what they plan to do with the content afterward. If a .pptx file needs to be opened, created, or touched, use this skill.", - "lastIndexed": 1774743050147 + "lastIndexed": 1775952737581 }, { "name": "skill-creator", "registry": "anthropics/skills", "path": "skills/skill-creator", "description": "Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, edit, or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy.", - "lastIndexed": 1774743050153 + "lastIndexed": 1775952737594 }, { "name": "slack-gif-creator", "registry": "anthropics/skills", "path": "skills/slack-gif-creator", "description": "Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like \"make me a GIF of X doing Y for Slack.\"", - "lastIndexed": 1774743050145 + "lastIndexed": 1775952737580 }, { "name": "theme-factory", "registry": "anthropics/skills", "path": "skills/theme-factory", "description": "Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.", - "lastIndexed": 1774743050142 + "lastIndexed": 1775952737592 }, { "name": "web-artifacts-builder", "registry": "anthropics/skills", "path": "skills/web-artifacts-builder", "description": "Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.", - "lastIndexed": 1774743050143 + "lastIndexed": 1775952737592 }, { "name": "webapp-testing", "registry": "anthropics/skills", "path": "skills/webapp-testing", "description": "Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.", - "lastIndexed": 1774743050143 + "lastIndexed": 1775952737598 }, { "name": "xlsx", "registry": "anthropics/skills", "path": "skills/xlsx", "description": "Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like \"the xlsx in my downloads\") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved.", - "lastIndexed": 1774743050147 + "lastIndexed": 1775952737586 }, { "name": "composition-patterns", "registry": "vercel-labs/agent-skills", "path": "skills/composition-patterns", "description": "React composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component libraries, or designing reusable APIs. Triggers on tasks involving compound components, render props, context providers, or component architecture. Includes React 19 API changes.", - "lastIndexed": 1774743050148 + "lastIndexed": 1775952737647 }, { "name": "deploy-to-vercel", "registry": "vercel-labs/agent-skills", "path": "skills/deploy-to-vercel", "description": "Deploy applications and websites to Vercel. Use when the user requests deployment actions like \"deploy my app\", \"deploy and give me the link\", \"push this live\", or \"create a preview deployment\".", - "lastIndexed": 1774743050158 + "lastIndexed": 1775952737575 }, { "name": "react-best-practices", "registry": "vercel-labs/agent-skills", "path": "skills/react-best-practices", "description": "React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.", - "lastIndexed": 1774743050166 + "lastIndexed": 1775952737582 }, { "name": "react-native-skills", "registry": "vercel-labs/agent-skills", "path": "skills/react-native-skills", "description": "React Native and Expo best practices for building performant mobile apps. Use when building React Native components, optimizing list performance, implementing animations, or working with native modules. Triggers on tasks involving React Native, Expo, mobile performance, or native platform APIs.", - "lastIndexed": 1774743050163 + "lastIndexed": 1775952737588 + }, + { + "name": "react-view-transitions", + "registry": "vercel-labs/agent-skills", + "path": "skills/react-view-transitions", + "description": "Guide for implementing smooth, native-feeling animations using React's View Transition API (`` component, `addTransitionType`, and CSS view transition pseudo-elements). Use this skill whenever the user wants to add page transitions, animate route changes, create shared element animations, animate enter/exit of components, animate list reorder, implement directional (forward/back) navigation animations, or integrate view transitions in Next.js. Also use when the user mentions view transitions, `startViewTransition`, `ViewTransition`, transition types, or asks about animating between UI states in React without third-party animation libraries.", + "lastIndexed": 1775952737587 }, { "name": "vercel-cli-with-tokens", "registry": "vercel-labs/agent-skills", "path": "skills/vercel-cli-with-tokens", "description": "Deploy and manage projects on Vercel using token-based authentication. Use when working with Vercel CLI using access tokens rather than interactive login — e.g. \"deploy to vercel\", \"set up vercel\", \"add environment variables to vercel\".", - "lastIndexed": 1774743050152 + "lastIndexed": 1775952737583 }, { "name": "web-design-guidelines", "registry": "vercel-labs/agent-skills", "path": "skills/web-design-guidelines", "description": "Review UI code for Web Interface Guidelines compliance. Use when asked to \"review my UI\", \"check accessibility\", \"audit design\", \"review UX\", or \"check my site against best practices\".", - "lastIndexed": 1774743050152 + "lastIndexed": 1775952737574 }, { "name": "remotion", "registry": "remotion-dev/skills", "path": "skills/remotion", "description": "Best practices for Remotion - Video creation in React", - "lastIndexed": 1774743050187 + "lastIndexed": 1775952737550 }, { "name": "supabase-postgres-best-practices", "registry": "supabase/agent-skills", "path": "skills/supabase-postgres-best-practices", "description": "Postgres performance optimization and best practices from Supabase. Use this skill when writing, reviewing, or optimizing Postgres queries, schema designs, or database configurations.", - "lastIndexed": 1774743050178 + "lastIndexed": 1775952737506 + }, + { + "name": "supabase", + "registry": "supabase/agent-skills", + "path": "skills/supabase", + "description": "Use when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector).", + "lastIndexed": 1775952737539 }, { "name": "brainstorming", "registry": "obra/superpowers", "path": "skills/brainstorming", "description": "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation.", - "lastIndexed": 1774743050183 + "lastIndexed": 1775952737552 }, { "name": "dispatching-parallel-agents", "registry": "obra/superpowers", "path": "skills/dispatching-parallel-agents", "description": "Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies", - "lastIndexed": 1774743050208 + "lastIndexed": 1775952737552 }, { "name": "executing-plans", "registry": "obra/superpowers", "path": "skills/executing-plans", "description": "Use when you have a written implementation plan to execute in a separate session with review checkpoints", - "lastIndexed": 1774743050230 + "lastIndexed": 1775952737552 }, { "name": "finishing-a-development-branch", "registry": "obra/superpowers", "path": "skills/finishing-a-development-branch", "description": "Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup", - "lastIndexed": 1774743050210 + "lastIndexed": 1775952737546 }, { "name": "receiving-code-review", "registry": "obra/superpowers", "path": "skills/receiving-code-review", "description": "Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation", - "lastIndexed": 1774743050214 + "lastIndexed": 1775952737547 }, { "name": "requesting-code-review", "registry": "obra/superpowers", "path": "skills/requesting-code-review", "description": "Use when completing tasks, implementing major features, or before merging to verify work meets requirements", - "lastIndexed": 1774743050208 + "lastIndexed": 1775952737548 }, { "name": "subagent-driven-development", "registry": "obra/superpowers", "path": "skills/subagent-driven-development", "description": "Use when executing implementation plans with independent tasks in the current session", - "lastIndexed": 1774743050209 + "lastIndexed": 1775952737545 }, { "name": "systematic-debugging", "registry": "obra/superpowers", "path": "skills/systematic-debugging", "description": "Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes", - "lastIndexed": 1774743050212 + "lastIndexed": 1775952737556 }, { "name": "test-driven-development", "registry": "obra/superpowers", "path": "skills/test-driven-development", "description": "Use when implementing any feature or bugfix, before writing implementation code", - "lastIndexed": 1774743050229 + "lastIndexed": 1775952737554 }, { "name": "using-git-worktrees", "registry": "obra/superpowers", "path": "skills/using-git-worktrees", "description": "Use when starting feature work that needs isolation from current workspace or before executing implementation plans - creates isolated git worktrees with smart directory selection and safety verification", - "lastIndexed": 1774743050223 + "lastIndexed": 1775952737545 }, { "name": "using-superpowers", "registry": "obra/superpowers", "path": "skills/using-superpowers", "description": "Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions", - "lastIndexed": 1774743050195 + "lastIndexed": 1775952737553 }, { "name": "verification-before-completion", "registry": "obra/superpowers", "path": "skills/verification-before-completion", "description": "Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always", - "lastIndexed": 1774743050208 + "lastIndexed": 1775952737545 }, { "name": "writing-plans", "registry": "obra/superpowers", "path": "skills/writing-plans", "description": "Use when you have a spec or requirements for a multi-step task, before touching code", - "lastIndexed": 1774743050202 + "lastIndexed": 1775952737553 }, { "name": "writing-skills", "registry": "obra/superpowers", "path": "skills/writing-skills", "description": "Use when creating new skills, editing existing skills, or verifying skills work before deployment", - "lastIndexed": 1774743050205 + "lastIndexed": 1775952737648 }, { "name": "agent-md-refactor", "registry": "softaworks/agent-toolkit", "path": "skills/agent-md-refactor", "description": "Refactor bloated AGENTS.md, CLAUDE.md, or similar agent instruction files to follow progressive disclosure principles. Splits monolithic files into organized, linked documentation.", - "lastIndexed": 1774743050573 + "lastIndexed": 1775952737888 }, { "name": "backend-to-frontend-handoff-docs", "registry": "softaworks/agent-toolkit", "path": "skills/backend-to-frontend-handoff-docs", "description": "Create API handoff documentation for frontend developers. Use when backend work is complete and needs to be documented for frontend integration, or user says 'create handoff', 'document API', 'frontend handoff', or 'API documentation'.", - "lastIndexed": 1774743050602 + "lastIndexed": 1775952737875 }, { "name": "c4-architecture", "registry": "softaworks/agent-toolkit", "path": "skills/c4-architecture", "description": "Generate architecture documentation using C4 model Mermaid diagrams. Use when asked to create architecture diagrams, document system architecture, visualize software structure, create C4 diagrams, or generate context/container/component/deployment diagrams. Triggers include \"architecture diagram\", \"C4 diagram\", \"system context\", \"container diagram\", \"component diagram\", \"deployment diagram\", \"document architecture\", \"visualize architecture\".", - "lastIndexed": 1774743050588 + "lastIndexed": 1775952737883 }, { "name": "codex", "registry": "softaworks/agent-toolkit", "path": "skills/codex", "description": "Use when the user asks to run Codex CLI (codex exec, codex resume) or references OpenAI Codex for code analysis, refactoring, or automated editing. Uses GPT-5.2 by default for state-of-the-art software engineering.", - "lastIndexed": 1774743050586 + "lastIndexed": 1775952737884 }, { "name": "command-creator", "registry": "softaworks/agent-toolkit", "path": "skills/command-creator", "description": "This skill should be used when creating a Claude Code slash command. Use when users ask to \"create a command\", \"make a slash command\", \"add a command\", or want to document a workflow as a reusable command. Essential for creating optimized, agent-executable slash commands with proper structure and best practices.", - "lastIndexed": 1774743050578 + "lastIndexed": 1775952737888 }, { "name": "commit-work", "registry": "softaworks/agent-toolkit", "path": "skills/commit-work", "description": "Create high-quality git commits: review/stage intended changes, split into logical commits, and write clear commit messages (including Conventional Commits). Use when the user asks to commit, craft a commit message, stage changes, or split work into multiple commits.", - "lastIndexed": 1774743050571 + "lastIndexed": 1775952737882 }, { "name": "crafting-effective-readmes", "registry": "softaworks/agent-toolkit", "path": "skills/crafting-effective-readmes", "description": "Use when writing or improving README files. Not all READMEs are the same — provides templates and guidance matched to your audience and project type.", - "lastIndexed": 1774743050566 + "lastIndexed": 1775952737886 }, { "name": "daily-meeting-update", "registry": "softaworks/agent-toolkit", "path": "skills/daily-meeting-update", "description": "Interactive daily standup/meeting update generator. Use when user says 'daily', 'standup', 'scrum update', 'status update', 'what did I do yesterday', 'prepare for meeting', 'morning update', or 'team sync'. Pulls activity from GitHub, Jira, and Claude Code session history. Conducts 4-question interview (yesterday, today, blockers, discussion topics) and generates formatted Markdown update.", - "lastIndexed": 1774743050567 + "lastIndexed": 1775952737895 }, { "name": "database-schema-designer", "registry": "softaworks/agent-toolkit", "path": "skills/database-schema-designer", "description": "Design robust, scalable database schemas for SQL and NoSQL databases. Provides normalization guidelines, indexing strategies, migration patterns, constraint design, and performance optimization. Ensures data integrity, query performance, and maintainable data models.", - "lastIndexed": 1774743050559 + "lastIndexed": 1775952737895 }, { "name": "datadog-cli", "registry": "softaworks/agent-toolkit", "path": "skills/datadog-cli", "description": "Datadog CLI for searching logs, querying metrics, tracing requests, and managing dashboards. Use this when debugging production issues or working with Datadog observability.", - "lastIndexed": 1774743050588 + "lastIndexed": 1775952737875 }, { "name": "dependency-updater", "registry": "softaworks/agent-toolkit", "path": "skills/dependency-updater", "description": "Smart dependency management for any language. Auto-detects project type, applies safe updates automatically, prompts for major versions, diagnoses and fixes dependency issues.", - "lastIndexed": 1774743050568 + "lastIndexed": 1775952737891 }, { "name": "design-system-starter", "registry": "softaworks/agent-toolkit", "path": "skills/design-system-starter", "description": "Create and evolve design systems with design tokens, component architecture, accessibility guidelines, and documentation templates. Ensures consistent, scalable, and accessible UI across products.", - "lastIndexed": 1774743050601 + "lastIndexed": 1775952737887 }, { "name": "difficult-workplace-conversations", "registry": "softaworks/agent-toolkit", "path": "skills/difficult-workplace-conversations", "description": "Structured approach to workplace conflicts, performance discussions, and challenging feedback using preparation-delivery-followup framework. Use when preparing for tough conversations, addressing conflicts, giving critical feedback, or navigating sensitive workplace discussions.", - "lastIndexed": 1774743050590 + "lastIndexed": 1775952737887 }, { "name": "domain-name-brainstormer", "registry": "softaworks/agent-toolkit", "path": "skills/domain-name-brainstormer", "description": "Generates creative domain name ideas for your project and checks availability across multiple TLDs (.com, .io, .dev, .ai, etc.). Saves hours of brainstorming and manual checking.", - "lastIndexed": 1774743050590 + "lastIndexed": 1775952737889 }, { "name": "draw-io", "registry": "softaworks/agent-toolkit", "path": "skills/draw-io", "description": "draw.io diagram creation, editing, and review. Use for .drawio XML editing, PNG conversion, layout adjustment, and AWS icon usage.", - "lastIndexed": 1774743050588 + "lastIndexed": 1775952737891 }, { "name": "excalidraw", "registry": "softaworks/agent-toolkit", "path": "skills/excalidraw", "description": "Use when working with *.excalidraw or *.excalidraw.json files, user mentions diagrams/flowcharts, or requests architecture visualization - delegates all Excalidraw operations to subagents to prevent context exhaustion from verbose JSON (single files: 4k-22k tokens, can exceed read limits)", - "lastIndexed": 1774743050599 + "lastIndexed": 1775952737883 }, { "name": "feedback-mastery", "registry": "softaworks/agent-toolkit", "path": "skills/feedback-mastery", "description": "Navigate difficult conversations and deliver constructive feedback using structured frameworks. Covers the Preparation-Delivery-Follow-up model and Situation-Behavior-Impact (SBI) feedback technique. Use when preparing for difficult conversations, giving feedback, or managing conflicts.", - "lastIndexed": 1774743050602 + "lastIndexed": 1775952737892 }, { "name": "frontend-to-backend-requirements", "registry": "softaworks/agent-toolkit", "path": "skills/frontend-to-backend-requirements", "description": "Document frontend data needs for backend developers. Use when frontend needs to communicate API requirements to backend, or user says 'backend requirements', 'what data do I need', 'API requirements', or is describing data needs for a UI.", - "lastIndexed": 1774743050566 + "lastIndexed": 1775952737885 }, { "name": "game-changing-features", "registry": "softaworks/agent-toolkit", "path": "skills/game-changing-features", "description": "Find 10x product opportunities and high-leverage improvements. Use when user wants strategic product thinking, mentions '10x', wants to find high-impact features, or says 'what would make this 10x better', 'product strategy', or 'what should we build next'.", - "lastIndexed": 1774743050598 + "lastIndexed": 1775952737896 }, { "name": "gemini", "registry": "softaworks/agent-toolkit", "path": "skills/gemini", "description": "Use when the user asks to run Gemini CLI for code review, plan review, or big context (>200k) processing. Ideal for comprehensive analysis requiring large context windows. Uses Gemini 3 Pro by default for state-of-the-art reasoning and coding.", - "lastIndexed": 1774743050557 + "lastIndexed": 1775952737883 }, { "name": "gepetto", "registry": "softaworks/agent-toolkit", "path": "skills/gepetto", "description": "Creates detailed, sectionized implementation plans through research, stakeholder interviews, and multi-LLM review. Use when planning features that need thorough pre-implementation analysis.", - "lastIndexed": 1774743050588 + "lastIndexed": 1775952737891 }, { "name": "humanizer", "registry": "softaworks/agent-toolkit", "path": "skills/humanizer", "description": "Remove signs of AI-generated writing from text. Use when editing or reviewing\ntext to make it sound more natural and human-written. Based on Wikipedia's\ncomprehensive \"Signs of AI writing\" guide. Detects and fixes patterns including:\ninflated symbolism, promotional language, superficial -ing analyses, vague\nattributions, em dash overuse, rule of three, AI vocabulary words, negative\nparallelisms, and excessive conjunctive phrases.\n\nCredits: Original skill by @blader - https://github.com/blader/humanizer", - "lastIndexed": 1774743050584 + "lastIndexed": 1775952737894 }, { "name": "jira", "registry": "softaworks/agent-toolkit", "path": "skills/jira", "description": "Use when the user mentions Jira issues (e.g., \"PROJ-123\"), asks about tickets, wants to create/view/update issues, check sprint status, or manage their Jira workflow. Triggers on keywords like \"jira\", \"issue\", \"ticket\", \"sprint\", \"backlog\", or issue key patterns.", - "lastIndexed": 1774743050598 + "lastIndexed": 1775952737884 }, { "name": "lesson-learned", "registry": "softaworks/agent-toolkit", "path": "skills/lesson-learned", "description": "Analyze recent code changes via git history and extract software engineering lessons. Use when the user asks 'what is the lesson here?', 'what can I learn from this?', 'engineering takeaway', 'what did I just learn?', 'reflect on this code', or wants to extract principles from recent work.", - "lastIndexed": 1774743050600 + "lastIndexed": 1775952737920 }, { "name": "marp-slide", "registry": "softaworks/agent-toolkit", "path": "skills/marp-slide", "description": "Create professional Marp presentation slides with 7 beautiful themes (default, minimal, colorful, dark, gradient, tech, business). Use when users request slide creation, presentations, or Marp documents. Supports custom themes, image layouts, and \"make it look good\" requests with automatic quality improvements.", - "lastIndexed": 1774743050596 + "lastIndexed": 1775952737894 }, { "name": "meme-factory", "registry": "softaworks/agent-toolkit", "path": "skills/meme-factory", "description": "Generate memes using the memegen.link API. Use when users request memes, want to add humor to content, or need visual aids for social media. Supports 100+ popular templates with custom text and styling.", - "lastIndexed": 1774743050588 + "lastIndexed": 1775952737888 }, { "name": "mermaid-diagrams", "registry": "softaworks/agent-toolkit", "path": "skills/mermaid-diagrams", "description": "Comprehensive guide for creating software diagrams using Mermaid syntax. Use when users need to create, visualize, or document software through diagrams including class diagrams (domain modeling, object-oriented design), sequence diagrams (application flows, API interactions, code execution), flowcharts (processes, algorithms, user journeys), entity relationship diagrams (database schemas), C4 architecture diagrams (system context, containers, components), state diagrams, git graphs, pie charts, gantt charts, or any other diagram type. Triggers include requests to \"diagram\", \"visualize\", \"model\", \"map out\", \"show the flow\", or when explaining system architecture, database design, code structure, or user/application flows.", - "lastIndexed": 1774743050556 + "lastIndexed": 1775952737888 }, { "name": "mui", "registry": "softaworks/agent-toolkit", "path": "skills/mui", "description": "Material-UI v7 component library patterns including sx prop styling, theme integration, responsive design, and MUI-specific hooks. Use when working with MUI components, styling with sx prop, theme customization, or MUI utilities.", - "lastIndexed": 1774743050599 + "lastIndexed": 1775952737887 }, { "name": "naming-analyzer", "registry": "softaworks/agent-toolkit", "path": "skills/naming-analyzer", "description": "Suggest better variable, function, and class names based on context and conventions.", - "lastIndexed": 1774743050602 + "lastIndexed": 1775952737890 }, { "name": "openapi-to-typescript", "registry": "softaworks/agent-toolkit", "path": "skills/openapi-to-typescript", "description": "Converts OpenAPI 3.0 JSON/YAML to TypeScript interfaces and type guards. This skill should be used when the user asks to generate types from OpenAPI, convert schema to TS, create API interfaces, or generate TypeScript types from an API specification.", - "lastIndexed": 1774743050584 + "lastIndexed": 1775952737884 }, { "name": "perplexity", "registry": "softaworks/agent-toolkit", "path": "skills/perplexity", "description": "Web search and research using Perplexity AI. Use when user says \"search\", \"find\", \"look up\", \"ask\", \"research\", or \"what's the latest\" for generic queries. NOT for library/framework docs (use Context7) or workspace questions.", - "lastIndexed": 1774743050588 + "lastIndexed": 1775952737884 }, { "name": "plugin-forge", "registry": "softaworks/agent-toolkit", "path": "skills/plugin-forge", "description": "Create and manage Claude Code plugins with proper structure, manifests, and marketplace integration. Use when creating plugins for a marketplace, adding plugin components (commands, agents, hooks), bumping plugin versions, or working with plugin.json/marketplace.json manifests.", - "lastIndexed": 1774743050572 + "lastIndexed": 1775952737926 }, { "name": "professional-communication", "registry": "softaworks/agent-toolkit", "path": "skills/professional-communication", "description": "Guide technical communication for software developers. Covers email structure, team messaging etiquette, meeting agendas, and adapting messages for technical vs non-technical audiences. Use when drafting professional messages, preparing meeting communications, or improving written communication.", - "lastIndexed": 1774743050590 + "lastIndexed": 1775952737920 }, { "name": "qa-test-planner", "registry": "softaworks/agent-toolkit", "path": "skills/qa-test-planner", "description": "Generate comprehensive test plans, manual test cases, regression test suites, and bug reports for QA engineers. Includes Figma MCP integration for design validation.", - "lastIndexed": 1774743050603 + "lastIndexed": 1775952737923 }, { "name": "react-dev", "registry": "softaworks/agent-toolkit", "path": "skills/react-dev", "description": "This skill should be used when building React components with TypeScript, typing hooks, handling events, or when React TypeScript, React 19, Server Components are mentioned. Covers type-safe patterns for React 18-19 including generic components, proper event typing, and routing integration (TanStack Router, React Router).", - "lastIndexed": 1774743050567 + "lastIndexed": 1775952737909 }, { "name": "react-useeffect", "registry": "softaworks/agent-toolkit", "path": "skills/react-useeffect", "description": "React useEffect best practices from official docs. Use when writing/reviewing useEffect, useState for derived values, data fetching, or state synchronization. Teaches when NOT to use Effect and better alternatives.", - "lastIndexed": 1774743050592 + "lastIndexed": 1775952737913 }, { "name": "reducing-entropy", "registry": "softaworks/agent-toolkit", "path": "skills/reducing-entropy", "description": "Manual-only skill for minimizing total codebase size. Only activate when explicitly requested by user. Measures success by final code amount, not effort. Bias toward deletion.", - "lastIndexed": 1774743050586 + "lastIndexed": 1775952737905 }, { "name": "requirements-clarity", "registry": "softaworks/agent-toolkit", "path": "skills/requirements-clarity", "description": "Clarify ambiguous requirements through focused dialogue before implementation. Use when requirements are unclear, features are complex (>2 days), or involve cross-team coordination. Ask two core questions - Why? (YAGNI check) and Simpler? (KISS check) - to ensure clarity before coding.", - "lastIndexed": 1774743050602 + "lastIndexed": 1775952737903 }, { "name": "session-handoff", "registry": "softaworks/agent-toolkit", "path": "skills/session-handoff", "description": "Creates comprehensive handoff documents for seamless AI agent session transfers. Triggered when: (1) user requests handoff/memory/context save, (2) context window approaches capacity, (3) major task milestone completed, (4) work session ending, (5) user says 'save state', 'create handoff', 'I need to pause', 'context is getting full', (6) resuming work with 'load handoff', 'resume from', 'continue where we left off'. Proactively suggests handoffs after substantial work (multiple file edits, complex debugging, architecture decisions). Solves long-running agent context exhaustion by enabling fresh agents to continue with zero ambiguity.", - "lastIndexed": 1774743050591 + "lastIndexed": 1775952737904 }, { "name": "ship-learn-next", "registry": "softaworks/agent-toolkit", "path": "skills/ship-learn-next", "description": "Transform learning content (like YouTube transcripts, articles, tutorials) into actionable implementation plans using the Ship-Learn-Next framework. Use when user wants to turn advice, lessons, or educational content into concrete action steps, reps, or a learning quest.", - "lastIndexed": 1774743050596 + "lastIndexed": 1775952737927 }, { "name": "skill-judge", "registry": "softaworks/agent-toolkit", "path": "skills/skill-judge", "description": "Evaluate Agent Skill design quality against official specifications and best practices. Use when reviewing, auditing, or improving SKILL.md files and skill packages. Provides multi-dimensional scoring and actionable improvement suggestions.", - "lastIndexed": 1774743050614 + "lastIndexed": 1775952737953 }, { "name": "web-to-markdown", "registry": "softaworks/agent-toolkit", "path": "skills/web-to-markdown", "description": "Use ONLY when the user explicitly says: 'use the skill web-to-markdown ...' (or 'use a skill web-to-markdown ...'). Converts webpage URLs to clean Markdown by calling the local web2md CLI (Puppeteer + Readability), suitable for JS-rendered pages.", - "lastIndexed": 1774743050612 + "lastIndexed": 1775952737917 }, { "name": "writing-clearly-and-concisely", "registry": "softaworks/agent-toolkit", "path": "skills/writing-clearly-and-concisely", "description": "Use when writing prose humans will read—documentation, commit messages, error messages, explanations, reports, or UI text. Applies Strunk's timeless rules for clearer, stronger, more professional writing.", - "lastIndexed": 1774743050600 + "lastIndexed": 1775952737912 + }, + { + "name": "agent-orchestration", + "registry": "codeaholicguy/ai-devkit", + "path": "skills/agent-orchestration", + "description": "Proactively orchestrate running AI agents — scan statuses, assess progress, send next instructions, and coordinate multi-agent workflows. Use when users ask to manage agents, orchestrate work across agents, or check on agent progress.", + "lastIndexed": 1775952737949 }, { "name": "capture-knowledge", "registry": "codeaholicguy/ai-devkit", "path": "skills/capture-knowledge", "description": "Capture structured knowledge about a code entry point and save it to the knowledge docs. Use when users ask to document, understand, or map code for a module, file, folder, function, or API.", - "lastIndexed": 1774743050613 + "lastIndexed": 1775952737863 }, { "name": "debug", "registry": "codeaholicguy/ai-devkit", "path": "skills/debug", "description": "Guide structured debugging before code changes by clarifying expected behavior, reproducing issues, identifying likely root causes, and agreeing on a fix plan with validation steps. Use when users ask to debug bugs, investigate regressions, triage incidents, diagnose failing behavior, handle failing tests, analyze production incidents, investigate error spikes, or run root cause analysis (RCA).", - "lastIndexed": 1774743050637 + "lastIndexed": 1775952737882 }, { "name": "dev-lifecycle", "registry": "codeaholicguy/ai-devkit", "path": "skills/dev-lifecycle", "description": "Structured SDLC workflow with 8 phases — requirements, design review, planning, implementation, testing, and code review. Use when the user wants to build a feature end-to-end, or run any individual phase (new requirement, review requirements, review design, execute plan, update planning, check implementation, write tests, code review).", - "lastIndexed": 1774743050651 + "lastIndexed": 1775952737874 }, { "name": "memory", "registry": "codeaholicguy/ai-devkit", "path": "skills/memory", - "description": "Use AI DevKit's memory service to store and retrieve knowledge via CLI commands instead of MCP.", - "lastIndexed": 1774743050631 + "description": "Use AI DevKit memory via CLI commands. Search before non-trivial work, store verified reusable knowledge, update stale entries, and avoid saving transcripts, secrets, or one-off task progress.", + "lastIndexed": 1775952737881 }, { "name": "simplify-implementation", "registry": "codeaholicguy/ai-devkit", "path": "skills/simplify-implementation", "description": "Analyze and simplify existing implementations to reduce complexity, improve maintainability, and enhance scalability. Use when users ask to simplify code, reduce complexity, refactor for readability, clean up implementations, improve maintainability, reduce technical debt, or make code easier to understand.", - "lastIndexed": 1774743050648 + "lastIndexed": 1775952737863 + }, + { + "name": "tdd", + "registry": "codeaholicguy/ai-devkit", + "path": "skills/tdd", + "description": "Test-driven development — write a failing test before writing production code. Use when implementing new functionality, adding behavior, or fixing bugs during active development.", + "lastIndexed": 1775952737875 }, { "name": "technical-writer", "registry": "codeaholicguy/ai-devkit", "path": "skills/technical-writer", "description": "Review and improve documentation for novice users. Use when users ask to review docs, improve documentation, audit README files, evaluate API docs, review guides, or improve technical writing.", - "lastIndexed": 1774743050639 + "lastIndexed": 1775952737863 + }, + { + "name": "verify", + "registry": "codeaholicguy/ai-devkit", + "path": "skills/verify", + "description": "Enforce evidence-based completion claims — require fresh command output before reporting success. Use when completing any task, fixing a bug, finishing a phase, running tests, building, deploying, or making any \"it works\" claim.", + "lastIndexed": 1775952737881 }, { "name": "antfu", "registry": "antfu/skills", "path": "skills/antfu", "description": "Anthony Fu's opinionated tooling and conventions for JavaScript/TypeScript projects. Use when setting up new projects, configuring ESLint/Prettier alternatives, monorepos, library publishing, or when the user mentions Anthony Fu's preferences.", - "lastIndexed": 1774743050672 + "lastIndexed": 1775952737913 }, { "name": "nuxt", "registry": "antfu/skills", "path": "skills/nuxt", "description": "Nuxt full-stack Vue framework with SSR, auto-imports, and file-based routing. Use when working with Nuxt apps, server routes, useFetch, middleware, or hybrid rendering.", - "lastIndexed": 1774743050681 + "lastIndexed": 1775952737908 }, { "name": "pinia", "registry": "antfu/skills", "path": "skills/pinia", "description": "Pinia official Vue state management library, type-safe and extensible. Use when defining stores, working with state/getters/actions, or implementing store patterns in Vue apps.", - "lastIndexed": 1774743050689 + "lastIndexed": 1775952737923 }, { "name": "pnpm", "registry": "antfu/skills", "path": "skills/pnpm", "description": "Node.js package manager with strict dependency resolution. Use when running pnpm specific commands, configuring workspaces, or managing dependencies with catalogs, patches, or overrides.", - "lastIndexed": 1774743050695 + "lastIndexed": 1775952737928 }, { "name": "slidev", "registry": "antfu/skills", "path": "skills/slidev", "description": "Create and present web-based slidedecks for developers using Slidev with Markdown, Vue components, code highlighting, animations, and interactive features. Use when building technical presentations, conference talks, code walkthroughs, teaching materials, or developer decks.", - "lastIndexed": 1774743050697 + "lastIndexed": 1775952737919 }, { "name": "tsdown", "registry": "antfu/skills", "path": "skills/tsdown", "description": "Bundle TypeScript and JavaScript libraries with blazing-fast speed powered by Rolldown. Use when building libraries, generating type declarations, bundling for multiple formats, or migrating from tsup.", - "lastIndexed": 1774743050692 + "lastIndexed": 1775952737922 }, { "name": "turborepo", "registry": "antfu/skills", "path": "skills/turborepo", "description": "Turborepo monorepo build system guidance. Triggers on: turbo.json, task pipelines,\ndependsOn, caching, remote cache, the \"turbo\" CLI, --filter, --affected, CI optimization, environment\nvariables, internal packages, monorepo structure/best practices, and boundaries.\n\nUse when user: configures tasks/workflows/pipelines, creates packages, sets up\nmonorepo, shares code between apps, runs changed/affected packages, debugs cache,\nor has apps/packages directories.", - "lastIndexed": 1774743050670 + "lastIndexed": 1775952737954 }, { "name": "unocss", "registry": "antfu/skills", "path": "skills/unocss", "description": "UnoCSS instant atomic CSS engine, superset of Tailwind CSS. Use when configuring UnoCSS, writing utility rules, shortcuts, or working with presets like Wind, Icons, Attributify.", - "lastIndexed": 1774743050669 + "lastIndexed": 1775952737926 }, { "name": "vite", "registry": "antfu/skills", "path": "skills/vite", "description": "Vite build tool configuration, plugin API, SSR, and Vite 8 Rolldown migration. Use when working with Vite projects, vite.config.ts, Vite plugins, or building libraries/SSR apps with Vite.", - "lastIndexed": 1774743050688 + "lastIndexed": 1775952737925 }, { "name": "vitepress", "registry": "antfu/skills", "path": "skills/vitepress", "description": "VitePress static site generator powered by Vite and Vue. Use when building documentation sites, configuring themes, or writing Markdown with Vue components.", - "lastIndexed": 1774743050686 + "lastIndexed": 1775952737928 }, { "name": "vitest", "registry": "antfu/skills", "path": "skills/vitest", "description": "Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures.", - "lastIndexed": 1774743050690 + "lastIndexed": 1775952737931 }, { "name": "vue-best-practices", "registry": "antfu/skills", "path": "skills/vue-best-practices", "description": "MUST be used for Vue.js tasks. Strongly recommends Composition API with `