Remove config value
-agentos config set-key Set API key
-agentos config keys List API keys
-
-agentos models list List available models
-agentos models aliases List model aliases
-agentos models providers List LLM providers
-agentos models describe Describe model details
-
-agentos memory get Get memory entry
-agentos memory set Set memory entry
-agentos memory delete Delete memory entry
-agentos memory list List agent memories
-
-agentos security audit View audit log
-agentos security verify Verify audit chain
-agentos security scan Scan for injection
-
-agentos approvals list List pending approvals
-agentos approvals approve Approve action
-agentos approvals reject Reject action
-
-agentos sessions list [agent] List sessions
-agentos sessions delete Delete session
-
-agentos vault init Initialize vault
-agentos vault set Store secret
-agentos vault list List secrets
-agentos vault remove Remove secret
-
-agentos cron list List cron jobs
-agentos cron create Create cron job
-agentos cron delete Delete cron job
-agentos cron enable Enable cron job
-agentos cron disable Disable cron job
-
-agentos replay get Get session replay
-agentos replay list [--agent] List replays
-agentos replay summary Replay summary
-
-agentos migrate scan Scan for migrations
-agentos migrate openclaw [--dry-run] Migrate from OpenClaw
-agentos migrate langchain [--dry-run] Migrate from LangChain
-agentos migrate report Migration report
-
-agentos logs [--lines N] [--follow] View logs
-agentos integrations [query] Browse integrations
-agentos add [--key K] Add integration
-agentos remove Remove integration
-agentos mcp MCP server mode
-agentos onboard [--quick] Interactive onboarding
-agentos reset [--confirm] Factory reset
-agentos completion Shell completions
-agentos tui Launch terminal UI
-agentos dashboard Open web dashboard
+let iii = register_worker("ws://localhost:49134", InitOptions::default());
+let result = iii.trigger(TriggerRequest {
+ function_id: "realm::create".to_string(),
+ payload: json!({"name": "prod"}),
+ action: None,
+ timeout_ms: None,
+}).await?;
```
-## TUI Dashboard
-
-25 screens accessible via keyboard shortcuts:
-
-```
-1 Dashboard 2 Agents 3 Chat 4 Channels 5 Skills
-6 Hands 7 Workflows 8 Sessions 9 Approvals 0 Logs
-m Memory a Audit s Security p Peers e Extensions
-t Triggers T Templates u Usage S Settings w Wizard
-W Wf Builder L Lifecycle K Tasks R Recovery O Orchestrator
+## Calling functions across workers
-Tab/Shift-Tab to cycle · r to refresh · q to quit
+```rust
+iii.trigger(TriggerRequest {
+ function_id: "memory::recall".to_string(),
+ payload: json!({"agentId": "alice", "query": "..."}),
+ action: None,
+ timeout_ms: None,
+}).await?
```
-## Templates
-
-### 45 Agent Templates (`agents/`)
-
-Pre-built agents ready to spawn across 9 divisions: assistant, coder, researcher, debugger, architect, code-reviewer, doc-writer, test-engineer, devops-lead, ops, orchestrator, planner, analyst, data-scientist, customer-support, email-assistant, health-tracker, home-automation, legal-assistant, meeting-assistant, personal-finance, recruiter, sales-assistant, security-auditor, social-media, translator, travel-planner, tutor, writer, hello-world, ux-architect, brand-guardian, image-prompt-engineer, growth-hacker, content-creator, app-store-optimizer, evidence-collector, performance-benchmarker, reality-checker, trend-researcher, feedback-synthesizer, sprint-prioritizer, rapid-prototyper, mobile-builder, ai-engineer.
-
-### 7 Hands (`hands/`)
-
-Autonomous background workers: researcher, collector, predictor, twitter, browser, clip, lead.
-
-### 25 MCP Integrations (`integrations/`)
-
-GitHub, GitLab, Slack, Discord, Jira, Linear, Notion, Google Drive, Gmail, Google Calendar, AWS, Azure, GCP, PostgreSQL, MongoDB, Redis, Elasticsearch, SQLite, Sentry, Dropbox, Brave Search, Exa Search, Bitbucket, Teams, Todoist.
-
-### 40 Channel Adapters (`src/channels/`)
-
-Telegram, Discord, Slack, WhatsApp, Email, Teams, Google Chat, IRC, Matrix, Mattermost, Signal, Bluesky, Mastodon, Reddit, Twitch, XMPP, Zulip, Rocket.Chat, Feishu, LINE, Messenger, Viber, Revolt, Webex, DingTalk, Discourse, Nostr, Threema, Guilded, Keybase, Nextcloud, Flock, Pumble, Twist, Gitter, Gotify, ntfy, Mumble, LinkedIn, Webhook.
-
-## Security
-
-- **Fail-Closed Defaults**: All security gates deny by default when services are unavailable
-- **RBAC**: Per-agent capability enforcement with tool-level granularity
-- **Mutual Auth (MAP)**: Agent-to-agent authentication with HMAC-SHA256 challenge-response
-- **Timing-Safe Comparison**: Constant-time HMAC verification prevents timing attacks
-- **Audit Chain**: Merkle-linked SHA-256 audit log with tamper detection
-- **Taint Tracking**: Information flow control (Secret, PII, UntrustedAgent labels)
-- **Manifest Signing**: Ed25519 signatures for agent/skill manifests
-- **Tool Policy**: Configurable allow/deny/approve policies per tool
-- **Approval Tiers**: Auto (read-only), Async (write), Sync (destructive) with exponential backoff
-- **Docker Sandbox**: Container-isolated code execution
-- **WASM Sandbox**: wasmtime-based untrusted code execution with fuel limits
-- **Prompt Injection**: Regex-based injection pattern scanning
-- **Rate Limiting**: GCRA token bucket per agent
-- **Loop Guard**: Infinite loop detection with circuit breaker
-- **Vault**: AES-256-GCM encrypted secrets with PBKDF2 key derivation and auto-lock
-- **SQL Injection Prevention**: Identifier validation on all dynamic query fields
-- **Sensitive Data Zeroing**: Auto-zeroing of decrypted secrets after configurable TTL
-
-## LLM Providers (25)
-
-| Provider | Models |
-|----------|--------|
-| Anthropic | Claude Opus, Sonnet, Haiku |
-| OpenAI | GPT-4o, GPT-4o-mini, o1, o3-mini |
-| Google | Gemini 2.0 Flash, Pro |
-| AWS Bedrock | Claude, Titan |
-| DeepSeek | V3, R1 |
-| Groq | Llama, Mixtral |
-| Mistral | Large, Medium, Small |
-| Together | Open-source models |
-| Fireworks | Fast inference |
-| Cohere | Command R+ |
-| Perplexity | Sonar |
-| xAI | Grok |
-| Replicate | Open-source |
-| Ollama | Local models |
-| vLLM | Self-hosted |
-| LM Studio | Local models |
-| OpenRouter | Multi-provider routing |
-| HuggingFace | Inference API |
-| AI21 | Jamba |
-| Cerebras | Fast inference |
-| SambaNova | Enterprise |
-| Qwen | Qwen models |
-| Minimax | Minimax models |
-| Zhipu | GLM models |
-| Moonshot | Kimi |
-
-## SkillKit Integration
-
-Access 15,000+ skills from the SkillKit marketplace:
-
-```bash
-agentos skill search "code review"
-agentos skill install pro-workflow
-```
+This is the only inter-worker contract. Workers stay narrow because the engine carries routing, retries, state, and traces.
-Built-in bridge to [agenstskills.com](https://agenstskills.com) for skill discovery, installation, and AI-powered recommendations.
+## Sandbox surfaces
-## Configuration
+Two distinct namespaces:
-`config.yaml` configures the iii-engine with modules:
+| Namespace | Worker | Semantics |
+|-----------|--------|-----------|
+| `sandbox::*` | builtin iii-sandbox (engine) | Ephemeral microVMs from OCI rootfs |
+| `wasm::*` | agentos `wasm-sandbox` | wasmtime, fuel-metered, sub-millisecond cold start |
-| Module | Port | Purpose |
-|--------|------|---------|
-| WebSocket | 49134 | Worker connections |
-| REST API | 3111 | HTTP API |
-| Streams | 3112 | WebSocket streams |
-| State | — | File-based KV store |
-| Queue | — | Built-in job queue |
-| PubSub | — | Event pub/sub |
-| Cron | — | Scheduled jobs |
-| Observability | — | OpenTelemetry metrics |
+CI's `no sandbox::* clash with builtin` job ensures no agentos worker registers under the reserved namespace.
-## Project Structure
+## Layout
```
-agentos/
-├── Cargo.toml Rust workspace
-├── package.json Node.js package
-├── config.yaml iii-engine configuration
-├── vitest.config.ts Unit/integration test configuration
-├── vitest.e2e.config.ts E2E test configuration
-│
-├── crates/ Surfaces (clients, not workers)
-│ ├── cli/ CLI (50+ commands)
-│ └── tui/ 21-screen terminal dashboard
-│
-├── workers/ Narrow iii workers (one binary each, registered functions over iii.trigger)
-│ ├── agent-core/ ReAct agent loop agent::*
-│ ├── bridge/ External runtime adapters bridge::*
-│ ├── council/ Proposals + hash-chained activity log council::*
-│ ├── directive/ Hierarchical goal alignment directive::*
-│ ├── embedding/ Embedding generation (Python) embedding::*
-│ ├── hierarchy/ Agent org graph (cycle-safe) hierarchy::*
-│ ├── ledger/ Budget enforcement ledger::*
-│ ├── llm-router/ LLM provider routing llm::*
-│ ├── memory/ Narrow agent memory memory::*
-│ ├── mission/ Task lifecycle + state machine mission::*
-│ ├── pulse/ Scheduled agent invocation pulse::*
-│ ├── realm/ Multi-tenant isolation realm::*
-│ ├── security/ Combined guardrails + audit security::*
-│ └── wasm-sandbox/ WASM execution (wasmtime) wasm::*
-│
-├── src/ TypeScript workers (51)
-│ ├── api.ts OpenAI-compatible API
-│ ├── agent-core.ts TS agent loop (profile injection, memory reflection)
-│ ├── tools.ts 22 built-in tools
-│ ├── tools-extended.ts 38 extended tools
-│ ├── memory-reflection.ts Self-curating memory reflection
-│ ├── session-lifecycle.ts Session state machine + reaction rules
-│ ├── task-decomposer.ts Recursive task decomposition
-│ ├── recovery.ts Session health + auto-recovery
-│ ├── orchestrator.ts Multi-agent orchestrator
-│ ├── evolve.ts Dynamic function evolution + DAG branching
-│ ├── eval.ts Production eval harness
-│ ├── feedback.ts Feedback loop + signal injection
-│ ├── artifact-dag.ts DAG-based content artifact exchange
-│ ├── coordination.ts Inter-agent coordination board
-│ ├── swarm.ts Multi-agent swarms
-│ ├── knowledge-graph.ts Entity-relation graph
-│ ├── session-replay.ts Session recording
-│ ├── tool-profiles.ts 8 tool filtering profiles
-│ ├── skillkit-bridge.ts SkillKit marketplace bridge
-│ ├── security-map.ts Mutual Authentication Protocol
-│ ├── channels/ 40 channel adapters
-│ ├── shared/ Shared utilities
-│ ├── __tests__/ 1,748 TypeScript tests (70 files)
-│ └── ... 25 more workers
-│
-├── agents/ 45 agent templates
-├── hands/ 7 autonomous hands
-├── integrations/ 25 MCP integrations
-└── identity/ System identity files
+workers/ 64 Rust workers + 1 Python (embedding)
+crates/ CLI + TUI surfaces (HTTP clients, not workers)
+e2e/ vitest end-to-end suite (live engine + workers)
+tests/ Rust integration tests
+hands/ Agent personas (TOML)
+integrations/ MCP server configs (TOML)
+agents/ Agent templates (markdown)
+workflows/ Workflow definitions (YAML)
+plugin/ Reusable agent/command/skill/hook bundles
+config.yaml iii engine boot config
```
-## Testing
+See [ARCHITECTURE.md](ARCHITECTURE.md) for the full primitive flow and worker manifest spec.
-Test suites:
+## Build and test
```bash
-npx vitest --run # TypeScript unit/integration
-npm run test:e2e # Black-box API e2e (requires AGENTOS_API_KEY)
-cargo test --workspace # Rust crates
-python3 -m pytest # Python worker tests
-```
-
-## How It Works
-
-Every component is a **Worker** that registers **Functions** and binds them to **Triggers**:
-
-```rust
-// Rust worker
-let iii = register_worker("ws://localhost:49134", InitOptions::default());
-
-iii.register_function_with_description(
- "agent::chat",
- "Process a message through the agent loop",
- move |input: Value| { /* ... */ },
-);
-
-iii.register_trigger("queue", "agent::chat", json!({ "topic": "agent.inbox" }))?;
-```
-
-```typescript
-// TypeScript worker (direct iii-sdk registerWorker API)
-import { registerWorker } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "./shared/config.js";
-
-const sdk = registerWorker(ENGINE_URL, { workerName: "api", otel: OTEL_CONFIG });
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-registerFunction(
- {
- id: 'api::chat_completions',
- description: 'OpenAI-compatible chat completions',
- metadata: { category: 'api' },
- request_format: [
- { name: 'model', type: 'string', required: true },
- { name: 'messages', type: 'array', required: true },
- ],
- },
- async (req) => { /* ... */ }
-)
-
-registerTrigger({
- type: 'http',
- function_id: 'api::chat_completions',
- config: { api_path: 'v1/chat/completions', http_method: 'POST' }
-})
-```
-
-```python
-# Python worker
-iii = III("ws://localhost:49134", worker_name="embedding")
-
-@iii.function(id="embedding::generate", description="Generate text embeddings")
-async def generate_embedding(input):
- # ...
-```
-
-Functions call each other via `trigger()` regardless of language:
-
-```
-HTTP POST /v1/chat/completions
- → api::chat_completions (TypeScript)
- → agent::chat (Rust)
- → security::check_capability (Rust)
- → approval::classify (TypeScript)
- → memory::recall (Rust)
- → llm::route (Rust)
- → llm::complete (TypeScript)
- → tool::web_search (TypeScript)
- → loop_guard::check (TypeScript)
- → memory::store (Rust)
- → replay::record (TypeScript)
+cargo build --workspace --release # all workers
+cargo test --workspace --release # 1281 tests
+npm install && npm run test:e2e # live engine + workers (requires AGENTOS_API_KEY)
```
-## Requirements
+## Versioning
-- [iii-engine](https://iii.dev) current stable (iii-sdk registerWorker API with built-in OTel, cron triggers, metadata schemas)
-- Rust 1.75+
-- Node.js 20+
-- Python 3.11+ (optional, for embeddings)
+- iii engine: `v0.11.4-next.4`
+- iii-sdk (Rust): pinned at `=0.11.4-next.4` in workspace
+- iii-sdk (Python): `>=0.11.3` for the embedding worker
+- agentos: `0.0.1` (pre-1.0; 1.0 is reserved for behavioral proof against live infra, not feature completeness)
## License
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index d61963b..0000000
--- a/package-lock.json
+++ /dev/null
@@ -1,3459 +0,0 @@
-{
- "name": "agentos",
- "version": "0.0.1",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "agentos",
- "version": "0.0.1",
- "dependencies": {
- "@anthropic-ai/sdk": "^0.39.0",
- "iii-sdk": "^0.10.0",
- "nodemailer": "^8.0.1"
- },
- "devDependencies": {
- "@types/node": "^25.3.2",
- "@types/nodemailer": "^7.0.11",
- "@vitest/coverage-v8": "^3.2.4",
- "tsx": "^4.7.0",
- "typescript": "^5.4.0",
- "vitest": "^3.2.4"
- }
- },
- "node_modules/@ampproject/remapping": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
- "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@anthropic-ai/sdk": {
- "version": "0.39.0",
- "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz",
- "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "^18.11.18",
- "@types/node-fetch": "^2.6.4",
- "abort-controller": "^3.0.0",
- "agentkeepalive": "^4.2.1",
- "form-data-encoder": "1.7.2",
- "formdata-node": "^4.3.2",
- "node-fetch": "^2.6.7"
- }
- },
- "node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
- "version": "18.19.130",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
- "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
- "license": "MIT",
- "dependencies": {
- "undici-types": "~5.26.4"
- }
- },
- "node_modules/@anthropic-ai/sdk/node_modules/undici-types": {
- "version": "5.26.5",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
- "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
- "license": "MIT"
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
- "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
- "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
- "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.29.0"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/types": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
- "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.28.5"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@bcoe/v8-coverage": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
- "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
- "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
- "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
- "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
- "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
- "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
- "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
- "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
- "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
- "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
- "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
- "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
- "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
- "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
- "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
- "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
- "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
- "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
- "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
- "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
- "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
- "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
- "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
- "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
- "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
- "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
- "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@isaacs/cliui": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
- "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^5.1.2",
- "string-width-cjs": "npm:string-width@^4.2.0",
- "strip-ansi": "^7.0.1",
- "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
- "wrap-ansi": "^8.1.0",
- "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@istanbuljs/schema": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
- "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@opentelemetry/api": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
- "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/@opentelemetry/api-logs": {
- "version": "0.57.2",
- "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz",
- "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api": "^1.3.0"
- },
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@opentelemetry/context-async-hooks": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz",
- "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/core": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz",
- "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/semantic-conventions": "1.28.0"
- },
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": {
- "version": "1.28.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
- "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@opentelemetry/instrumentation": {
- "version": "0.57.2",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz",
- "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api-logs": "0.57.2",
- "@types/shimmer": "^1.2.0",
- "import-in-the-middle": "^1.8.1",
- "require-in-the-middle": "^7.1.1",
- "semver": "^7.5.2",
- "shimmer": "^1.2.1"
- },
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/otlp-transformer": {
- "version": "0.57.2",
- "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.57.2.tgz",
- "integrity": "sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api-logs": "0.57.2",
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/resources": "1.30.1",
- "@opentelemetry/sdk-logs": "0.57.2",
- "@opentelemetry/sdk-metrics": "1.30.1",
- "@opentelemetry/sdk-trace-base": "1.30.1",
- "protobufjs": "^7.3.0"
- },
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/propagator-b3": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.30.1.tgz",
- "integrity": "sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "1.30.1"
- },
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/propagator-jaeger": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.30.1.tgz",
- "integrity": "sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "1.30.1"
- },
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/resources": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz",
- "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/semantic-conventions": "1.28.0"
- },
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": {
- "version": "1.28.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
- "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@opentelemetry/sdk-logs": {
- "version": "0.57.2",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.57.2.tgz",
- "integrity": "sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api-logs": "0.57.2",
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/resources": "1.30.1"
- },
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.4.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/sdk-metrics": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.30.1.tgz",
- "integrity": "sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/resources": "1.30.1"
- },
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.3.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/sdk-trace-base": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz",
- "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/resources": "1.30.1",
- "@opentelemetry/semantic-conventions": "1.28.0"
- },
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": {
- "version": "1.28.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
- "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@opentelemetry/sdk-trace-node": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.30.1.tgz",
- "integrity": "sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/context-async-hooks": "1.30.1",
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/propagator-b3": "1.30.1",
- "@opentelemetry/propagator-jaeger": "1.30.1",
- "@opentelemetry/sdk-trace-base": "1.30.1",
- "semver": "^7.5.2"
- },
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/semantic-conventions": {
- "version": "1.40.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz",
- "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@pkgjs/parseargs": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
- "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@protobufjs/aspromise": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
- "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
- "license": "BSD-3-Clause"
- },
- "node_modules/@protobufjs/base64": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
- "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
- "license": "BSD-3-Clause"
- },
- "node_modules/@protobufjs/codegen": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
- "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
- "license": "BSD-3-Clause"
- },
- "node_modules/@protobufjs/eventemitter": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
- "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
- "license": "BSD-3-Clause"
- },
- "node_modules/@protobufjs/fetch": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
- "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@protobufjs/aspromise": "^1.1.1",
- "@protobufjs/inquire": "^1.1.0"
- }
- },
- "node_modules/@protobufjs/float": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
- "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
- "license": "BSD-3-Clause"
- },
- "node_modules/@protobufjs/inquire": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
- "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
- "license": "BSD-3-Clause"
- },
- "node_modules/@protobufjs/path": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
- "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
- "license": "BSD-3-Clause"
- },
- "node_modules/@protobufjs/pool": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
- "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
- "license": "BSD-3-Clause"
- },
- "node_modules/@protobufjs/utf8": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
- "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
- "license": "BSD-3-Clause"
- },
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
- "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
- "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
- "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
- "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
- "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
- "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
- "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
- "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
- "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
- "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
- "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
- "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
- "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
- "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
- "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
- "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
- "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
- "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
- "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
- "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ]
- },
- "node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
- "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ]
- },
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
- "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
- "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
- "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
- "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@types/chai": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
- "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/deep-eql": "*",
- "assertion-error": "^2.0.1"
- }
- },
- "node_modules/@types/deep-eql": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
- "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/estree": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
- "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "25.3.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.2.tgz",
- "integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==",
- "license": "MIT",
- "dependencies": {
- "undici-types": "~7.18.0"
- }
- },
- "node_modules/@types/node-fetch": {
- "version": "2.6.13",
- "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
- "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*",
- "form-data": "^4.0.4"
- }
- },
- "node_modules/@types/nodemailer": {
- "version": "7.0.11",
- "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz",
- "integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/shimmer": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
- "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==",
- "license": "MIT"
- },
- "node_modules/@vitest/coverage-v8": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz",
- "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@ampproject/remapping": "^2.3.0",
- "@bcoe/v8-coverage": "^1.0.2",
- "ast-v8-to-istanbul": "^0.3.3",
- "debug": "^4.4.1",
- "istanbul-lib-coverage": "^3.2.2",
- "istanbul-lib-report": "^3.0.1",
- "istanbul-lib-source-maps": "^5.0.6",
- "istanbul-reports": "^3.1.7",
- "magic-string": "^0.30.17",
- "magicast": "^0.3.5",
- "std-env": "^3.9.0",
- "test-exclude": "^7.0.1",
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@vitest/browser": "3.2.4",
- "vitest": "3.2.4"
- },
- "peerDependenciesMeta": {
- "@vitest/browser": {
- "optional": true
- }
- }
- },
- "node_modules/@vitest/expect": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
- "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/chai": "^5.2.2",
- "@vitest/spy": "3.2.4",
- "@vitest/utils": "3.2.4",
- "chai": "^5.2.0",
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/mocker": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
- "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/spy": "3.2.4",
- "estree-walker": "^3.0.3",
- "magic-string": "^0.30.17"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "msw": "^2.4.9",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
- },
- "peerDependenciesMeta": {
- "msw": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
- }
- },
- "node_modules/@vitest/pretty-format": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
- "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/runner": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
- "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/utils": "3.2.4",
- "pathe": "^2.0.3",
- "strip-literal": "^3.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/snapshot": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
- "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "3.2.4",
- "magic-string": "^0.30.17",
- "pathe": "^2.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/spy": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
- "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyspy": "^4.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/utils": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
- "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "3.2.4",
- "loupe": "^3.1.4",
- "tinyrainbow": "^2.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "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/acorn": {
- "version": "8.16.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
- "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/acorn-import-attributes": {
- "version": "1.9.5",
- "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
- "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
- "license": "MIT",
- "peerDependencies": {
- "acorn": "^8"
- }
- },
- "node_modules/agentkeepalive": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
- "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
- "license": "MIT",
- "dependencies": {
- "humanize-ms": "^1.2.1"
- },
- "engines": {
- "node": ">= 8.0.0"
- }
- },
- "node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/assertion-error": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/ast-v8-to-istanbul": {
- "version": "0.3.12",
- "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz",
- "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/trace-mapping": "^0.3.31",
- "estree-walker": "^3.0.3",
- "js-tokens": "^10.0.0"
- }
- },
- "node_modules/asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "license": "MIT"
- },
- "node_modules/balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/brace-expansion": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
- "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/cac": {
- "version": "6.7.14",
- "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
- "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/chai": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
- "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "assertion-error": "^2.0.1",
- "check-error": "^2.1.1",
- "deep-eql": "^5.0.1",
- "loupe": "^3.1.0",
- "pathval": "^2.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/check-error": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
- "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 16"
- }
- },
- "node_modules/cjs-module-lexer": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
- "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
- "license": "MIT"
- },
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/combined-stream": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
- "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "license": "MIT",
- "dependencies": {
- "delayed-stream": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/deep-eql": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
- "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/eastasianwidth": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-module-lexer": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
- "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-set-tostringtag": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
- "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6",
- "has-tostringtag": "^1.0.2",
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/esbuild": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
- "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.27.3",
- "@esbuild/android-arm": "0.27.3",
- "@esbuild/android-arm64": "0.27.3",
- "@esbuild/android-x64": "0.27.3",
- "@esbuild/darwin-arm64": "0.27.3",
- "@esbuild/darwin-x64": "0.27.3",
- "@esbuild/freebsd-arm64": "0.27.3",
- "@esbuild/freebsd-x64": "0.27.3",
- "@esbuild/linux-arm": "0.27.3",
- "@esbuild/linux-arm64": "0.27.3",
- "@esbuild/linux-ia32": "0.27.3",
- "@esbuild/linux-loong64": "0.27.3",
- "@esbuild/linux-mips64el": "0.27.3",
- "@esbuild/linux-ppc64": "0.27.3",
- "@esbuild/linux-riscv64": "0.27.3",
- "@esbuild/linux-s390x": "0.27.3",
- "@esbuild/linux-x64": "0.27.3",
- "@esbuild/netbsd-arm64": "0.27.3",
- "@esbuild/netbsd-x64": "0.27.3",
- "@esbuild/openbsd-arm64": "0.27.3",
- "@esbuild/openbsd-x64": "0.27.3",
- "@esbuild/openharmony-arm64": "0.27.3",
- "@esbuild/sunos-x64": "0.27.3",
- "@esbuild/win32-arm64": "0.27.3",
- "@esbuild/win32-ia32": "0.27.3",
- "@esbuild/win32-x64": "0.27.3"
- }
- },
- "node_modules/estree-walker": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "^1.0.0"
- }
- },
- "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/expect-type": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
- "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=12.0.0"
- }
- },
- "node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/foreground-child": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
- "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "cross-spawn": "^7.0.6",
- "signal-exit": "^4.0.1"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/form-data": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
- "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
- "license": "MIT",
- "dependencies": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "es-set-tostringtag": "^2.1.0",
- "hasown": "^2.0.2",
- "mime-types": "^2.1.12"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/form-data-encoder": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
- "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
- "license": "MIT"
- },
- "node_modules/formdata-node": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
- "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
- "license": "MIT",
- "dependencies": {
- "node-domexception": "1.0.0",
- "web-streams-polyfill": "4.0.0-beta.3"
- },
- "engines": {
- "node": ">= 12.20"
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/get-tsconfig": {
- "version": "4.13.6",
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
- "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "resolve-pkg-maps": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
- }
- },
- "node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/glob/node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/glob/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/glob/node_modules/minimatch": {
- "version": "9.0.9",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
- "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.2"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-tostringtag": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
- "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "license": "MIT",
- "dependencies": {
- "has-symbols": "^1.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/html-escaper": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
- "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/humanize-ms": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
- "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.0.0"
- }
- },
- "node_modules/iii-sdk": {
- "version": "0.10.0",
- "resolved": "https://registry.npmjs.org/iii-sdk/-/iii-sdk-0.10.0.tgz",
- "integrity": "sha512-suXrgWcVh7hel3a7ylOfttrVzHxi+OJlhWtHDdKra5HU9nqpmIsRc55qFuHB34GTeNWPq3HisEYW+1+7vX+GUA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api": "^1.9.0",
- "@opentelemetry/api-logs": "^0.57.0",
- "@opentelemetry/core": "^1.30.0",
- "@opentelemetry/instrumentation": "^0.57.0",
- "@opentelemetry/otlp-transformer": "^0.57.0",
- "@opentelemetry/resources": "^1.30.0",
- "@opentelemetry/sdk-logs": "^0.57.0",
- "@opentelemetry/sdk-metrics": "^1.30.0",
- "@opentelemetry/sdk-trace-base": "^1.30.0",
- "@opentelemetry/sdk-trace-node": "^1.30.0",
- "@opentelemetry/semantic-conventions": "^1.28.0",
- "ws": "^8.18.3"
- }
- },
- "node_modules/import-in-the-middle": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz",
- "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==",
- "license": "Apache-2.0",
- "dependencies": {
- "acorn": "^8.14.0",
- "acorn-import-attributes": "^1.9.5",
- "cjs-module-lexer": "^1.2.2",
- "module-details-from-path": "^1.0.3"
- }
- },
- "node_modules/is-core-module": {
- "version": "2.16.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
- "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
- "license": "MIT",
- "dependencies": {
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/istanbul-lib-coverage": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
- "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/istanbul-lib-report": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
- "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "istanbul-lib-coverage": "^3.0.0",
- "make-dir": "^4.0.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/istanbul-lib-source-maps": {
- "version": "5.0.6",
- "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
- "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "@jridgewell/trace-mapping": "^0.3.23",
- "debug": "^4.1.1",
- "istanbul-lib-coverage": "^3.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/istanbul-reports": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
- "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "html-escaper": "^2.0.0",
- "istanbul-lib-report": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/jackspeak": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
- "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/cliui": "^8.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- },
- "optionalDependencies": {
- "@pkgjs/parseargs": "^0.11.0"
- }
- },
- "node_modules/js-tokens": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
- "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/long": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
- "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
- "license": "Apache-2.0"
- },
- "node_modules/loupe": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
- "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/magicast": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
- "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.25.4",
- "@babel/types": "^7.25.4",
- "source-map-js": "^1.2.0"
- }
- },
- "node_modules/make-dir": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
- "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "semver": "^7.5.3"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/minimatch": {
- "version": "10.2.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
- "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "brace-expansion": "^5.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/minipass": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
- "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
- "node_modules/module-details-from-path": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz",
- "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
- "license": "MIT"
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/nanoid": {
- "version": "3.3.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/node-domexception": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
- "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
- "deprecated": "Use your platform's native DOMException instead",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/jimmywarting"
- },
- {
- "type": "github",
- "url": "https://paypal.me/jimmywarting"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=10.5.0"
- }
- },
- "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/nodemailer": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
- "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
- "license": "MIT-0",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/package-json-from-dist": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
- "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
- "dev": true,
- "license": "BlueOak-1.0.0"
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "license": "MIT"
- },
- "node_modules/path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
- },
- "engines": {
- "node": ">=16 || 14 >=14.18"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/pathe": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/pathval": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
- "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 14.16"
- }
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/postcss": {
- "version": "8.5.6",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
- "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.11",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/protobufjs": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
- "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
- "hasInstallScript": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "@protobufjs/aspromise": "^1.1.2",
- "@protobufjs/base64": "^1.1.2",
- "@protobufjs/codegen": "^2.0.4",
- "@protobufjs/eventemitter": "^1.1.0",
- "@protobufjs/fetch": "^1.1.0",
- "@protobufjs/float": "^1.0.2",
- "@protobufjs/inquire": "^1.1.0",
- "@protobufjs/path": "^1.1.2",
- "@protobufjs/pool": "^1.1.0",
- "@protobufjs/utf8": "^1.1.0",
- "@types/node": ">=13.7.0",
- "long": "^5.0.0"
- },
- "engines": {
- "node": ">=12.0.0"
- }
- },
- "node_modules/require-in-the-middle": {
- "version": "7.5.2",
- "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz",
- "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.3.5",
- "module-details-from-path": "^1.0.3",
- "resolve": "^1.22.8"
- },
- "engines": {
- "node": ">=8.6.0"
- }
- },
- "node_modules/resolve": {
- "version": "1.22.11",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
- "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
- "license": "MIT",
- "dependencies": {
- "is-core-module": "^2.16.1",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/resolve-pkg-maps": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
- "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
- }
- },
- "node_modules/rollup": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
- "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "1.0.8"
- },
- "bin": {
- "rollup": "dist/bin/rollup"
- },
- "engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
- },
- "optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.59.0",
- "@rollup/rollup-android-arm64": "4.59.0",
- "@rollup/rollup-darwin-arm64": "4.59.0",
- "@rollup/rollup-darwin-x64": "4.59.0",
- "@rollup/rollup-freebsd-arm64": "4.59.0",
- "@rollup/rollup-freebsd-x64": "4.59.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
- "@rollup/rollup-linux-arm64-gnu": "4.59.0",
- "@rollup/rollup-linux-arm64-musl": "4.59.0",
- "@rollup/rollup-linux-loong64-gnu": "4.59.0",
- "@rollup/rollup-linux-loong64-musl": "4.59.0",
- "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
- "@rollup/rollup-linux-ppc64-musl": "4.59.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
- "@rollup/rollup-linux-riscv64-musl": "4.59.0",
- "@rollup/rollup-linux-s390x-gnu": "4.59.0",
- "@rollup/rollup-linux-x64-gnu": "4.59.0",
- "@rollup/rollup-linux-x64-musl": "4.59.0",
- "@rollup/rollup-openbsd-x64": "4.59.0",
- "@rollup/rollup-openharmony-arm64": "4.59.0",
- "@rollup/rollup-win32-arm64-msvc": "4.59.0",
- "@rollup/rollup-win32-ia32-msvc": "4.59.0",
- "@rollup/rollup-win32-x64-gnu": "4.59.0",
- "@rollup/rollup-win32-x64-msvc": "4.59.0",
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/semver": {
- "version": "7.7.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
- "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shimmer": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
- "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==",
- "license": "BSD-2-Clause"
- },
- "node_modules/siginfo": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
- "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/stackback": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
- "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/std-env": {
- "version": "3.10.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
- "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/string-width": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/string-width-cjs": {
- "name": "string-width",
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string-width-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string-width-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/string-width-cjs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
- "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.2.2"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/strip-ansi-cjs": {
- "name": "strip-ansi",
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-literal": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
- "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^9.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/strip-literal/node_modules/js-tokens": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
- "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/test-exclude": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz",
- "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "@istanbuljs/schema": "^0.1.2",
- "glob": "^10.4.1",
- "minimatch": "^10.2.2"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tinybench": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
- "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinyexec": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
- "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinyglobby": {
- "version": "0.2.15",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
- "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.3"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
- }
- },
- "node_modules/tinypool": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
- "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- }
- },
- "node_modules/tinyrainbow": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
- "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/tinyspy": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
- "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "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/tsx": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
- "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "~0.27.0",
- "get-tsconfig": "^4.7.5"
- },
- "bin": {
- "tsx": "dist/cli.mjs"
- },
- "engines": {
- "node": ">=18.0.0"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- }
- },
- "node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/undici-types": {
- "version": "7.18.2",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
- "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
- "license": "MIT"
- },
- "node_modules/vite": {
- "version": "7.3.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
- "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "^0.27.0",
- "fdir": "^6.5.0",
- "picomatch": "^4.0.3",
- "postcss": "^8.5.6",
- "rollup": "^4.43.0",
- "tinyglobby": "^0.2.15"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^20.19.0 || >=22.12.0",
- "jiti": ">=1.21.0",
- "less": "^4.0.0",
- "lightningcss": "^1.21.0",
- "sass": "^1.70.0",
- "sass-embedded": "^1.70.0",
- "stylus": ">=0.54.8",
- "sugarss": "^5.0.0",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "jiti": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "lightningcss": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- },
- "node_modules/vite-node": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
- "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cac": "^6.7.14",
- "debug": "^4.4.1",
- "es-module-lexer": "^1.7.0",
- "pathe": "^2.0.3",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
- },
- "bin": {
- "vite-node": "vite-node.mjs"
- },
- "engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/vitest": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
- "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/chai": "^5.2.2",
- "@vitest/expect": "3.2.4",
- "@vitest/mocker": "3.2.4",
- "@vitest/pretty-format": "^3.2.4",
- "@vitest/runner": "3.2.4",
- "@vitest/snapshot": "3.2.4",
- "@vitest/spy": "3.2.4",
- "@vitest/utils": "3.2.4",
- "chai": "^5.2.0",
- "debug": "^4.4.1",
- "expect-type": "^1.2.1",
- "magic-string": "^0.30.17",
- "pathe": "^2.0.3",
- "picomatch": "^4.0.2",
- "std-env": "^3.9.0",
- "tinybench": "^2.9.0",
- "tinyexec": "^0.3.2",
- "tinyglobby": "^0.2.14",
- "tinypool": "^1.1.1",
- "tinyrainbow": "^2.0.0",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
- "vite-node": "3.2.4",
- "why-is-node-running": "^2.3.0"
- },
- "bin": {
- "vitest": "vitest.mjs"
- },
- "engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@edge-runtime/vm": "*",
- "@types/debug": "^4.1.12",
- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
- "@vitest/browser": "3.2.4",
- "@vitest/ui": "3.2.4",
- "happy-dom": "*",
- "jsdom": "*"
- },
- "peerDependenciesMeta": {
- "@edge-runtime/vm": {
- "optional": true
- },
- "@types/debug": {
- "optional": true
- },
- "@types/node": {
- "optional": true
- },
- "@vitest/browser": {
- "optional": true
- },
- "@vitest/ui": {
- "optional": true
- },
- "happy-dom": {
- "optional": true
- },
- "jsdom": {
- "optional": true
- }
- }
- },
- "node_modules/web-streams-polyfill": {
- "version": "4.0.0-beta.3",
- "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
- "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
- "license": "MIT",
- "engines": {
- "node": ">= 14"
- }
- },
- "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",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/why-is-node-running": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
- "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "siginfo": "^2.0.0",
- "stackback": "0.0.2"
- },
- "bin": {
- "why-is-node-running": "cli.js"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.1.0",
- "string-width": "^5.0.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs": {
- "name": "wrap-ansi",
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/wrap-ansi-cjs/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/ws": {
- "version": "8.19.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
- "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
- "license": "MIT",
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- }
- }
-}
diff --git a/package.json b/package.json
index 6b853fb..badd1a8 100644
--- a/package.json
+++ b/package.json
@@ -3,29 +3,15 @@
"version": "0.0.1",
"description": "Agent Operating System built on iii primitives",
"type": "module",
- "workspaces": [
- "packages/*"
- ],
"scripts": {
- "dev": "iii --config config.yaml & sleep 2 && tsx watch src/",
- "start": "iii --config config.yaml & tsx src/",
- "test": "vitest --run",
- "test:watch": "vitest",
- "test:coverage": "vitest --run --coverage",
"test:e2e": "AGENTOS_E2E=1 vitest --run --config vitest.e2e.config.ts",
"test:e2e:smoke": "AGENTOS_E2E=1 vitest --run --config vitest.e2e.config.ts --testNamePattern=\"health|chat_completions\""
},
"dependencies": {
- "@agentos/shared": "workspace:*",
- "@anthropic-ai/sdk": "^0.39.0",
- "iii-sdk": "^0.11.3",
- "nodemailer": "^8.0.1"
+ "iii-sdk": "0.11.4-next.4"
},
"devDependencies": {
"@types/node": "^25.3.2",
- "@types/nodemailer": "^7.0.11",
- "@vitest/coverage-v8": "^3.2.4",
- "tsx": "^4.7.0",
"typescript": "^5.4.0",
"vitest": "^3.2.4"
}
diff --git a/packages/shared/package.json b/packages/shared/package.json
deleted file mode 100644
index f2e8659..0000000
--- a/packages/shared/package.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "@agentos/shared",
- "version": "0.0.1",
- "private": true,
- "type": "module",
- "main": "./src/index.ts",
- "exports": {
- ".": "./src/index.ts",
- "./config": "./src/config.ts",
- "./errors": "./src/errors.ts",
- "./metrics": "./src/metrics.ts",
- "./pricing": "./src/pricing.ts",
- "./secrets": "./src/secrets.ts",
- "./security-headers": "./src/security-headers.ts",
- "./shutdown": "./src/shutdown.ts",
- "./tokens": "./src/tokens.ts",
- "./utils": "./src/utils.ts",
- "./validate": "./src/validate.ts"
- }
-}
diff --git a/packages/shared/src/config.ts b/packages/shared/src/config.ts
deleted file mode 100644
index bb6c3c1..0000000
--- a/packages/shared/src/config.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { realpathSync } from "node:fs";
-import { isAbsolute, relative, resolve } from "node:path";
-import { shutdownManager } from "./shutdown.js";
-
-export const ENGINE_URL = process.env.III_ENGINE_URL || "ws://localhost:49134";
-export const WORKSPACE_ROOT = process.env.AGENTOS_WORKSPACE || process.cwd();
-
-export const OTEL_CONFIG = {
- enabled: true,
- serviceName: "agentos",
- serviceVersion: "0.0.1",
- metricsEnabled: true,
- fetchInstrumentationEnabled: true,
-} as const;
-
-export function registerShutdown(sdk: { shutdown?: () => Promise }) {
- if (typeof sdk.shutdown === "function") {
- shutdownManager.registerIIIShutdown(sdk.shutdown);
- }
-}
-
-export function assertPathContained(resolved: string) {
- let real: string;
- try {
- real = realpathSync(resolved);
- } catch {
- real = resolve(resolved);
- }
- let workspaceReal: string;
- try {
- workspaceReal = realpathSync(WORKSPACE_ROOT);
- } catch {
- workspaceReal = resolve(WORKSPACE_ROOT);
- }
- const rel = relative(workspaceReal, real);
- if (rel.startsWith("..") || isAbsolute(rel)) {
- throw new Error(`Path traversal denied: ${resolved}`);
- }
-}
diff --git a/packages/shared/src/errors.ts b/packages/shared/src/errors.ts
deleted file mode 100644
index 2f27b4e..0000000
--- a/packages/shared/src/errors.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-export class AppError extends Error {
- code: string;
- context?: Record;
- retryable: boolean;
-
- constructor(
- message: string,
- opts: {
- code: string;
- context?: Record;
- retryable?: boolean;
- cause?: unknown;
- },
- ) {
- super(message, { cause: opts.cause });
- this.name = "AppError";
- this.code = opts.code;
- this.context = opts.context;
- this.retryable = opts.retryable ?? false;
- }
-}
-
-export type ErrorClass = "transient" | "permanent" | "degraded";
-
-export function classifyError(e: unknown): ErrorClass {
- if (e instanceof AppError) {
- if (e.retryable) return "transient";
- return "permanent";
- }
- if (e instanceof Error) {
- const msg = e.message.toLowerCase();
- if (
- msg.includes("timeout") ||
- msg.includes("econnreset") ||
- msg.includes("econnrefused") ||
- msg.includes("socket hang up") ||
- msg.includes("aborted") ||
- msg.includes("network")
- ) {
- return "transient";
- }
- if (
- msg.includes("rate limit") ||
- msg.includes("429") ||
- msg.includes("503") ||
- msg.includes("unavailable")
- ) {
- return "degraded";
- }
- }
- return "permanent";
-}
-
-export async function withRetry(
- fn: () => Promise,
- maxRetries = 3,
- backoffMs = 1000,
-): Promise {
- let lastError: unknown;
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
- try {
- return await fn();
- } catch (e) {
- lastError = e;
- const cls = classifyError(e);
- if (cls === "permanent" || attempt === maxRetries) break;
- const delay = backoffMs * Math.pow(2, attempt);
- await new Promise((r) => setTimeout(r, delay));
- }
- }
- throw lastError;
-}
-
-export async function safeCall(
- fn: () => Promise,
- fallback: T,
- context: { functionId?: string; agentId?: string; operation: string },
-): Promise {
- try {
- return await fn();
- } catch (e) {
- logError(e, context);
- return fallback;
- }
-}
-
-export function logError(
- error: unknown,
- context: { functionId?: string; agentId?: string; operation?: string },
-): void {
- const entry: Record = {
- timestamp: new Date().toISOString(),
- level: "error",
- ...context,
- };
- if (error instanceof AppError) {
- entry.code = error.code;
- entry.message = error.message;
- entry.retryable = error.retryable;
- entry.errorContext = error.context;
- entry.classification = classifyError(error);
- } else if (error instanceof Error) {
- entry.message = error.message;
- entry.stack = error.stack;
- entry.classification = classifyError(error);
- } else {
- entry.message = String(error);
- entry.classification = "permanent";
- }
- console.error(JSON.stringify(entry));
-}
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
deleted file mode 100644
index 02946f2..0000000
--- a/packages/shared/src/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export * from "./config.js";
-export * from "./errors.js";
-export * from "./security-headers.js";
-export * from "./metrics.js";
-export * from "./pricing.js";
-export * from "./secrets.js";
-export * from "./shutdown.js";
-export * from "./tokens.js";
-export * from "./utils.js";
-export * from "./validate.js";
diff --git a/packages/shared/src/metrics.ts b/packages/shared/src/metrics.ts
deleted file mode 100644
index 1605bfd..0000000
--- a/packages/shared/src/metrics.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-export function recordMetric(
- name: string,
- value: number,
- labels?: Record,
- _type?: string,
-) {
-}
-
-type TriggerVoidFn = (id: string, input: unknown) => void | Promise;
-
-export function createRecordMetric(triggerVoid: TriggerVoidFn) {
- return (name: string, value: number, labels?: Record, _type?: string) => {
- triggerVoid("engine::log::info", {
- message: `metric:${name}=${value}`,
- ...labels,
- });
- };
-}
diff --git a/packages/shared/src/pricing.ts b/packages/shared/src/pricing.ts
deleted file mode 100644
index c3efa04..0000000
--- a/packages/shared/src/pricing.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-export const PRICING: Record<
- string,
- { input: number; output: number; cacheRead?: number; cacheWrite?: number }
-> = {
- "claude-opus-4-6": {
- input: 15,
- output: 75,
- cacheRead: 1.5,
- cacheWrite: 18.75,
- },
- "claude-sonnet-4-6": {
- input: 3,
- output: 15,
- cacheRead: 0.3,
- cacheWrite: 3.75,
- },
- "claude-haiku-4-5": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
- "gpt-4o": { input: 2.5, output: 10 },
- "gpt-4.1": { input: 2, output: 8 },
- o3: { input: 10, output: 40 },
- "o4-mini": { input: 1.1, output: 4.4 },
- "gpt-4o-mini": { input: 0.15, output: 0.6 },
- "gemini-2.5-flash": { input: 0.15, output: 0.6 },
- "gemini-2.5-pro": { input: 1.25, output: 10 },
- "deepseek-chat": { input: 0.14, output: 0.28 },
- "deepseek-reasoner": { input: 0.55, output: 2.19 },
- "llama-3.3-70b": { input: 0.59, output: 0.79 },
- "grok-2": { input: 2, output: 10 },
- "grok-3": { input: 3, output: 15 },
- "grok-3-mini": { input: 0.3, output: 0.5 },
- "mistral-large": { input: 2, output: 6 },
- "sonar-pro": { input: 3, output: 15 },
- sonar: { input: 1, output: 1 },
- "command-a": { input: 2.5, output: 10 },
- "command-r-plus": { input: 3, output: 15 },
- "command-r": { input: 0.5, output: 1.5 },
- "jamba-1.5-large": { input: 2, output: 8 },
- "jamba-1.5-mini": { input: 0.2, output: 0.4 },
- "cerebras-llama-3.3-70b": { input: 0.6, output: 0.6 },
- "samba-llama-3.1-405b": { input: 5, output: 10 },
- "samba-llama-3.3-70b": { input: 0.6, output: 0.6 },
- "hf-llama-3.3-70b": { input: 0.36, output: 0.36 },
- "hf-mistral-7b": { input: 0, output: 0 },
- "replicate-llama-3.3-70b": { input: 0.65, output: 2.75 },
- "qwen-max": { input: 2.4, output: 9.6 },
- "qwen-plus": { input: 0.5, output: 1.5 },
- "qwen-turbo": { input: 0.05, output: 0.15 },
- "abab7-chat": { input: 1, output: 1 },
- "glm-4-plus": { input: 7, output: 7 },
- "glm-4": { input: 1.4, output: 1.4 },
- "moonshot-v1-128k": { input: 8.5, output: 8.5 },
- "moonshot-v1-32k": { input: 3.3, output: 3.3 },
- "ernie-4.0-turbo": { input: 4.2, output: 8.4 },
- "ernie-3.5-turbo": { input: 0.56, output: 1.12 },
- "bedrock-claude-sonnet": { input: 3, output: 15 },
- "bedrock-nova-pro": { input: 0.8, output: 3.2 },
- "bedrock-llama-3.3-70b": { input: 0.72, output: 0.72 },
- "copilot-gpt-4o": { input: 2.5, output: 10 },
- "together-llama-3.3-70b": { input: 0.88, output: 0.88 },
- "fireworks-llama-3.3-70b": { input: 0.9, output: 0.9 },
-};
diff --git a/packages/shared/src/secrets.ts b/packages/shared/src/secrets.ts
deleted file mode 100644
index 7bc6d5c..0000000
--- a/packages/shared/src/secrets.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-type TriggerFn = (req: {
- function_id: string;
- payload: unknown;
-}) => Promise;
-
-export function createSecretGetter(triggerFn: TriggerFn) {
- return async function getSecret(key: string): Promise {
- try {
- const result = await triggerFn({
- function_id: "vault::get",
- payload: { key },
- });
- return result?.value || process.env[key] || "";
- } catch {
- return process.env[key] || "";
- }
- };
-}
diff --git a/packages/shared/src/security-headers.ts b/packages/shared/src/security-headers.ts
deleted file mode 100644
index aecf5f5..0000000
--- a/packages/shared/src/security-headers.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export const SECURITY_HEADERS: Record = {
- "Content-Security-Policy":
- "default-src 'self'; script-src 'none'; object-src 'none'; frame-ancestors 'none'",
- "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
- "X-Content-Type-Options": "nosniff",
- "X-Frame-Options": "DENY",
- "X-XSS-Protection": "0",
- "Referrer-Policy": "strict-origin-when-cross-origin",
- "Permissions-Policy": "camera=(), microphone=(), geolocation=()",
- "Cache-Control": "no-store",
- Pragma: "no-cache",
-};
diff --git a/packages/shared/src/shutdown.ts b/packages/shared/src/shutdown.ts
deleted file mode 100644
index 6552ffe..0000000
--- a/packages/shared/src/shutdown.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { logError } from "./errors.js";
-
-const DRAIN_TIMEOUT_MS = 10_000;
-
-type CleanupCallback = () => void | Promise;
-
-class ShutdownManager {
- private inFlight = new Map();
- private cleanupCallbacks: CleanupCallback[] = [];
- private shuttingDown = false;
- private initialized = false;
-
- register(operationId: string): void {
- if (this.shuttingDown) {
- throw new Error("Shutting down: rejecting new operation");
- }
- this.inFlight.set(operationId, Date.now());
- }
-
- complete(operationId: string): void {
- this.inFlight.delete(operationId);
- }
-
- onShutdown(callback: CleanupCallback): void {
- this.cleanupCallbacks.push(callback);
- }
-
- registerIIIShutdown(shutdownFn: () => Promise): void {
- this.cleanupCallbacks.push(shutdownFn);
- }
-
- isShuttingDown(): boolean {
- return this.shuttingDown;
- }
-
- inFlightCount(): number {
- return this.inFlight.size;
- }
-
- initShutdown(): void {
- if (this.initialized) return;
- this.initialized = true;
-
- const handler = async (signal: string) => {
- if (this.shuttingDown) return;
- this.shuttingDown = true;
- console.log(
- JSON.stringify({
- timestamp: new Date().toISOString(),
- level: "info",
- message: `Shutdown initiated (${signal}), draining ${this.inFlight.size} in-flight operations`,
- }),
- );
-
- const deadline = Date.now() + DRAIN_TIMEOUT_MS;
- while (this.inFlight.size > 0 && Date.now() < deadline) {
- await new Promise((r) => setTimeout(r, 250));
- }
-
- if (this.inFlight.size > 0) {
- console.warn(
- JSON.stringify({
- timestamp: new Date().toISOString(),
- level: "warn",
- message: `Force shutdown: ${this.inFlight.size} operations still in-flight`,
- operations: [...this.inFlight.keys()],
- }),
- );
- }
-
- for (const cb of this.cleanupCallbacks) {
- try {
- await cb();
- } catch (e) {
- logError(e, { operation: "shutdown_cleanup" });
- }
- }
-
- process.exit(0);
- };
-
- process.on("SIGTERM", () => handler("SIGTERM"));
- process.on("SIGINT", () => handler("SIGINT"));
- }
-}
-
-export const shutdownManager = new ShutdownManager();
diff --git a/packages/shared/src/tokens.ts b/packages/shared/src/tokens.ts
deleted file mode 100644
index af85fb7..0000000
--- a/packages/shared/src/tokens.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-export interface Message {
- role: string;
- content: string;
- toolResults?: unknown;
- importance?: number;
- timestamp?: number;
-}
-
-export function estimateTokens(text: string): number {
- return Math.ceil([...text].length / 4);
-}
-
-export function estimateMessagesTokens(messages: Message[]): number {
- let total = 0;
- for (const msg of messages) {
- total += estimateTokens(msg.content || "");
- if (msg.toolResults) {
- total += estimateTokens(JSON.stringify(msg.toolResults));
- }
- }
- return total;
-}
diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts
deleted file mode 100644
index 0a398a7..0000000
--- a/packages/shared/src/utils.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-import { createHmac, timingSafeEqual } from "crypto";
-import { lookup } from "node:dns/promises";
-
-export function splitMessage(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;
- }
- let splitAt = remaining.lastIndexOf("\n", maxLen);
- if (splitAt < maxLen / 2) splitAt = maxLen;
- chunks.push(remaining.slice(0, splitAt));
- remaining = remaining.slice(splitAt);
- }
- return chunks;
-}
-
-const BLOCKED_PATTERNS = [
- /^127\./,
- /^10\./,
- /^172\.(1[6-9]|2\d|3[01])\./,
- /^192\.168\./,
- /^169\.254\./,
- /^0\./,
- /^100\.(6[4-9]|[7-9]\d|1[0-1]\d|12[0-7])\./,
- /^::1$/,
- /^fe80:/i,
- /^::ffff:/i,
- /^fc00:/i,
- /^fd/i,
- /^localhost$/i,
- /^.*\.local$/i,
- /^metadata\.google\.internal$/i,
-];
-
-export async function assertNoSsrf(urlStr: string): Promise {
- let parsed: URL;
- try {
- parsed = new URL(urlStr);
- } catch {
- throw new Error(`Invalid URL: ${urlStr}`);
- }
- const host = parsed.hostname;
-
- if (host === "localhost" || host === "0.0.0.0") {
- throw new Error(`SSRF blocked: ${host}`);
- }
-
- for (const pattern of BLOCKED_PATTERNS) {
- if (pattern.test(host)) {
- throw new Error(`SSRF blocked: ${host} is a private/reserved address`);
- }
- }
-
- try {
- const { address } = await lookup(host);
- for (const pattern of BLOCKED_PATTERNS) {
- if (pattern.test(address)) {
- throw new Error(`SSRF blocked: ${host} resolves to ${address}`);
- }
- }
- } catch (e: any) {
- if (e.message?.startsWith("SSRF")) throw e;
- }
-}
-
-export async function resolveAgent(
- sdk: { trigger: (req: { function_id: string; payload: unknown }) => Promise },
- channel: string,
- channelId: string,
-): Promise {
- const mapping = await sdk
- .trigger({
- function_id: "state::get",
- payload: { scope: "channel_agents", key: `${channel}:${channelId}` },
- })
- .catch(() => null);
- return mapping?.agentId || "default";
-}
-
-export function requireAuth(req: any): void {
- const expected = process.env.AGENTOS_API_KEY;
- if (!expected) {
- throw Object.assign(new Error("AGENTOS_API_KEY not configured"), {
- statusCode: 500,
- });
- }
- const header = req.headers?.["authorization"] || "";
- const token = header.replace(/^Bearer\s+/i, "");
- if (!token || !safeEqual(token, expected)) {
- throw Object.assign(new Error("Unauthorized"), { statusCode: 401 });
- }
-}
-
-function safeEqual(a: string, b: string): boolean {
- if (a.length !== b.length) return false;
- try {
- return timingSafeEqual(Buffer.from(a), Buffer.from(b));
- } catch {
- return false;
- }
-}
-
-export function verifySlackSignature(req: any, signingSecret: string): void {
- const timestamp = req.headers?.["x-slack-request-timestamp"];
- const sig = req.headers?.["x-slack-signature"];
- if (!timestamp || !sig) throw new Error("Missing Slack signature headers");
- if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
- throw new Error("Stale Slack timestamp");
- }
- const body =
- typeof req.body === "string" ? req.body : JSON.stringify(req.body);
- const baseString = `v0:${timestamp}:${body}`;
- const computed =
- "v0=" +
- createHmac("sha256", signingSecret).update(baseString).digest("hex");
- if (!safeEqual(computed, sig)) {
- throw new Error("Invalid Slack signature");
- }
-}
-
-export function verifyTelegramUpdate(secretToken: string, req: any): boolean {
- if (!secretToken) return false;
- const header = req.headers?.["x-telegram-bot-api-secret-token"] || "";
- if (!header) return false;
- return safeEqual(header, secretToken);
-}
-
-const MCP_COMMAND_ALLOWLIST = new Set([
- "npx",
- "node",
- "python3",
- "python",
- "uv",
- "uvx",
- "bun",
- "deno",
-]);
-
-export function validateMcpCommand(command: string): void {
- const base = command.split("/").pop() || command;
- if (!MCP_COMMAND_ALLOWLIST.has(base)) {
- throw new Error(
- `MCP command not allowed: ${command}. Allowed: ${[...MCP_COMMAND_ALLOWLIST].join(", ")}`,
- );
- }
-}
-
-export function stripSecretsFromEnv(): Record {
- const SAFE_KEYS = new Set([
- "PATH",
- "HOME",
- "USER",
- "SHELL",
- "LANG",
- "LC_ALL",
- "TERM",
- "NODE_PATH",
- "NODE_ENV",
- "PYTHONPATH",
- ]);
- const env: Record = {};
- for (const [key, val] of Object.entries(process.env)) {
- if (SAFE_KEYS.has(key) && val !== undefined) {
- env[key] = val;
- }
- }
- return env;
-}
-
-export function sanitizeId(id: string): string {
- if (!id || !/^[a-zA-Z0-9_\-:.]{1,256}$/.test(id)) {
- throw Object.assign(new Error(`Invalid ID format: ${id}`), {
- statusCode: 400,
- });
- }
- return id;
-}
-
-export function httpOk(req: any, data: any) {
- return req?.headers ? { status_code: 200, body: data } : data;
-}
-
-export function stripCodeFences(text: string): string {
- return text
- .replace(/^```\w*\s*\n?/gm, "")
- .replace(/\n?```\s*$/gm, "")
- .trim();
-}
diff --git a/packages/shared/src/validate.ts b/packages/shared/src/validate.ts
deleted file mode 100644
index f52472d..0000000
--- a/packages/shared/src/validate.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-export function safeInt(
- val: unknown,
- min: number,
- max: number,
- defaultVal: number,
-): number {
- const n = Number(val);
- if (!Number.isFinite(n)) return defaultVal;
- const i = Math.trunc(n);
- if (i < min) return min;
- if (i > max) return max;
- return i;
-}
-
-export function safeString(val: unknown, maxLen = 10_000): string {
- if (typeof val !== "string") return "";
- return val.trim().slice(0, maxLen);
-}
-
-export function safeArray(val: unknown, maxLen = 1000): T[] {
- if (!Array.isArray(val)) return [];
- return val.slice(0, maxLen);
-}
-
-export function safePagination(
- limit: unknown,
- offset: unknown,
-): { limit: number; offset: number } {
- return {
- limit: safeInt(limit, 1, 1000, 100),
- offset: safeInt(offset, 0, Number.MAX_SAFE_INTEGER, 0),
- };
-}
diff --git a/src/__tests__/api.test.ts b/src/__tests__/api.test.ts
deleted file mode 100644
index a6cf6b0..0000000
--- a/src/__tests__/api.test.ts
+++ /dev/null
@@ -1,508 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const kvStore: Record> = {};
-function getScope(scope: string) {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-function resetKv() {
- for (const key of Object.keys(kvStore)) delete kvStore[key];
-}
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "state::delete") {
- getScope(data.scope).delete(data.key);
- return { ok: true };
- }
- if (fnId === "state::list")
- return [...getScope(data.scope).entries()].map(([key, value]) => ({
- key,
- value,
- }));
- if (fnId === "agent::chat")
- return {
- content: "Hello from agent",
- model: "claude-sonnet-4-6",
- usage: { input: 10, output: 20, total: 30 },
- };
- if (fnId === "agent::list") return { agents: [{ name: "default" }] };
- if (fnId === "agent::create") return { id: data.id || "new-agent" };
- if (fnId === "agent::delete") return { deleted: true };
- if (fnId === "rate::check") return { allowed: true };
- if (fnId === "memory::recall") return [{ role: "system", content: "memory" }];
- if (fnId === "engine::workers::list")
- return [{ name: "api" }, { name: "agent-core" }];
- return null;
-});
-
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/validate", () => ({
- safeInt: vi.fn((val: any, min: number, max: number, def: number) => {
- const n = parseInt(val, 10);
- if (isNaN(n)) return def;
- return Math.max(min, Math.min(max, n));
- }),
-}));
-
-
-vi.mock("@agentos/shared/errors", () => ({
- safeCall: vi.fn(async (fn: Function, fallback: any) => {
- try {
- return await fn();
- } catch {
- return fallback;
- }
- }),
-}));
-
-vi.mock("@agentos/shared/shutdown", () => ({
- shutdownManager: {
- initShutdown: vi.fn(),
- isShuttingDown: vi.fn(() => false),
- inFlightCount: vi.fn(() => 0),
- register: vi.fn(),
- complete: vi.fn(),
- registerIIIShutdown: vi.fn(),
- },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- requireAuth: vi.fn(),
- sanitizeId: vi.fn((id: string) => id?.replace(/[^a-zA-Z0-9_-]/g, "")),
-}));
-
-vi.mock("@agentos/shared/security-headers", () => ({
- SECURITY_HEADERS: { "X-Frame-Options": "DENY" },
-}));
-
-vi.mock("node:fs", () => ({
- readFileSync: vi.fn(() => JSON.stringify({ version: "1.0.0" })),
-}));
-
-beforeEach(() => {
- resetKv();
- mockTrigger.mockReset();
- mockTriggerVoid.mockClear();
- mockTrigger.mockImplementation(
- async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get")
- return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "state::delete") {
- getScope(data.scope).delete(data.key);
- return { ok: true };
- }
- if (fnId === "state::list")
- return [...getScope(data.scope).entries()].map(([key, value]) => ({
- key,
- value,
- }));
- if (fnId === "agent::chat")
- return {
- content: "Hello from agent",
- model: "claude-sonnet-4-6",
- usage: { input: 10, output: 20, total: 30 },
- };
- if (fnId === "agent::list") return { agents: [{ name: "default" }] };
- if (fnId === "agent::create") return { id: data.id || "new-agent" };
- if (fnId === "agent::delete") return { deleted: true };
- if (fnId === "rate::check") return { allowed: true };
- if (fnId === "memory::recall")
- return [{ role: "system", content: "memory" }];
- if (fnId === "engine::workers::list")
- return [{ name: "api" }, { name: "agent-core" }];
- return null;
- },
- );
- process.env.AGENTOS_API_KEY = "test-key";
-});
-
-beforeAll(async () => {
- await import("../api.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-const authReq = (body?: any, extra?: any) => ({
- headers: { authorization: "Bearer test-key" },
- body,
- ...extra,
-});
-
-describe("api::chat_completions", () => {
- it("returns 200 with OpenAI-compatible response", async () => {
- const result = await call(
- "api::chat_completions",
- authReq({
- model: "claude-sonnet-4-6",
- messages: [{ role: "user", content: "Hello" }],
- }),
- );
- expect(result.status_code).toBe(200);
- expect(result.body.object).toBe("chat.completion");
- });
-
- it("returns choices array", async () => {
- const result = await call(
- "api::chat_completions",
- authReq({
- messages: [{ role: "user", content: "Hi" }],
- }),
- );
- expect(result.body.choices).toHaveLength(1);
- expect(result.body.choices[0].message.role).toBe("assistant");
- });
-
- it("returns finish_reason stop", async () => {
- const result = await call(
- "api::chat_completions",
- authReq({
- messages: [{ role: "user", content: "Hi" }],
- }),
- );
- expect(result.body.choices[0].finish_reason).toBe("stop");
- });
-
- it("returns usage information", async () => {
- const result = await call(
- "api::chat_completions",
- authReq({
- messages: [{ role: "user", content: "Hi" }],
- }),
- );
- expect(result.body.usage).toBeDefined();
- expect(result.body.usage.prompt_tokens).toBe(10);
- expect(result.body.usage.completion_tokens).toBe(20);
- expect(result.body.usage.total_tokens).toBe(30);
- });
-
- it("returns created timestamp", async () => {
- const result = await call(
- "api::chat_completions",
- authReq({
- messages: [{ role: "user", content: "Hi" }],
- }),
- );
- expect(result.body.created).toBeGreaterThan(0);
- });
-
- it("returns chatcmpl id prefix", async () => {
- const result = await call(
- "api::chat_completions",
- authReq({
- messages: [{ role: "user", content: "Hi" }],
- }),
- );
- expect(result.body.id).toMatch(/^chatcmpl-/);
- });
-
- it("includes model in response", async () => {
- const result = await call(
- "api::chat_completions",
- authReq({
- messages: [{ role: "user", content: "Hi" }],
- }),
- );
- expect(result.body.model).toBeDefined();
- });
-
- it("includes security headers", async () => {
- const result = await call(
- "api::chat_completions",
- authReq({
- messages: [{ role: "user", content: "Hi" }],
- }),
- );
- expect(result.headers).toBeDefined();
- });
-
- it("uses last message content for agent chat", async () => {
- await call(
- "api::chat_completions",
- authReq({
- messages: [
- { role: "user", content: "first" },
- { role: "assistant", content: "reply" },
- { role: "user", content: "second" },
- ],
- }),
- );
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].message).toBe("second");
- });
-
- it("returns 401 when auth fails", async () => {
- const { requireAuth } = await import("@agentos/shared/utils");
- vi.mocked(requireAuth).mockImplementationOnce(() => {
- const err: any = new Error("Unauthorized");
- err.statusCode = 401;
- throw err;
- });
- const result = await call("api::chat_completions", { headers: {} });
- expect(result.status_code).toBe(401);
- });
-
- it("returns 429 when rate limited", async () => {
- mockTrigger.mockImplementation(async (fnId: string) => {
- if (fnId === "rate::check") return { allowed: false, retryAfter: 60 };
- return null;
- });
- const result = await call(
- "api::chat_completions",
- authReq({
- messages: [{ role: "user", content: "Hi" }],
- }),
- );
- expect(result.status_code).toBe(429);
- expect(result.body.retryAfter).toBe(60);
- });
-});
-
-describe("api::agent_message", () => {
- it("returns 200 with agent response", async () => {
- const result = await call("api::agent_message", {
- ...authReq({ message: "Hello", sessionId: "s1" }),
- path_params: { id: "test-agent" },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.content).toBeDefined();
- });
-
- it("passes sanitized agentId to trigger", async () => {
- await call("api::agent_message", {
- ...authReq({ message: "Hello" }),
- path_params: { id: "my-agent" },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].agentId).toBe("my-agent");
- });
-
- it("passes sessionId to trigger", async () => {
- await call("api::agent_message", {
- ...authReq({ message: "Hello", sessionId: "sess-1" }),
- path_params: { id: "agent1" },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].sessionId).toBe("sess-1");
- });
-});
-
-describe("api::list_agents", () => {
- it("returns 200 with agent list", async () => {
- const result = await call("api::list_agents", authReq());
- expect(result.status_code).toBe(200);
- expect(result.body.agents).toBeDefined();
- });
-
- it("calls agent::list trigger", async () => {
- await call("api::list_agents", authReq());
- const calls = mockTrigger.mock.calls.filter((c) => c[0] === "agent::list");
- expect(calls.length).toBe(1);
- });
-});
-
-describe("api::create_agent", () => {
- it("returns 201 with created agent", async () => {
- const result = await call(
- "api::create_agent",
- authReq({
- name: "new-agent",
- model: { model: "claude-sonnet-4-6" },
- }),
- );
- expect(result.status_code).toBe(201);
- });
-
- it("calls agent::create trigger", async () => {
- await call("api::create_agent", authReq({ name: "a" }));
- const calls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::create",
- );
- expect(calls.length).toBe(1);
- });
-});
-
-describe("api::get_agent", () => {
- it("returns 200 when agent exists", async () => {
- getScope("agents").set("test-agent", { name: "test-agent" });
- const result = await call("api::get_agent", {
- ...authReq(),
- path_params: { id: "test-agent" },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("returns 404 when agent not found", async () => {
- const result = await call("api::get_agent", {
- ...authReq(),
- path_params: { id: "nonexistent" },
- });
- expect(result.status_code).toBe(404);
- expect(result.body.error).toContain("not found");
- });
-});
-
-describe("api::delete_agent", () => {
- it("returns 204 on successful delete", async () => {
- const result = await call("api::delete_agent", {
- ...authReq(),
- path_params: { id: "to-delete" },
- });
- expect(result.status_code).toBe(204);
- expect(result.body).toBeNull();
- });
-
- it("calls agent::delete trigger", async () => {
- await call("api::delete_agent", {
- ...authReq(),
- path_params: { id: "to-delete" },
- });
- const calls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::delete",
- );
- expect(calls.length).toBe(1);
- });
-});
-
-describe("api::agent_sessions", () => {
- it("returns 200 with sessions list", async () => {
- const result = await call("api::agent_sessions", {
- ...authReq(),
- path_params: { id: "agent1" },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("queries sessions scoped to agent", async () => {
- await call("api::agent_sessions", {
- ...authReq(),
- path_params: { id: "agent1" },
- });
- const calls = mockTrigger.mock.calls.filter((c) => c[0] === "state::list");
- expect(calls[0][1].scope).toContain("agent1");
- });
-});
-
-describe("api::health", () => {
- it("returns 200 with health status", async () => {
- const result = await call("api::health", authReq());
- expect(result.status_code).toBe(200);
- expect(result.body.status).toBe("healthy");
- });
-
- it("returns version", async () => {
- const result = await call("api::health", authReq());
- expect(result.body.version).toBeDefined();
- });
-
- it("returns worker count", async () => {
- const result = await call("api::health", authReq());
- expect(result.body.workers).toBeDefined();
- });
-
- it("returns uptime", async () => {
- const result = await call("api::health", authReq());
- expect(result.body.uptime).toBeGreaterThan(0);
- });
-
- it("handles worker list failure", async () => {
- mockTrigger.mockImplementation(async (fnId: string) => {
- if (fnId === "engine::workers::list") throw new Error("fail");
- if (fnId === "rate::check") return { allowed: true };
- return null;
- });
- const result = await call("api::health", authReq());
- expect(result.status_code).toBe(200);
- expect(result.body.workers).toBe(0);
- });
-});
-
-describe("api::costs", () => {
- it("returns 200 with cost data", async () => {
- const result = await call("api::costs", authReq());
- expect(result.status_code).toBe(200);
- });
-
- it("returns zero cost when no data", async () => {
- const result = await call("api::costs", authReq());
- expect(result.body.totalCost).toBe(0);
- });
-
- it("returns cost data when available", async () => {
- const today = new Date().toISOString().slice(0, 10);
- getScope("costs").set(today, { totalCost: 5.5, breakdown: {} });
- const result = await call("api::costs", authReq());
- expect(result.body.totalCost).toBe(5.5);
- });
-});
-
-describe("api::memory_query", () => {
- it("returns 200 with memory results", async () => {
- const result = await call("api::memory_query", {
- ...authReq(),
- path_params: { id: "agent1" },
- query_params: { query: "test", limit: 5 },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("calls memory::recall with correct params", async () => {
- await call("api::memory_query", {
- ...authReq(),
- path_params: { id: "agent1" },
- query_params: { query: "search term", limit: 10 },
- });
- const recallCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "memory::recall",
- );
- expect(recallCalls.length).toBe(1);
- expect(recallCalls[0][1].agentId).toBe("agent1");
- });
-
- it("uses body params when query_params absent", async () => {
- await call("api::memory_query", {
- ...authReq({ query: "from body", limit: 3 }),
- path_params: { id: "agent1" },
- });
- const recallCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "memory::recall",
- );
- expect(recallCalls.length).toBe(1);
- });
-});
diff --git a/src/__tests__/artifact-dag.test.ts b/src/__tests__/artifact-dag.test.ts
deleted file mode 100644
index 7534cae..0000000
--- a/src/__tests__/artifact-dag.test.ts
+++ /dev/null
@@ -1,420 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const kvStore: Record> = {};
-function getScope(scope: string) {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-function resetKv() {
- for (const key of Object.keys(kvStore)) delete kvStore[key];
-}
-function seedKv(scope: string, key: string, value: unknown) {
- getScope(scope).set(key, value);
-}
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "state::list") {
- return [...getScope(data.scope).entries()].map(([key, value]) => ({
- key,
- value,
- }));
- }
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
- Logger: class { info() {} warn() {} error() {} },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- requireAuth: vi.fn(),
- sanitizeId: (id: string) => {
- if (!id || !/^[a-zA-Z0-9_\-:.]{1,256}$/.test(id))
- throw new Error(`Invalid ID: ${id}`);
- return id;
- },
-}));
-
-
-vi.mock("@agentos/shared/metrics", () => ({
- createRecordMetric: () => vi.fn(),
-}));
-
-vi.mock("@agentos/shared/errors", () => ({
- safeCall: async (fn: Function, fallback: any, _context?: any) => {
- try {
- return await fn();
- } catch {
- return fallback;
- }
- },
-}));
-
-beforeEach(() => {
- resetKv();
- mockTrigger.mockClear();
- mockTriggerVoid.mockClear();
-});
-
-beforeAll(async () => {
- await import("../artifact-dag.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-function authReq(body: any) {
- return { headers: { authorization: "Bearer test" }, body, ...body };
-}
-
-describe("artifact::push", () => {
- it("pushes a root artifact with no parents", async () => {
- const result = await call(
- "artifact::push",
- authReq({ agentId: "agent-1", content: { code: "x+1" }, label: "v1" }),
- );
- expect(result.nodeId).toBeDefined();
- expect(result.contentHash).toBeDefined();
- expect(result.parentIds).toEqual([]);
-
- const stored: any = getScope("artifacts").get(result.nodeId);
- expect(stored.agentId).toBe("agent-1");
- expect(stored.label).toBe("v1");
- expect(stored.content).toEqual({ code: "x+1" });
- });
-
- it("pushes artifact with parent references", async () => {
- seedKv("artifacts", "parent-1", {
- id: "parent-1",
- agentId: "agent-1",
- parentIds: [],
- content: "base",
- label: "root",
- });
-
- const result = await call(
- "artifact::push",
- authReq({
- agentId: "agent-2",
- content: "improved",
- label: "child",
- parentIds: ["parent-1"],
- }),
- );
- expect(result.parentIds).toEqual(["parent-1"]);
- });
-
- it("rejects missing parent", async () => {
- await expect(
- call(
- "artifact::push",
- authReq({
- agentId: "agent-1",
- content: "test",
- label: "test",
- parentIds: ["nonexistent"],
- }),
- ),
- ).rejects.toThrow("Parent artifact nonexistent not found");
- });
-
- it("rejects missing required fields", async () => {
- await expect(
- call("artifact::push", authReq({ agentId: "agent-1" })),
- ).rejects.toThrow("agentId, content, and label are required");
- });
-
- it("publishes event for swarm artifacts", async () => {
- await call(
- "artifact::push",
- authReq({
- agentId: "agent-1",
- content: "data",
- label: "shared",
- swarmId: "swarm-1",
- }),
- );
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "publish",
- expect.objectContaining({
- topic: "artifact:swarm-1",
- }),
- );
- });
-});
-
-describe("artifact::fetch", () => {
- it("fetches existing artifact", async () => {
- seedKv("artifacts", "node-1", {
- id: "node-1",
- agentId: "agent-1",
- content: "hello",
- label: "test",
- });
-
- const result = await call(
- "artifact::fetch",
- authReq({ nodeId: "node-1" }),
- );
- expect(result.id).toBe("node-1");
- expect(result.content).toBe("hello");
- });
-
- it("rejects missing nodeId", async () => {
- await expect(
- call("artifact::fetch", authReq({})),
- ).rejects.toThrow("nodeId is required");
- });
-
- it("rejects nonexistent artifact", async () => {
- await expect(
- call("artifact::fetch", authReq({ nodeId: "nope" })),
- ).rejects.toThrow("Artifact not found");
- });
-});
-
-describe("artifact::children", () => {
- it("returns child nodes", async () => {
- seedKv("artifacts", "root-1", {
- id: "root-1",
- agentId: "agent-1",
- parentIds: [],
- content: "root",
- });
- seedKv("artifacts", "child-1", {
- id: "child-1",
- agentId: "agent-2",
- parentIds: ["root-1"],
- content: "child",
- });
- seedKv("artifacts", "child-2", {
- id: "child-2",
- agentId: "agent-3",
- parentIds: ["root-1"],
- content: "child2",
- });
- seedKv("artifacts", "unrelated", {
- id: "unrelated",
- agentId: "agent-4",
- parentIds: [],
- content: "other",
- });
-
- const result = await call(
- "artifact::children",
- authReq({ nodeId: "root-1" }),
- );
- expect(result).toHaveLength(2);
- });
-
- it("returns empty for leaf node", async () => {
- seedKv("artifacts", "leaf-1", {
- id: "leaf-1",
- agentId: "agent-1",
- parentIds: [],
- content: "leaf",
- });
-
- const result = await call(
- "artifact::children",
- authReq({ nodeId: "leaf-1" }),
- );
- expect(result).toHaveLength(0);
- });
-});
-
-describe("artifact::leaves", () => {
- it("finds frontier nodes", async () => {
- seedKv("artifacts", "a1", {
- id: "a1",
- agentId: "agent-1",
- parentIds: [],
- content: "root",
- });
- seedKv("artifacts", "a2", {
- id: "a2",
- agentId: "agent-1",
- parentIds: ["a1"],
- content: "mid",
- });
- seedKv("artifacts", "a3", {
- id: "a3",
- agentId: "agent-1",
- parentIds: ["a2"],
- content: "leaf",
- });
- seedKv("artifacts", "a4", {
- id: "a4",
- agentId: "agent-2",
- parentIds: ["a1"],
- content: "branch",
- });
-
- const result = await call("artifact::leaves", authReq({}));
- expect(result).toHaveLength(2);
- const ids = result.map((n: any) => n.id);
- expect(ids).toContain("a3");
- expect(ids).toContain("a4");
- });
-
- it("filters by swarmId", async () => {
- seedKv("artifacts", "s1", {
- id: "s1",
- agentId: "agent-1",
- swarmId: "swarm-x",
- parentIds: [],
- content: "a",
- });
- seedKv("artifacts", "s2", {
- id: "s2",
- agentId: "agent-1",
- swarmId: "swarm-y",
- parentIds: [],
- content: "b",
- });
-
- const result = await call(
- "artifact::leaves",
- authReq({ swarmId: "swarm-x" }),
- );
- expect(result).toHaveLength(1);
- expect(result[0].id).toBe("s1");
- });
-});
-
-describe("artifact::diff", () => {
- it("compares two artifacts", async () => {
- seedKv("artifacts", "d1", {
- id: "d1",
- agentId: "agent-1",
- content: "hello world",
- contentHash: "abc123",
- createdAt: 1000,
- });
- seedKv("artifacts", "d2", {
- id: "d2",
- agentId: "agent-2",
- content: "goodbye world",
- contentHash: "def456",
- createdAt: 2000,
- });
-
- const result = await call(
- "artifact::diff",
- authReq({ nodeIdA: "d1", nodeIdB: "d2" }),
- );
- expect(result.contentMatch).toBe(false);
- expect(result.agentA).toBe("agent-1");
- expect(result.agentB).toBe("agent-2");
- expect(result.sizeA).toBeGreaterThan(0);
- expect(result.sizeB).toBeGreaterThan(0);
- });
-
- it("detects matching content", async () => {
- seedKv("artifacts", "m1", {
- id: "m1",
- agentId: "agent-1",
- content: "same",
- contentHash: "aaa",
- createdAt: 1000,
- });
- seedKv("artifacts", "m2", {
- id: "m2",
- agentId: "agent-2",
- content: "same",
- contentHash: "aaa",
- createdAt: 2000,
- });
-
- const result = await call(
- "artifact::diff",
- authReq({ nodeIdA: "m1", nodeIdB: "m2" }),
- );
- expect(result.contentMatch).toBe(true);
- });
-
- it("rejects missing nodeIdB", async () => {
- await expect(
- call("artifact::diff", authReq({ nodeIdA: "x" })),
- ).rejects.toThrow("nodeIdA and nodeIdB are required");
- });
-});
-
-describe("artifact::history", () => {
- it("returns artifacts sorted by recency", async () => {
- seedKv("artifacts", "h1", {
- id: "h1",
- agentId: "agent-1",
- label: "first",
- contentHash: "a",
- parentIds: [],
- createdAt: 1000,
- });
- seedKv("artifacts", "h2", {
- id: "h2",
- agentId: "agent-1",
- label: "second",
- contentHash: "b",
- parentIds: [],
- createdAt: 2000,
- });
- seedKv("artifacts", "h3", {
- id: "h3",
- agentId: "agent-2",
- label: "other",
- contentHash: "c",
- parentIds: [],
- createdAt: 3000,
- });
-
- const result = await call(
- "artifact::history",
- authReq({ agentId: "agent-1" }),
- );
- expect(result).toHaveLength(2);
- expect(result[0].label).toBe("second");
- expect(result[1].label).toBe("first");
- });
-
- it("respects limit", async () => {
- for (let i = 0; i < 5; i++) {
- seedKv("artifacts", `lim-${i}`, {
- id: `lim-${i}`,
- agentId: "agent-1",
- label: `item-${i}`,
- contentHash: `h${i}`,
- parentIds: [],
- createdAt: i * 1000,
- });
- }
-
- const result = await call(
- "artifact::history",
- authReq({ agentId: "agent-1", limit: 2 }),
- );
- expect(result).toHaveLength(2);
- });
-});
diff --git a/src/__tests__/channels-bluesky.test.ts b/src/__tests__/channels-bluesky.test.ts
deleted file mode 100644
index 27038b0..0000000
--- a/src/__tests__/channels-bluesky.test.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-// @ts-nocheck
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- splitMessage: vi.fn((text: string, limit: number) => {
- const chunks: string[] = [];
- for (let i = 0; i < text.length; i += limit)
- chunks.push(text.slice(i, i + limit));
- return chunks.length ? chunks : [text];
- }),
- resolveAgent: vi.fn(async () => "default-agent"),
-}));
-
-const mockFetch = vi.fn(async () => ({
- ok: true,
- json: async () => ({
- text: "Fetched message",
- id: "msg-1",
- accessJwt: "jwt",
- did: "did:plc:test",
- }),
-}));
-vi.stubGlobal("fetch", mockFetch);
-
-beforeEach(() => {
- mockTrigger.mockReset();
- mockTrigger.mockImplementation(
- async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
- },
- );
- mockTriggerVoid.mockClear();
- mockFetch.mockClear();
- mockFetch.mockImplementation(async () => ({
- ok: true,
- json: async () => ({
- text: "Fetched message",
- id: "msg-1",
- accessJwt: "jwt",
- did: "did:plc:test",
- }),
- }));
-});
-
-beforeAll(async () => {
- process.env.BLUESKY_HANDLE = "test.bsky.social";
- process.env.BLUESKY_PASSWORD = "test-password";
- await import("../channels/bluesky.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("channel::bluesky::webhook", () => {
- it("registers the handler", () => {
- expect(handlers["channel::bluesky::webhook"]).toBeDefined();
- });
-
- it("ignores messages without text", async () => {
- const result = await call("channel::bluesky::webhook", {
- body: { did: "did:plc:user1" },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("processes valid mention", async () => {
- const result = await call("channel::bluesky::webhook", {
- body: {
- did: "did:plc:user2",
- text: "Hello Bluesky",
- uri: "at://did:plc:user2/post/1",
- cid: "bafyrei1",
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("routes to agent::chat with bluesky session", async () => {
- await call("channel::bluesky::webhook", {
- body: {
- did: "did:plc:user3",
- text: "Bluesky msg",
- uri: "at://x/post/2",
- cid: "bafyrei2",
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls.length).toBe(1);
- expect(chatCalls[0][1].sessionId).toBe("bluesky:did:plc:user3");
- });
-
- it("calls Bluesky API to send reply", async () => {
- await call("channel::bluesky::webhook", {
- body: {
- did: "did:plc:user4",
- text: "Auth test",
- uri: "at://x/post/3",
- cid: "bafyrei3",
- },
- });
- expect(mockFetch).toHaveBeenCalled();
- });
-
- it("sends reply as post via AT Protocol", async () => {
- await call("channel::bluesky::webhook", {
- body: {
- did: "did:plc:user5",
- text: "Post test",
- uri: "at://x/post/4",
- cid: "bafyrei4",
- },
- });
- const createCalls = (mockFetch.mock.calls as any[][]).filter((c) =>
- (c[0] as string).includes("createRecord"),
- );
- expect(createCalls.length).toBeGreaterThanOrEqual(1);
- });
-
- it("emits audit event", async () => {
- await call("channel::bluesky::webhook", {
- body: {
- did: "did:plc:user6",
- text: "Audit bsky",
- uri: "at://x/post/5",
- cid: "bafyrei5",
- },
- });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "security::audit",
- expect.objectContaining({
- detail: expect.objectContaining({ channel: "bluesky" }),
- }),
- );
- });
-});
diff --git a/src/__tests__/channels-gchat-line-messenger.test.ts b/src/__tests__/channels-gchat-line-messenger.test.ts
deleted file mode 100644
index 1174522..0000000
--- a/src/__tests__/channels-gchat-line-messenger.test.ts
+++ /dev/null
@@ -1,481 +0,0 @@
-// @ts-nocheck
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- splitMessage: vi.fn((text: string, limit: number) => {
- const chunks: string[] = [];
- for (let i = 0; i < text.length; i += limit)
- chunks.push(text.slice(i, i + limit));
- return chunks.length ? chunks : [text];
- }),
- resolveAgent: vi.fn(async () => "default-agent"),
-}));
-
-const mockFetch = vi.fn(async () => ({ ok: true, json: async () => ({}) }));
-vi.stubGlobal("fetch", mockFetch);
-
-beforeEach(() => {
- mockTrigger.mockReset();
- mockTrigger.mockImplementation(
- async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
- },
- );
- mockTriggerVoid.mockClear();
- mockFetch.mockClear();
-});
-
-beforeAll(async () => {
- process.env.GOOGLE_CHAT_TOKEN = "test-gchat-token";
- process.env.LINE_CHANNEL_TOKEN = "test-line-token";
- process.env.MESSENGER_PAGE_TOKEN = "test-page-token";
- process.env.MESSENGER_VERIFY_TOKEN = "test-verify-token";
- await import("../channels/google-chat.js");
- await import("../channels/line.js");
- await import("../channels/messenger.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("channel::google-chat::webhook", () => {
- it("registers the handler", () => {
- expect(handlers["channel::google-chat::webhook"]).toBeDefined();
- });
-
- it("ignores non-MESSAGE events", async () => {
- const result = await call("channel::google-chat::webhook", {
- body: { type: "ADDED_TO_SPACE" },
- headers: { Authorization: "Bearer test-gchat-token" },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.ok).toBe(true);
- });
-
- it("processes MESSAGE event", async () => {
- const result = await call("channel::google-chat::webhook", {
- body: {
- type: "MESSAGE",
- space: { name: "spaces/abc" },
- message: { text: "Hello Google Chat" },
- user: { name: "users/u1" },
- },
- headers: { Authorization: "Bearer test-gchat-token" },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.text).toBe("Reply");
- });
-
- it("routes to agent::chat with google-chat session", async () => {
- await call("channel::google-chat::webhook", {
- body: {
- type: "MESSAGE",
- space: { name: "spaces/room-1" },
- message: { text: "Routing test" },
- user: { name: "users/u2" },
- },
- headers: { Authorization: "Bearer test-gchat-token" },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls.length).toBe(1);
- expect(chatCalls[0][1].sessionId).toBe("google-chat:spaces/room-1");
- expect(chatCalls[0][1].message).toBe("Routing test");
- });
-
- it("emits audit event", async () => {
- await call("channel::google-chat::webhook", {
- body: {
- type: "MESSAGE",
- space: { name: "spaces/audit" },
- message: { text: "Audit" },
- user: { name: "users/u3" },
- },
- headers: { Authorization: "Bearer test-gchat-token" },
- });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "security::audit",
- expect.objectContaining({
- detail: expect.objectContaining({
- channel: "google-chat",
- spaceId: "spaces/audit",
- userId: "users/u3",
- }),
- }),
- );
- });
-
- it("returns response text in body", async () => {
- mockTrigger.mockImplementation(async (fnId: string) => {
- if (fnId === "agent::chat") return { content: "Custom response" };
- return null;
- });
- const result = await call("channel::google-chat::webhook", {
- body: {
- type: "MESSAGE",
- space: { name: "spaces/resp" },
- message: { text: "Get response" },
- user: { name: "users/u4" },
- },
- headers: { Authorization: "Bearer test-gchat-token" },
- });
- expect(result.body.text).toBe("Custom response");
- });
-
- it("handles missing message text gracefully", async () => {
- const result = await call("channel::google-chat::webhook", {
- body: {
- type: "MESSAGE",
- space: { name: "spaces/empty" },
- message: {},
- user: { name: "users/u5" },
- },
- headers: { Authorization: "Bearer test-gchat-token" },
- });
- expect(result.status_code).toBe(200);
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].message).toBe("");
- });
-});
-
-describe("channel::line::webhook", () => {
- it("registers the handler", () => {
- expect(handlers["channel::line::webhook"]).toBeDefined();
- });
-
- it("ignores non-message events", async () => {
- const result = await call("channel::line::webhook", {
- body: { events: [{ type: "follow" }] },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.ok).toBe(true);
- });
-
- it("ignores non-text message types", async () => {
- const result = await call("channel::line::webhook", {
- body: {
- events: [
- {
- type: "message",
- message: { type: "image" },
- source: { userId: "u1" },
- replyToken: "rt-1",
- },
- ],
- },
- });
- expect(result.status_code).toBe(200);
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls.length).toBe(0);
- });
-
- it("processes text message event", async () => {
- const result = await call("channel::line::webhook", {
- body: {
- events: [
- {
- type: "message",
- message: { type: "text", text: "Hello LINE" },
- source: { userId: "line-user-1" },
- replyToken: "rt-2",
- },
- ],
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("routes to agent::chat with line session", async () => {
- await call("channel::line::webhook", {
- body: {
- events: [
- {
- type: "message",
- message: { type: "text", text: "Route test" },
- source: { userId: "line-u2" },
- replyToken: "rt-3",
- },
- ],
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls.length).toBe(1);
- expect(chatCalls[0][1].sessionId).toBe("line:line-u2");
- expect(chatCalls[0][1].message).toBe("Route test");
- });
-
- it("sends reply via LINE API", async () => {
- await call("channel::line::webhook", {
- body: {
- events: [
- {
- type: "message",
- message: { type: "text", text: "API test" },
- source: { userId: "line-u3" },
- replyToken: "rt-4",
- },
- ],
- },
- });
- expect(mockFetch).toHaveBeenCalledWith(
- expect.stringContaining("api.line.me/v2/bot/message/reply"),
- expect.objectContaining({ method: "POST" }),
- );
- });
-
- it("uses correct LINE auth header", async () => {
- await call("channel::line::webhook", {
- body: {
- events: [
- {
- type: "message",
- message: { type: "text", text: "Auth test" },
- source: { userId: "line-u4" },
- replyToken: "rt-5",
- },
- ],
- },
- });
- const fetchCall = mockFetch.mock.calls[0];
- const headers = (fetchCall[1] as any).headers;
- expect(headers.Authorization).toBe("Bearer test-line-token");
- });
-
- it("uses groupId when userId is absent", async () => {
- await call("channel::line::webhook", {
- body: {
- events: [
- {
- type: "message",
- message: { type: "text", text: "Group msg" },
- source: { groupId: "group-g1" },
- replyToken: "rt-6",
- },
- ],
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].sessionId).toBe("line:group-g1");
- });
-
- it("emits audit event", async () => {
- await call("channel::line::webhook", {
- body: {
- events: [
- {
- type: "message",
- message: { type: "text", text: "Audit" },
- source: { userId: "line-u5" },
- replyToken: "rt-7",
- },
- ],
- },
- });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "security::audit",
- expect.objectContaining({
- detail: expect.objectContaining({
- channel: "line",
- userId: "line-u5",
- }),
- }),
- );
- });
-
- it("handles empty events array", async () => {
- const result = await call("channel::line::webhook", {
- body: { events: [] },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.ok).toBe(true);
- });
-});
-
-describe("channel::messenger::webhook", () => {
- it("registers the handler", () => {
- expect(handlers["channel::messenger::webhook"]).toBeDefined();
- });
-
- it("handles subscription verification", async () => {
- const result = await call("channel::messenger::webhook", {
- body: {
- "hub.mode": "subscribe",
- "hub.verify_token": "test-verify-token",
- "hub.challenge": "challenge-abc",
- },
- });
- expect(result.status_code).toBe(200);
- expect(result.body).toBe("challenge-abc");
- });
-
- it("rejects incorrect verify token", async () => {
- const result = await call("channel::messenger::webhook", {
- body: {
- "hub.mode": "subscribe",
- "hub.verify_token": "wrong-token",
- "hub.challenge": "challenge-xyz",
- },
- });
- expect(result.status_code).toBe(403);
- });
-
- it("ignores messages without text", async () => {
- const result = await call("channel::messenger::webhook", {
- body: {
- entry: [{ messaging: [{ sender: { id: "s1" }, message: {} }] }],
- },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.ok).toBe(true);
- });
-
- it("processes text message", async () => {
- const result = await call("channel::messenger::webhook", {
- body: {
- entry: [
- {
- messaging: [
- {
- sender: { id: "sender-1" },
- message: { text: "Hello Messenger" },
- },
- ],
- },
- ],
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("routes to agent::chat with messenger session", async () => {
- await call("channel::messenger::webhook", {
- body: {
- entry: [
- {
- messaging: [
- {
- sender: { id: "sender-2" },
- message: { text: "Route test" },
- },
- ],
- },
- ],
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls.length).toBe(1);
- expect(chatCalls[0][1].sessionId).toBe("messenger:sender-2");
- expect(chatCalls[0][1].message).toBe("Route test");
- });
-
- it("sends reply via Graph API", async () => {
- await call("channel::messenger::webhook", {
- body: {
- entry: [
- {
- messaging: [
- {
- sender: { id: "sender-3" },
- message: { text: "API test" },
- },
- ],
- },
- ],
- },
- });
- expect(mockFetch).toHaveBeenCalledWith(
- expect.stringContaining("graph.facebook.com"),
- expect.objectContaining({ method: "POST" }),
- );
- });
-
- it("sends page token in Authorization header", async () => {
- await call("channel::messenger::webhook", {
- body: {
- entry: [
- {
- messaging: [
- {
- sender: { id: "sender-4" },
- message: { text: "Token test" },
- },
- ],
- },
- ],
- },
- });
- const fetchOpts = mockFetch.mock.calls[0][1] as any;
- expect(fetchOpts.headers.Authorization).toBe("Bearer test-page-token");
- });
-
- it("emits audit event", async () => {
- await call("channel::messenger::webhook", {
- body: {
- entry: [
- {
- messaging: [
- {
- sender: { id: "sender-5" },
- message: { text: "Audit" },
- },
- ],
- },
- ],
- },
- });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "security::audit",
- expect.objectContaining({
- detail: expect.objectContaining({
- channel: "messenger",
- senderId: "sender-5",
- }),
- }),
- );
- });
-
- it("handles empty entry array", async () => {
- const result = await call("channel::messenger::webhook", {
- body: { entry: [] },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.ok).toBe(true);
- });
-});
diff --git a/src/__tests__/channels-mastodon.test.ts b/src/__tests__/channels-mastodon.test.ts
deleted file mode 100644
index d461601..0000000
--- a/src/__tests__/channels-mastodon.test.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-// @ts-nocheck
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- splitMessage: vi.fn((text: string, limit: number) => {
- const chunks: string[] = [];
- for (let i = 0; i < text.length; i += limit)
- chunks.push(text.slice(i, i + limit));
- return chunks.length ? chunks : [text];
- }),
- resolveAgent: vi.fn(async () => "default-agent"),
-}));
-
-const mockFetch = vi.fn(async () => ({
- ok: true,
- json: async () => ({ access_token: "test-token", id: "status-1" }),
-}));
-vi.stubGlobal("fetch", mockFetch);
-
-beforeEach(() => {
- mockTrigger.mockReset();
- mockTrigger.mockImplementation(
- async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
- },
- );
- mockTriggerVoid.mockClear();
- mockFetch.mockClear();
- mockFetch.mockImplementation(async () => ({
- ok: true,
- json: async () => ({ access_token: "test-token", id: "status-1" }),
- }));
-});
-
-beforeAll(async () => {
- process.env.MASTODON_INSTANCE = "https://mastodon.social";
- process.env.MASTODON_TOKEN = "test-masto-token";
- await import("../channels/mastodon.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("channel::mastodon::webhook", () => {
- it("registers the handler", () => {
- expect(handlers["channel::mastodon::webhook"]).toBeDefined();
- });
-
- it("ignores messages without status content", async () => {
- const result = await call("channel::mastodon::webhook", {
- body: { account: { acct: "user@masto.social" } },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("processes valid mention", async () => {
- const result = await call("channel::mastodon::webhook", {
- body: {
- account: { acct: "tester@masto.social" },
- status: { content: "Hello Mastodon
", id: "111222" },
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("strips HTML tags from content", async () => {
- await call("channel::mastodon::webhook", {
- body: {
- account: { acct: "html@masto.social" },
- status: { content: "Plain text here
", id: "333" },
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].message).toBe("Plain text here");
- });
-
- it("routes to agent::chat with mastodon session", async () => {
- await call("channel::mastodon::webhook", {
- body: {
- account: { acct: "session@masto.social" },
- status: { content: "Session test
", id: "444" },
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].sessionId).toBe("mastodon:session@masto.social");
- });
-
- it("uses account.id as fallback for session", async () => {
- await call("channel::mastodon::webhook", {
- body: {
- account: { id: "12345" },
- status: { content: "ID fallback
", id: "555" },
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].sessionId).toBe("mastodon:12345");
- });
-
- it("sends reply as mastodon status", async () => {
- await call("channel::mastodon::webhook", {
- body: {
- account: { acct: "reply@masto.social" },
- status: { content: "Reply
", id: "666" },
- },
- });
- const statusCalls = mockFetch.mock.calls.filter(
- (c) =>
- (c[0] as string).includes("api/v1/statuses") &&
- (c[1] as any)?.method === "POST",
- );
- expect(statusCalls.length).toBeGreaterThanOrEqual(1);
- });
-
- it("sends reply as reply to original status", async () => {
- await call("channel::mastodon::webhook", {
- body: {
- account: { acct: "thread@masto.social" },
- status: { content: "Thread reply
", id: "777" },
- },
- });
- const body = JSON.parse(
- mockFetch.mock.calls.find(
- (c) =>
- (c[1] as any)?.method === "POST" &&
- (c[0] as string).includes("statuses"),
- )?.[1]?.body as string,
- );
- expect(body.in_reply_to_id).toBe("777");
- });
-
- it("emits audit event", async () => {
- await call("channel::mastodon::webhook", {
- body: {
- account: { acct: "audit@masto.social" },
- status: { content: "Audit
", id: "888" },
- },
- });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "security::audit",
- expect.objectContaining({
- detail: expect.objectContaining({ channel: "mastodon" }),
- }),
- );
- });
-});
diff --git a/src/__tests__/channels-matrix.test.ts b/src/__tests__/channels-matrix.test.ts
deleted file mode 100644
index 38bbfd4..0000000
--- a/src/__tests__/channels-matrix.test.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-// @ts-nocheck
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- splitMessage: vi.fn((text: string) => [text]),
- resolveAgent: vi.fn(async () => "default-agent"),
-}));
-
-const mockFetch = vi.fn(async () => ({
- ok: true,
- json: async () => ({ access_token: "test-token-123" }),
-}));
-vi.stubGlobal("fetch", mockFetch);
-
-beforeEach(() => {
- mockTrigger.mockReset();
- mockTrigger.mockImplementation(async (fnId: string): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
- });
- mockTriggerVoid.mockClear();
- mockFetch.mockClear();
- mockFetch.mockImplementation(async () => ({
- ok: true,
- json: async () => ({ access_token: "test-token-123" }),
- }));
-});
-
-describe("Matrix channel", () => {
- beforeAll(async () => {
- process.env.MATRIX_HOMESERVER = "https://matrix.example.org";
- process.env.MATRIX_TOKEN = "test-matrix-token";
- await import("../channels/matrix.js");
- });
-
- it("registers channel::matrix::webhook", () => {
- expect(handlers["channel::matrix::webhook"]).toBeDefined();
- });
-
- it("processes m.room.message event", async () => {
- const result = await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!room1:matrix.org",
- content: { body: "Hello Matrix" },
- sender: "@user:matrix.org",
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("routes to agent::chat", async () => {
- await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!room2:matrix.org",
- content: { body: "Matrix msg" },
- sender: "@user2:matrix.org",
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(c => c[0] === "agent::chat");
- expect(chatCalls.length).toBeGreaterThanOrEqual(1);
- });
-
- it("uses room_id for session", async () => {
- await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!sess-room:matrix.org",
- content: { body: "Session" },
- sender: "@user3:matrix.org",
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(c => c[0] === "agent::chat");
- const lastChat = chatCalls[chatCalls.length - 1];
- expect(lastChat[1].sessionId).toBe("matrix:!sess-room:matrix.org");
- });
-
- it("ignores non-message events", async () => {
- const result = await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.member",
- room_id: "!room3:matrix.org",
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("ignores empty body content", async () => {
- const result = await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!room4:matrix.org",
- content: {},
- sender: "@user4:matrix.org",
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("sends reply via Matrix PUT API", async () => {
- await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!reply-room:matrix.org",
- content: { body: "Reply test" },
- sender: "@user5:matrix.org",
- },
- });
- expect(mockFetch).toHaveBeenCalledWith(
- expect.stringContaining("/_matrix/client/v3/rooms/"),
- expect.objectContaining({ method: "PUT" }),
- );
- });
-
- it("sends m.text msgtype", async () => {
- await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!type-room:matrix.org",
- content: { body: "Type" },
- sender: "@user6:matrix.org",
- },
- });
- const putCall = mockFetch.mock.calls.find(c => c[1]?.method === "PUT");
- if (putCall) {
- const body = JSON.parse(putCall[1].body);
- expect(body.msgtype).toBe("m.text");
- }
- });
-
- it("audits channel message", async () => {
- await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!audit-room:matrix.org",
- content: { body: "Audit" },
- sender: "@audit-user:matrix.org",
- },
- });
- const auditCalls = mockTriggerVoid.mock.calls.filter(c => c[0] === "security::audit");
- expect(auditCalls.some(c => c[1].detail?.channel === "matrix")).toBe(true);
- });
-
- it("includes bearer token in authorization", async () => {
- await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!auth-room:matrix.org",
- content: { body: "Auth check" },
- sender: "@auth-user:matrix.org",
- },
- });
- const putCall = mockFetch.mock.calls.find(c => c[1]?.method === "PUT");
- if (putCall) {
- expect(putCall[1].headers.Authorization).toBe("Bearer test-matrix-token");
- }
- });
-});
diff --git a/src/__tests__/channels-mattermost.test.ts b/src/__tests__/channels-mattermost.test.ts
deleted file mode 100644
index 87588fd..0000000
--- a/src/__tests__/channels-mattermost.test.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-// @ts-nocheck
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- splitMessage: vi.fn((text: string) => [text]),
- resolveAgent: vi.fn(async () => "default-agent"),
-}));
-
-const mockFetch = vi.fn(async () => ({ ok: true, json: async () => ({}) }));
-vi.stubGlobal("fetch", mockFetch);
-
-beforeEach(() => {
- mockTrigger.mockReset();
- mockTrigger.mockImplementation(async (fnId: string): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
- });
- mockTriggerVoid.mockClear();
- mockFetch.mockClear();
-});
-
-describe("Mattermost channel", () => {
- beforeAll(async () => {
- process.env.MATTERMOST_URL = "https://mattermost.example.com";
- process.env.MATTERMOST_TOKEN = "test-mm-token";
- await import("../channels/mattermost.js");
- });
-
- it("registers channel::mattermost::webhook", () => {
- expect(handlers["channel::mattermost::webhook"]).toBeDefined();
- });
-
- it("processes outgoing webhook", async () => {
- const result = await handlers["channel::mattermost::webhook"]({
- body: {
- channel_id: "ch-1",
- user_id: "user-1",
- text: "Hello MM",
- post_id: "post-1",
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("routes to agent::chat", async () => {
- await handlers["channel::mattermost::webhook"]({
- body: {
- channel_id: "ch-2",
- user_id: "user-2",
- text: "MM message",
- post_id: "post-2",
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(c => c[0] === "agent::chat");
- expect(chatCalls.length).toBeGreaterThanOrEqual(1);
- });
-
- it("uses channel_id for session", async () => {
- await handlers["channel::mattermost::webhook"]({
- body: {
- channel_id: "mm-sess",
- user_id: "user-3",
- text: "Session test",
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(c => c[0] === "agent::chat");
- const lastChat = chatCalls[chatCalls.length - 1];
- expect(lastChat[1].sessionId).toBe("mattermost:mm-sess");
- });
-
- it("sends reply via Mattermost API", async () => {
- await handlers["channel::mattermost::webhook"]({
- body: {
- channel_id: "ch-reply",
- user_id: "user-4",
- text: "Reply test",
- post_id: "post-reply",
- },
- });
- expect(mockFetch).toHaveBeenCalledWith(
- "https://mattermost.example.com/api/v4/posts",
- expect.objectContaining({ method: "POST" }),
- );
- });
-
- it("includes root_id for threading", async () => {
- await handlers["channel::mattermost::webhook"]({
- body: {
- channel_id: "ch-thread",
- user_id: "user-5",
- text: "Thread test",
- post_id: "root-post-1",
- },
- });
- const fetchCall = mockFetch.mock.calls.find(c => c[0]?.includes("api/v4/posts"));
- if (fetchCall) {
- const body = JSON.parse(fetchCall[1].body);
- expect(body.root_id).toBe("root-post-1");
- }
- });
-
- it("sends bearer token", async () => {
- await handlers["channel::mattermost::webhook"]({
- body: {
- channel_id: "ch-auth",
- user_id: "user-6",
- text: "Auth test",
- },
- });
- const fetchCall = mockFetch.mock.calls.find(c => c[0]?.includes("api/v4/posts"));
- if (fetchCall) {
- expect(fetchCall[1].headers.Authorization).toBe("Bearer test-mm-token");
- }
- });
-
- it("ignores empty text", async () => {
- const result = await handlers["channel::mattermost::webhook"]({
- body: {
- channel_id: "ch-empty",
- user_id: "user-7",
- text: "",
- },
- });
- expect(result.status_code).toBe(200);
- const chatCalls = mockTrigger.mock.calls.filter(c => c[0] === "agent::chat");
- expect(chatCalls.length).toBe(0);
- });
-
- it("audits channel message", async () => {
- await handlers["channel::mattermost::webhook"]({
- body: {
- channel_id: "ch-audit",
- user_id: "user-audit",
- text: "Audit MM",
- },
- });
- const auditCalls = mockTriggerVoid.mock.calls.filter(c => c[0] === "security::audit");
- expect(auditCalls.some(c => c[1].detail?.channel === "mattermost")).toBe(true);
- });
-});
diff --git a/src/__tests__/channels-reddit.test.ts b/src/__tests__/channels-reddit.test.ts
deleted file mode 100644
index d461601..0000000
--- a/src/__tests__/channels-reddit.test.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-// @ts-nocheck
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- splitMessage: vi.fn((text: string, limit: number) => {
- const chunks: string[] = [];
- for (let i = 0; i < text.length; i += limit)
- chunks.push(text.slice(i, i + limit));
- return chunks.length ? chunks : [text];
- }),
- resolveAgent: vi.fn(async () => "default-agent"),
-}));
-
-const mockFetch = vi.fn(async () => ({
- ok: true,
- json: async () => ({ access_token: "test-token", id: "status-1" }),
-}));
-vi.stubGlobal("fetch", mockFetch);
-
-beforeEach(() => {
- mockTrigger.mockReset();
- mockTrigger.mockImplementation(
- async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
- },
- );
- mockTriggerVoid.mockClear();
- mockFetch.mockClear();
- mockFetch.mockImplementation(async () => ({
- ok: true,
- json: async () => ({ access_token: "test-token", id: "status-1" }),
- }));
-});
-
-beforeAll(async () => {
- process.env.MASTODON_INSTANCE = "https://mastodon.social";
- process.env.MASTODON_TOKEN = "test-masto-token";
- await import("../channels/mastodon.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("channel::mastodon::webhook", () => {
- it("registers the handler", () => {
- expect(handlers["channel::mastodon::webhook"]).toBeDefined();
- });
-
- it("ignores messages without status content", async () => {
- const result = await call("channel::mastodon::webhook", {
- body: { account: { acct: "user@masto.social" } },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("processes valid mention", async () => {
- const result = await call("channel::mastodon::webhook", {
- body: {
- account: { acct: "tester@masto.social" },
- status: { content: "Hello Mastodon
", id: "111222" },
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("strips HTML tags from content", async () => {
- await call("channel::mastodon::webhook", {
- body: {
- account: { acct: "html@masto.social" },
- status: { content: "Plain text here
", id: "333" },
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].message).toBe("Plain text here");
- });
-
- it("routes to agent::chat with mastodon session", async () => {
- await call("channel::mastodon::webhook", {
- body: {
- account: { acct: "session@masto.social" },
- status: { content: "Session test
", id: "444" },
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].sessionId).toBe("mastodon:session@masto.social");
- });
-
- it("uses account.id as fallback for session", async () => {
- await call("channel::mastodon::webhook", {
- body: {
- account: { id: "12345" },
- status: { content: "ID fallback
", id: "555" },
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].sessionId).toBe("mastodon:12345");
- });
-
- it("sends reply as mastodon status", async () => {
- await call("channel::mastodon::webhook", {
- body: {
- account: { acct: "reply@masto.social" },
- status: { content: "Reply
", id: "666" },
- },
- });
- const statusCalls = mockFetch.mock.calls.filter(
- (c) =>
- (c[0] as string).includes("api/v1/statuses") &&
- (c[1] as any)?.method === "POST",
- );
- expect(statusCalls.length).toBeGreaterThanOrEqual(1);
- });
-
- it("sends reply as reply to original status", async () => {
- await call("channel::mastodon::webhook", {
- body: {
- account: { acct: "thread@masto.social" },
- status: { content: "Thread reply
", id: "777" },
- },
- });
- const body = JSON.parse(
- mockFetch.mock.calls.find(
- (c) =>
- (c[1] as any)?.method === "POST" &&
- (c[0] as string).includes("statuses"),
- )?.[1]?.body as string,
- );
- expect(body.in_reply_to_id).toBe("777");
- });
-
- it("emits audit event", async () => {
- await call("channel::mastodon::webhook", {
- body: {
- account: { acct: "audit@masto.social" },
- status: { content: "Audit
", id: "888" },
- },
- });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "security::audit",
- expect.objectContaining({
- detail: expect.objectContaining({ channel: "mastodon" }),
- }),
- );
- });
-});
diff --git a/src/__tests__/channels-teams.test.ts b/src/__tests__/channels-teams.test.ts
deleted file mode 100644
index 38bbfd4..0000000
--- a/src/__tests__/channels-teams.test.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-// @ts-nocheck
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- splitMessage: vi.fn((text: string) => [text]),
- resolveAgent: vi.fn(async () => "default-agent"),
-}));
-
-const mockFetch = vi.fn(async () => ({
- ok: true,
- json: async () => ({ access_token: "test-token-123" }),
-}));
-vi.stubGlobal("fetch", mockFetch);
-
-beforeEach(() => {
- mockTrigger.mockReset();
- mockTrigger.mockImplementation(async (fnId: string): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
- });
- mockTriggerVoid.mockClear();
- mockFetch.mockClear();
- mockFetch.mockImplementation(async () => ({
- ok: true,
- json: async () => ({ access_token: "test-token-123" }),
- }));
-});
-
-describe("Matrix channel", () => {
- beforeAll(async () => {
- process.env.MATRIX_HOMESERVER = "https://matrix.example.org";
- process.env.MATRIX_TOKEN = "test-matrix-token";
- await import("../channels/matrix.js");
- });
-
- it("registers channel::matrix::webhook", () => {
- expect(handlers["channel::matrix::webhook"]).toBeDefined();
- });
-
- it("processes m.room.message event", async () => {
- const result = await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!room1:matrix.org",
- content: { body: "Hello Matrix" },
- sender: "@user:matrix.org",
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("routes to agent::chat", async () => {
- await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!room2:matrix.org",
- content: { body: "Matrix msg" },
- sender: "@user2:matrix.org",
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(c => c[0] === "agent::chat");
- expect(chatCalls.length).toBeGreaterThanOrEqual(1);
- });
-
- it("uses room_id for session", async () => {
- await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!sess-room:matrix.org",
- content: { body: "Session" },
- sender: "@user3:matrix.org",
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(c => c[0] === "agent::chat");
- const lastChat = chatCalls[chatCalls.length - 1];
- expect(lastChat[1].sessionId).toBe("matrix:!sess-room:matrix.org");
- });
-
- it("ignores non-message events", async () => {
- const result = await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.member",
- room_id: "!room3:matrix.org",
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("ignores empty body content", async () => {
- const result = await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!room4:matrix.org",
- content: {},
- sender: "@user4:matrix.org",
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("sends reply via Matrix PUT API", async () => {
- await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!reply-room:matrix.org",
- content: { body: "Reply test" },
- sender: "@user5:matrix.org",
- },
- });
- expect(mockFetch).toHaveBeenCalledWith(
- expect.stringContaining("/_matrix/client/v3/rooms/"),
- expect.objectContaining({ method: "PUT" }),
- );
- });
-
- it("sends m.text msgtype", async () => {
- await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!type-room:matrix.org",
- content: { body: "Type" },
- sender: "@user6:matrix.org",
- },
- });
- const putCall = mockFetch.mock.calls.find(c => c[1]?.method === "PUT");
- if (putCall) {
- const body = JSON.parse(putCall[1].body);
- expect(body.msgtype).toBe("m.text");
- }
- });
-
- it("audits channel message", async () => {
- await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!audit-room:matrix.org",
- content: { body: "Audit" },
- sender: "@audit-user:matrix.org",
- },
- });
- const auditCalls = mockTriggerVoid.mock.calls.filter(c => c[0] === "security::audit");
- expect(auditCalls.some(c => c[1].detail?.channel === "matrix")).toBe(true);
- });
-
- it("includes bearer token in authorization", async () => {
- await handlers["channel::matrix::webhook"]({
- body: {
- type: "m.room.message",
- room_id: "!auth-room:matrix.org",
- content: { body: "Auth check" },
- sender: "@auth-user:matrix.org",
- },
- });
- const putCall = mockFetch.mock.calls.find(c => c[1]?.method === "PUT");
- if (putCall) {
- expect(putCall[1].headers.Authorization).toBe("Bearer test-matrix-token");
- }
- });
-});
diff --git a/src/__tests__/channels-viber-feishu-dingtalk.test.ts b/src/__tests__/channels-viber-feishu-dingtalk.test.ts
deleted file mode 100644
index bb4d21e..0000000
--- a/src/__tests__/channels-viber-feishu-dingtalk.test.ts
+++ /dev/null
@@ -1,445 +0,0 @@
-// @ts-nocheck
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- splitMessage: vi.fn((text: string, limit: number) => {
- const chunks: string[] = [];
- for (let i = 0; i < text.length; i += limit)
- chunks.push(text.slice(i, i + limit));
- return chunks.length ? chunks : [text];
- }),
- resolveAgent: vi.fn(async () => "default-agent"),
-}));
-
-vi.mock("crypto", async () => {
- const actual = await vi.importActual("crypto");
- return {
- ...actual,
- createHmac: (algo: string, key: string) => ({
- update: (data: string) => ({
- digest: (enc: string) => "mock-signature-base64",
- }),
- }),
- };
-});
-
-const mockFetch = vi.fn(
- async () =>
- ({
- ok: true,
- json: async () => ({ code: 0, tenant_access_token: "mock-tenant-token" }),
- }) as any,
-);
-vi.stubGlobal("fetch", mockFetch);
-
-beforeEach(() => {
- mockTrigger.mockReset();
- mockTrigger.mockImplementation(
- async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
- },
- );
- mockTriggerVoid.mockClear();
- mockFetch.mockClear();
- mockFetch.mockImplementation(
- async () =>
- ({
- ok: true,
- json: async () => ({
- code: 0,
- tenant_access_token: "mock-tenant-token",
- }),
- }) as any,
- );
-});
-
-beforeAll(async () => {
- process.env.VIBER_TOKEN = "test-viber-token";
- process.env.FEISHU_APP_ID = "test-feishu-app-id";
- process.env.FEISHU_APP_SECRET = "test-feishu-secret";
- process.env.DINGTALK_TOKEN = "test-dingtalk-token";
- process.env.DINGTALK_SECRET = "test-dingtalk-secret";
- await import("../channels/viber.js");
- await import("../channels/feishu.js");
- await import("../channels/dingtalk.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("channel::viber::webhook", () => {
- it("registers the handler", () => {
- expect(handlers["channel::viber::webhook"]).toBeDefined();
- });
-
- it("ignores non-message events", async () => {
- const result = await call("channel::viber::webhook", {
- body: { event: "conversation_started" },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.ok).toBe(true);
- });
-
- it("ignores messages without text", async () => {
- const result = await call("channel::viber::webhook", {
- body: { event: "message", sender: { id: "v1" }, message: {} },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.ok).toBe(true);
- });
-
- it("ignores messages without sender id", async () => {
- const result = await call("channel::viber::webhook", {
- body: { event: "message", sender: {}, message: { text: "Hi" } },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.ok).toBe(true);
- });
-
- it("processes message event", async () => {
- const result = await call("channel::viber::webhook", {
- body: {
- event: "message",
- sender: { id: "viber-u1" },
- message: { text: "Hello Viber" },
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("routes to agent::chat with viber session", async () => {
- await call("channel::viber::webhook", {
- body: {
- event: "message",
- sender: { id: "viber-u2" },
- message: { text: "Route test" },
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls.length).toBe(1);
- expect(chatCalls[0][1].sessionId).toBe("viber:viber-u2");
- expect(chatCalls[0][1].message).toBe("Route test");
- });
-
- it("sends reply via Viber API", async () => {
- await call("channel::viber::webhook", {
- body: {
- event: "message",
- sender: { id: "viber-u3" },
- message: { text: "API test" },
- },
- });
- expect(mockFetch).toHaveBeenCalledWith(
- expect.stringContaining("chatapi.viber.com"),
- expect.objectContaining({ method: "POST" }),
- );
- });
-
- it("uses correct Viber auth header", async () => {
- await call("channel::viber::webhook", {
- body: {
- event: "message",
- sender: { id: "viber-u4" },
- message: { text: "Auth test" },
- },
- });
- const fetchCall = mockFetch.mock.calls[0];
- const headers = (fetchCall[1] as any).headers;
- expect(headers["X-Viber-Auth-Token"]).toBe("test-viber-token");
- });
-
- it("emits audit event", async () => {
- await call("channel::viber::webhook", {
- body: {
- event: "message",
- sender: { id: "viber-u5" },
- message: { text: "Audit" },
- },
- });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "security::audit",
- expect.objectContaining({
- detail: expect.objectContaining({
- channel: "viber",
- userId: "viber-u5",
- }),
- }),
- );
- });
-});
-
-describe("channel::feishu::webhook", () => {
- it("registers the handler", () => {
- expect(handlers["channel::feishu::webhook"]).toBeDefined();
- });
-
- it("handles challenge verification", async () => {
- const result = await call("channel::feishu::webhook", {
- body: { challenge: "feishu-challenge-123" },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.challenge).toBe("feishu-challenge-123");
- });
-
- it("ignores events without message content", async () => {
- const result = await call("channel::feishu::webhook", {
- body: { event: { message: {} } },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.ok).toBe(true);
- });
-
- it("processes message event", async () => {
- const result = await call("channel::feishu::webhook", {
- body: {
- event: {
- message: {
- chat_id: "chat-1",
- content: JSON.stringify({ text: "Hello Feishu" }),
- },
- sender: { sender_id: { user_id: "feishu-u1" } },
- },
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("routes to agent::chat with feishu session", async () => {
- await call("channel::feishu::webhook", {
- body: {
- event: {
- message: {
- chat_id: "chat-2",
- content: JSON.stringify({ text: "Route test" }),
- },
- sender: { sender_id: { user_id: "feishu-u2" } },
- },
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls.length).toBe(1);
- expect(chatCalls[0][1].sessionId).toBe("feishu:chat-2");
- expect(chatCalls[0][1].message).toBe("Route test");
- });
-
- it("fetches tenant token for API calls", async () => {
- await call("channel::feishu::webhook", {
- body: {
- event: {
- message: {
- chat_id: "chat-3",
- content: JSON.stringify({ text: "Token test" }),
- },
- sender: { sender_id: { user_id: "feishu-u3" } },
- },
- },
- });
- expect(mockFetch).toHaveBeenCalled();
- });
-
- it("sends reply via Feishu API", async () => {
- await call("channel::feishu::webhook", {
- body: {
- event: {
- message: {
- chat_id: "chat-4",
- content: JSON.stringify({ text: "API test" }),
- },
- sender: { sender_id: { user_id: "feishu-u4" } },
- },
- },
- });
- const apiCalls = mockFetch.mock.calls.filter((c) =>
- (c[0] as string).includes("feishu.cn"),
- );
- expect(apiCalls.length).toBeGreaterThan(0);
- });
-
- it("emits audit event", async () => {
- await call("channel::feishu::webhook", {
- body: {
- event: {
- message: {
- chat_id: "chat-5",
- content: JSON.stringify({ text: "Audit" }),
- },
- sender: { sender_id: { user_id: "feishu-u5" } },
- },
- },
- });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "security::audit",
- expect.objectContaining({
- detail: expect.objectContaining({
- channel: "feishu",
- chatId: "chat-5",
- userId: "feishu-u5",
- }),
- }),
- );
- });
-
- it("ignores events with no event field", async () => {
- const result = await call("channel::feishu::webhook", {
- body: {},
- });
- expect(result.status_code).toBe(200);
- expect(result.body.ok).toBe(true);
- });
-});
-
-describe("channel::dingtalk::webhook", () => {
- it("registers the handler", () => {
- expect(handlers["channel::dingtalk::webhook"]).toBeDefined();
- });
-
- it("ignores empty text content", async () => {
- const result = await call("channel::dingtalk::webhook", {
- body: { text: {}, conversationId: "conv-1", senderId: "s1" },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.ok).toBe(true);
- });
-
- it("ignores whitespace-only text", async () => {
- const result = await call("channel::dingtalk::webhook", {
- body: {
- text: { content: " " },
- conversationId: "conv-2",
- senderId: "s2",
- },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.ok).toBe(true);
- });
-
- it("processes text message", async () => {
- const result = await call("channel::dingtalk::webhook", {
- body: {
- text: { content: "Hello DingTalk" },
- conversationId: "conv-3",
- senderId: "ding-s1",
- senderNick: "TestUser",
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("routes to agent::chat with dingtalk session", async () => {
- await call("channel::dingtalk::webhook", {
- body: {
- text: { content: "Route test" },
- conversationId: "conv-4",
- senderId: "ding-s2",
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls.length).toBe(1);
- expect(chatCalls[0][1].sessionId).toBe("dingtalk:conv-4");
- expect(chatCalls[0][1].message).toBe("Route test");
- });
-
- it("sends reply via DingTalk API", async () => {
- await call("channel::dingtalk::webhook", {
- body: {
- text: { content: "API test" },
- conversationId: "conv-5",
- senderId: "ding-s3",
- },
- });
- expect(mockFetch).toHaveBeenCalledWith(
- expect.stringContaining("oapi.dingtalk.com"),
- expect.objectContaining({ method: "POST" }),
- );
- });
-
- it("includes access_token in API URL", async () => {
- await call("channel::dingtalk::webhook", {
- body: {
- text: { content: "Token test" },
- conversationId: "conv-6",
- senderId: "ding-s4",
- },
- });
- const fetchUrl = mockFetch.mock.calls[0][0] as string;
- expect(fetchUrl).toContain("access_token=test-dingtalk-token");
- });
-
- it("includes signature in API URL", async () => {
- await call("channel::dingtalk::webhook", {
- body: {
- text: { content: "Sign test" },
- conversationId: "conv-7",
- senderId: "ding-s5",
- },
- });
- const fetchUrl = mockFetch.mock.calls[0][0] as string;
- expect(fetchUrl).toContain("sign=");
- });
-
- it("emits audit event", async () => {
- await call("channel::dingtalk::webhook", {
- body: {
- text: { content: "Audit" },
- conversationId: "conv-8",
- senderId: "ding-s6",
- },
- });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "security::audit",
- expect.objectContaining({
- detail: expect.objectContaining({
- channel: "dingtalk",
- conversationId: "conv-8",
- senderId: "ding-s6",
- }),
- }),
- );
- });
-
- it("trims content whitespace", async () => {
- await call("channel::dingtalk::webhook", {
- body: {
- text: { content: " trimmed " },
- conversationId: "conv-9",
- senderId: "ding-s7",
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].message).toBe("trimmed");
- });
-});
diff --git a/src/__tests__/channels-webex.test.ts b/src/__tests__/channels-webex.test.ts
deleted file mode 100644
index 27038b0..0000000
--- a/src/__tests__/channels-webex.test.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-// @ts-nocheck
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- splitMessage: vi.fn((text: string, limit: number) => {
- const chunks: string[] = [];
- for (let i = 0; i < text.length; i += limit)
- chunks.push(text.slice(i, i + limit));
- return chunks.length ? chunks : [text];
- }),
- resolveAgent: vi.fn(async () => "default-agent"),
-}));
-
-const mockFetch = vi.fn(async () => ({
- ok: true,
- json: async () => ({
- text: "Fetched message",
- id: "msg-1",
- accessJwt: "jwt",
- did: "did:plc:test",
- }),
-}));
-vi.stubGlobal("fetch", mockFetch);
-
-beforeEach(() => {
- mockTrigger.mockReset();
- mockTrigger.mockImplementation(
- async (fnId: string, data?: any): Promise => {
- if (fnId === "agent::chat") return { content: "Reply" };
- return null;
- },
- );
- mockTriggerVoid.mockClear();
- mockFetch.mockClear();
- mockFetch.mockImplementation(async () => ({
- ok: true,
- json: async () => ({
- text: "Fetched message",
- id: "msg-1",
- accessJwt: "jwt",
- did: "did:plc:test",
- }),
- }));
-});
-
-beforeAll(async () => {
- process.env.BLUESKY_HANDLE = "test.bsky.social";
- process.env.BLUESKY_PASSWORD = "test-password";
- await import("../channels/bluesky.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("channel::bluesky::webhook", () => {
- it("registers the handler", () => {
- expect(handlers["channel::bluesky::webhook"]).toBeDefined();
- });
-
- it("ignores messages without text", async () => {
- const result = await call("channel::bluesky::webhook", {
- body: { did: "did:plc:user1" },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("processes valid mention", async () => {
- const result = await call("channel::bluesky::webhook", {
- body: {
- did: "did:plc:user2",
- text: "Hello Bluesky",
- uri: "at://did:plc:user2/post/1",
- cid: "bafyrei1",
- },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("routes to agent::chat with bluesky session", async () => {
- await call("channel::bluesky::webhook", {
- body: {
- did: "did:plc:user3",
- text: "Bluesky msg",
- uri: "at://x/post/2",
- cid: "bafyrei2",
- },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls.length).toBe(1);
- expect(chatCalls[0][1].sessionId).toBe("bluesky:did:plc:user3");
- });
-
- it("calls Bluesky API to send reply", async () => {
- await call("channel::bluesky::webhook", {
- body: {
- did: "did:plc:user4",
- text: "Auth test",
- uri: "at://x/post/3",
- cid: "bafyrei3",
- },
- });
- expect(mockFetch).toHaveBeenCalled();
- });
-
- it("sends reply as post via AT Protocol", async () => {
- await call("channel::bluesky::webhook", {
- body: {
- did: "did:plc:user5",
- text: "Post test",
- uri: "at://x/post/4",
- cid: "bafyrei4",
- },
- });
- const createCalls = (mockFetch.mock.calls as any[][]).filter((c) =>
- (c[0] as string).includes("createRecord"),
- );
- expect(createCalls.length).toBeGreaterThanOrEqual(1);
- });
-
- it("emits audit event", async () => {
- await call("channel::bluesky::webhook", {
- body: {
- did: "did:plc:user6",
- text: "Audit bsky",
- uri: "at://x/post/5",
- cid: "bafyrei5",
- },
- });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "security::audit",
- expect.objectContaining({
- detail: expect.objectContaining({ channel: "bluesky" }),
- }),
- );
- });
-});
diff --git a/src/__tests__/channels-webhook.test.ts b/src/__tests__/channels-webhook.test.ts
deleted file mode 100644
index 9ac32c4..0000000
--- a/src/__tests__/channels-webhook.test.ts
+++ /dev/null
@@ -1,272 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const kvStore: Record> = {};
-function getScope(scope: string) {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-function resetKv() {
- for (const key of Object.keys(kvStore)) delete kvStore[key];
-}
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "agent::chat") return { content: "Webhook reply" };
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- splitMessage: vi.fn((text: string, limit: number) => [text]),
- resolveAgent: vi.fn(async () => "default-agent"),
- assertNoSsrf: vi.fn(async (url: string) => {
- const parsed = new URL(url);
- if (
- ["127.0.0.1", "localhost", "169.254.169.254"].includes(parsed.hostname)
- ) {
- throw new Error(`SSRF blocked: ${parsed.hostname}`);
- }
- }),
-}));
-
-const mockFetch = vi.fn(async () => ({ ok: true, json: async () => ({}) }));
-vi.stubGlobal("fetch", mockFetch);
-
-beforeEach(() => {
- resetKv();
- mockTrigger.mockReset();
- mockTrigger.mockImplementation(
- async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get")
- return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "agent::chat") return { content: "Webhook reply" };
- return null;
- },
- );
- mockTriggerVoid.mockClear();
- mockFetch.mockClear();
-});
-
-beforeAll(async () => {
- await import("../channels/webhook.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("channel::webhook::inbound", () => {
- it("registers the handler", () => {
- expect(handlers["channel::webhook::inbound"]).toBeDefined();
- });
-
- it("processes webhook with message field", async () => {
- const result = await call("channel::webhook::inbound", {
- body: { message: "Hello" },
- headers: {},
- query_params: {},
- path_params: { channelId: "ch-1" },
- });
- expect(result.status_code).toBe(200);
- expect(result.body.response).toBe("Webhook reply");
- });
-
- it("processes webhook with text field", async () => {
- const result = await call("channel::webhook::inbound", {
- body: { text: "Text field" },
- headers: {},
- query_params: {},
- path_params: { channelId: "ch-2" },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("processes webhook with content field", async () => {
- const result = await call("channel::webhook::inbound", {
- body: { content: "Content field" },
- headers: {},
- query_params: {},
- path_params: { channelId: "ch-3" },
- });
- expect(result.status_code).toBe(200);
- });
-
- it("uses channelId from path_params", async () => {
- await call("channel::webhook::inbound", {
- body: { message: "Path param" },
- headers: {},
- query_params: {},
- path_params: { channelId: "my-channel" },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].sessionId).toBe("webhook:my-channel");
- });
-
- it("uses channel from query_params when no path param", async () => {
- await call("channel::webhook::inbound", {
- body: { message: "Query param" },
- headers: {},
- query_params: { channel: "query-ch" },
- path_params: {},
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].sessionId).toBe("webhook:query-ch");
- });
-
- it("uses default channelId when none provided", async () => {
- await call("channel::webhook::inbound", {
- body: { message: "Default" },
- headers: {},
- query_params: {},
- path_params: {},
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].sessionId).toBe("webhook:default");
- });
-
- it("calls callback_url when provided", async () => {
- await call("channel::webhook::inbound", {
- body: {
- message: "With callback",
- callback_url: "https://example.com/callback",
- },
- headers: {},
- query_params: {},
- path_params: { channelId: "cb-test" },
- });
- expect(mockFetch).toHaveBeenCalledWith(
- "https://example.com/callback",
- expect.objectContaining({ method: "POST" }),
- );
- });
-
- it("calls response_url when provided", async () => {
- await call("channel::webhook::inbound", {
- body: {
- message: "With response_url",
- response_url: "https://example.com/response",
- },
- headers: {},
- query_params: {},
- path_params: { channelId: "resp-test" },
- });
- expect(mockFetch).toHaveBeenCalledWith(
- "https://example.com/response",
- expect.objectContaining({ method: "POST" }),
- );
- });
-
- it("blocks SSRF on callback_url", async () => {
- await expect(
- call("channel::webhook::inbound", {
- body: { message: "SSRF", callback_url: "http://localhost/evil" },
- headers: {},
- query_params: {},
- path_params: { channelId: "ssrf-test" },
- }),
- ).rejects.toThrow("SSRF blocked");
- });
-
- it("handles unrecognized payload", async () => {
- await call("channel::webhook::inbound", {
- body: { something_else: "unknown" },
- headers: {},
- query_params: {},
- path_params: { channelId: "unknown" },
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls[0][1].message).toBe("[Unrecognized webhook payload]");
- });
-});
-
-describe("channel::webhook::configure", () => {
- it("registers the handler", () => {
- expect(handlers["channel::webhook::configure"]).toBeDefined();
- });
-
- it("configures a webhook channel", async () => {
- const result = await call("channel::webhook::configure", {
- channelId: "new-channel",
- agentId: "agent-1",
- });
- expect(result.configured).toBe(true);
- expect(result.channelId).toBe("new-channel");
- });
-
- it("stores configuration in state", async () => {
- await call("channel::webhook::configure", {
- channelId: "stored-ch",
- agentId: "agent-2",
- });
- const setCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "state::set",
- );
- expect(
- setCalls.some(
- (c) =>
- c[1].scope === "channel_agents" && c[1].key === "webhook:stored-ch",
- ),
- ).toBe(true);
- });
-
- it("stores callback URL when provided", async () => {
- await call("channel::webhook::configure", {
- channelId: "cb-ch",
- agentId: "agent-3",
- callbackUrl: "https://example.com/hook",
- });
- const setCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "state::set",
- );
- const storeCall = setCalls.find((c) => c[1].key === "webhook:cb-ch");
- expect(storeCall?.[1].value.callbackUrl).toBe("https://example.com/hook");
- });
-
- it("includes configuredAt timestamp", async () => {
- const before = Date.now();
- await call("channel::webhook::configure", {
- channelId: "ts-ch",
- agentId: "agent-4",
- });
- const setCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "state::set",
- );
- const storeCall = setCalls.find((c) => c[1].key === "webhook:ts-ch");
- expect(storeCall?.[1].value.configuredAt).toBeGreaterThanOrEqual(before);
- });
-});
diff --git a/src/__tests__/cost-tracker.test.ts b/src/__tests__/cost-tracker.test.ts
deleted file mode 100644
index 5759028..0000000
--- a/src/__tests__/cost-tracker.test.ts
+++ /dev/null
@@ -1,312 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const kvStore: Record> = {};
-function getScope(scope: string) {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-function resetKv() {
- for (const key of Object.keys(kvStore)) delete kvStore[key];
-}
-function seedKv(scope: string, key: string, value: unknown) {
- getScope(scope).set(key, value);
-}
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "state::list") {
- const entries = [...getScope(data.scope).entries()];
- if (data.prefix) {
- return entries
- .filter(([key]) => key.startsWith(data.prefix))
- .map(([key, value]) => ({ key, value }));
- }
- return entries.map(([key, value]) => ({ key, value }));
- }
- if (fnId === "state::update") {
- const scope = getScope(data.scope);
- const current: any = scope.get(data.key) || {};
- for (const op of data.operations || []) {
- if (op.type === "set") current[op.path] = op.value;
- if (op.type === "increment")
- current[op.path] = (current[op.path] || 0) + op.value;
- }
- scope.set(data.key, current);
- return current;
- }
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/pricing", () => ({
- PRICING: {
- "claude-opus-4-6": { input: 15, output: 75 },
- "gpt-4o": { input: 2.5, output: 10 },
- },
-}));
-
-beforeEach(() => {
- resetKv();
- mockTrigger.mockClear();
- mockTriggerVoid.mockClear();
-});
-
-beforeAll(async () => {
- await import("../cost-tracker.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("cost::track", () => {
- it("calculates cost correctly for known model", async () => {
- const result = await call("cost::track", {
- agentId: "agent-1",
- sessionId: "sess-1",
- model: "claude-opus-4-6",
- inputTokens: 1000,
- outputTokens: 500,
- });
- const expected = (1000 * 15 + 500 * 75) / 1_000_000;
- expect(result.cost).toBeCloseTo(expected, 8);
- expect(result.provider).toBe("known");
- expect(result.model).toBe("claude-opus-4-6");
- });
-
- it("handles unknown model with zero pricing", async () => {
- const result = await call("cost::track", {
- agentId: "agent-1",
- sessionId: "sess-1",
- model: "unknown-model-xyz",
- inputTokens: 5000,
- outputTokens: 2000,
- });
- expect(result.cost).toBe(0);
- expect(result.provider).toBe("unknown");
- });
-
- it("stores record in cost_records scope", async () => {
- await call("cost::track", {
- agentId: "agent-1",
- sessionId: "sess-1",
- model: "gpt-4o",
- inputTokens: 100,
- outputTokens: 200,
- });
- const setCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "state::set" && c[1].scope === "cost_records",
- );
- expect(setCalls.length).toBe(1);
- expect(setCalls[0][1].value.agentId).toBe("agent-1");
- expect(setCalls[0][1].value.model).toBe("gpt-4o");
- });
-
- it("calculates cost for gpt-4o model", async () => {
- const result = await call("cost::track", {
- agentId: "agent-2",
- sessionId: "sess-2",
- model: "gpt-4o",
- inputTokens: 2000,
- outputTokens: 1000,
- });
- const expected = (2000 * 2.5 + 1000 * 10) / 1_000_000;
- expect(result.cost).toBeCloseTo(expected, 8);
- });
-
- it("updates daily aggregates via triggerVoid", async () => {
- await call("cost::track", {
- agentId: "agent-1",
- sessionId: "sess-1",
- model: "gpt-4o",
- inputTokens: 100,
- outputTokens: 50,
- });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "state::update",
- expect.objectContaining({ scope: "cost_daily" }),
- );
- });
-
- it("includes cache token costs when provided", async () => {
- const result = await call("cost::track", {
- agentId: "agent-1",
- sessionId: "sess-1",
- model: "claude-opus-4-6",
- inputTokens: 1000,
- outputTokens: 500,
- cacheReadTokens: 200,
- cacheWriteTokens: 100,
- });
- const cacheReadPrice = 15 * 0.1;
- const cacheWritePrice = 15 * 1.25;
- const expected =
- (1000 * 15 + 500 * 75 + 200 * cacheReadPrice + 100 * cacheWritePrice) /
- 1_000_000;
- expect(result.cost).toBeCloseTo(expected, 8);
- expect(result.cacheReadTokens).toBe(200);
- expect(result.cacheWriteTokens).toBe(100);
- });
-});
-
-describe("cost::summary", () => {
- it("returns total and breakdown", async () => {
- const today = new Date().toISOString().slice(0, 10);
- seedKv("cost_records", `${today}:agent-1:sess-1:1000`, {
- agentId: "agent-1",
- model: "gpt-4o",
- inputTokens: 100,
- outputTokens: 50,
- cost: 0.00075,
- timestamp: Date.now(),
- });
- seedKv("cost_records", `${today}:agent-1:sess-1:2000`, {
- agentId: "agent-1",
- model: "gpt-4o",
- inputTokens: 200,
- outputTokens: 100,
- cost: 0.0015,
- timestamp: Date.now(),
- });
-
- const result = await call("cost::summary", {
- startDate: today,
- endDate: today,
- });
- expect(result.total).toBeCloseTo(0.00225, 6);
- expect(result.breakdown.length).toBeGreaterThan(0);
- expect(result.period.start).toBe(today);
- expect(result.period.end).toBe(today);
- });
-
- it("groups by agent", async () => {
- const today = new Date().toISOString().slice(0, 10);
- const now = Date.now();
- seedKv("cost_records", `${today}:agent-a:sess-1:${now}`, {
- agentId: "agent-a",
- model: "gpt-4o",
- inputTokens: 100,
- outputTokens: 50,
- cost: 0.001,
- timestamp: now,
- });
- seedKv("cost_records", `${today}:agent-b:sess-1:${now + 1}`, {
- agentId: "agent-b",
- model: "gpt-4o",
- inputTokens: 200,
- outputTokens: 100,
- cost: 0.002,
- timestamp: now + 1,
- });
-
- const result = await call("cost::summary", {
- startDate: today,
- endDate: today,
- groupBy: "agent",
- });
- expect(result.breakdown.length).toBe(2);
- const keys = result.breakdown.map((b: any) => b.key).sort();
- expect(keys).toEqual(["agent-a", "agent-b"]);
- });
-
- it("filters by agentId when provided", async () => {
- const today = new Date().toISOString().slice(0, 10);
- const now = Date.now();
- seedKv("cost_records", `${today}:agent-a:sess-1:${now}`, {
- agentId: "agent-a",
- model: "gpt-4o",
- inputTokens: 100,
- outputTokens: 50,
- cost: 0.001,
- timestamp: now,
- });
- seedKv("cost_records", `${today}:agent-b:sess-1:${now + 1}`, {
- agentId: "agent-b",
- model: "gpt-4o",
- inputTokens: 200,
- outputTokens: 100,
- cost: 0.002,
- timestamp: now + 1,
- });
-
- const result = await call("cost::summary", {
- agentId: "agent-a",
- startDate: today,
- endDate: today,
- });
- expect(result.total).toBeCloseTo(0.001, 6);
- });
-});
-
-describe("cost::budget_check", () => {
- it("returns within budget when no budget configured", async () => {
- const result = await call("cost::budget_check", { agentId: "agent-1" });
- expect(result.withinBudget).toBe(true);
- expect(result.limit).toBe(-1);
- expect(result.remaining).toBe(-1);
- });
-
- it("detects budget exceeded", async () => {
- seedKv("agents", "agent-1", {
- resources: { dailyBudget: 0.001, monthlyBudget: 1.0 },
- });
- const today = new Date().toISOString().slice(0, 10);
- seedKv("cost_daily", `${today}:agent-1`, {
- cost: 0.5,
- inputTokens: 10000,
- outputTokens: 5000,
- calls: 10,
- });
-
- const result = await call("cost::budget_check", { agentId: "agent-1" });
- expect(result.withinBudget).toBe(false);
- expect(result.spent).toBeGreaterThan(0);
- });
-
- it("audits when budget is exceeded", async () => {
- seedKv("agents", "agent-1", {
- resources: { dailyBudget: 0.0001 },
- });
- const today = new Date().toISOString().slice(0, 10);
- seedKv("cost_daily", `${today}:agent-1`, { cost: 1.0 });
-
- await call("cost::budget_check", { agentId: "agent-1" });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "security::audit",
- expect.objectContaining({ type: "budget_exceeded" }),
- );
- });
-
- it("returns within budget when spend is under limit", async () => {
- seedKv("agents", "agent-1", {
- resources: { dailyBudget: 100, monthlyBudget: 1000 },
- });
- const today = new Date().toISOString().slice(0, 10);
- seedKv("cost_daily", `${today}:agent-1`, { cost: 0.01 });
-
- const result = await call("cost::budget_check", { agentId: "agent-1" });
- expect(result.withinBudget).toBe(true);
- });
-});
diff --git a/src/__tests__/dashboard.test.ts b/src/__tests__/dashboard.test.ts
deleted file mode 100644
index 430d215..0000000
--- a/src/__tests__/dashboard.test.ts
+++ /dev/null
@@ -1,443 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const kvStore: Record> = {};
-function getScope(scope: string) {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-function resetKv() {
- for (const key of Object.keys(kvStore)) delete kvStore[key];
-}
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "state::list") {
- const entries = [...getScope(data.scope).entries()].map(([key, value]) => ({
- key,
- value,
- }));
- return { entries };
- }
- if (fnId === "agent::list")
- return {
- agents: [
- { name: "default", model: "claude-sonnet-4-6", status: "ready" },
- ],
- };
- if (fnId === "skill::list")
- return { skills: [{ name: "recall", tags: ["memory"] }] };
- if (fnId === "llm::usage")
- return {
- stats: [
- { requests: 100, input_tokens: 5000, output_tokens: 2000, cost: 0.15 },
- ],
- };
- if (fnId === "hand::list")
- return { hands: [{ id: "h1", name: "daily", enabled: true }] };
- if (fnId === "workflow::list")
- return { workflows: [{ name: "onboarding", steps: [1, 2] }] };
- if (fnId === "approval::list")
- return { pending: [{ id: "a1", action: "deploy" }] };
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- requireAuth: vi.fn(),
-}));
-
-vi.mock("@agentos/shared/security-headers", () => ({
- SECURITY_HEADERS: { "X-Frame-Options": "DENY" },
-}));
-
-const ENV_KEY = process.env.AGENTOS_API_KEY;
-const defaultMockImpl = async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "state::list") {
- const entries = [...getScope(data.scope).entries()].map(([key, value]) => ({
- key,
- value,
- }));
- return { entries };
- }
- if (fnId === "agent::list")
- return {
- agents: [
- { name: "default", model: "claude-sonnet-4-6", status: "ready" },
- ],
- };
- if (fnId === "skill::list")
- return { skills: [{ name: "recall", tags: ["memory"] }] };
- if (fnId === "llm::usage")
- return {
- stats: [
- { requests: 100, input_tokens: 5000, output_tokens: 2000, cost: 0.15 },
- ],
- };
- if (fnId === "hand::list")
- return { hands: [{ id: "h1", name: "daily", enabled: true }] };
- if (fnId === "workflow::list")
- return { workflows: [{ name: "onboarding", steps: [1, 2] }] };
- if (fnId === "approval::list")
- return { pending: [{ id: "a1", action: "deploy" }] };
- return null;
-};
-
-beforeEach(() => {
- resetKv();
- mockTrigger.mockReset();
- mockTrigger.mockImplementation(defaultMockImpl);
- mockTriggerVoid.mockClear();
- process.env.AGENTOS_API_KEY = "test-key";
-});
-
-beforeAll(async () => {
- await import("../dashboard.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("dashboard::page", () => {
- it("returns HTML content", async () => {
- const result = await call("dashboard::page", {});
- expect(result.html).toBeDefined();
- expect(result.contentType).toBe("text/html");
- });
-
- it("contains AgentOS title", async () => {
- const result = await call("dashboard::page", {});
- expect(result.html).toContain("AgentOS Dashboard ");
- });
-
- it("includes Tailwind CSS", async () => {
- const result = await call("dashboard::page", {});
- expect(result.html).toContain("cdn.tailwindcss.com");
- });
-
- it("includes Alpine.js", async () => {
- const result = await call("dashboard::page", {});
- expect(result.html).toContain("alpinejs");
- });
-
- it("includes security headers", async () => {
- const result = await call("dashboard::page", {});
- expect(result.headers).toBeDefined();
- });
-
- it("includes CSP header", async () => {
- const result = await call("dashboard::page", {});
- expect(result.headers["Content-Security-Policy"]).toBeDefined();
- expect(result.headers["Content-Security-Policy"]).toContain("default-src");
- });
-
- it("CSP blocks object-src", async () => {
- const result = await call("dashboard::page", {});
- expect(result.headers["Content-Security-Policy"]).toContain(
- "object-src 'none'",
- );
- });
-
- it("CSP blocks frame-ancestors", async () => {
- const result = await call("dashboard::page", {});
- expect(result.headers["Content-Security-Policy"]).toContain(
- "frame-ancestors 'none'",
- );
- });
-
- it("includes navigation items in HTML", async () => {
- const result = await call("dashboard::page", {});
- expect(result.html).toContain("navItems");
- });
-
- it("includes overview cards section", async () => {
- const result = await call("dashboard::page", {});
- expect(result.html).toContain("overviewCards");
- });
-
- it("includes dark mode class", async () => {
- const result = await call("dashboard::page", {});
- expect(result.html).toContain("dark bg-surface-50");
- });
-
- it("includes dashboard JavaScript function", async () => {
- const result = await call("dashboard::page", {});
- expect(result.html).toContain("function dashboard()");
- });
-
- it("includes refresh interval", async () => {
- const result = await call("dashboard::page", {});
- expect(result.html).toContain("setInterval");
- });
-
- it("includes chat section", async () => {
- const result = await call("dashboard::page", {});
- expect(result.html).toContain("sendChat()");
- });
-
- it("includes approval handling", async () => {
- const result = await call("dashboard::page", {});
- expect(result.html).toContain("handleApproval");
- });
-});
-
-describe("dashboard::stats", () => {
- it("returns aggregated statistics", async () => {
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.agents).toBeDefined();
- expect(result.skills).toBeDefined();
- });
-
- it("returns agent count", async () => {
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.agents).toBe(1);
- });
-
- it("returns skill count", async () => {
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.skills).toBe(1);
- });
-
- it("returns hand count", async () => {
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.hands).toBe(1);
- });
-
- it("returns workflow count", async () => {
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.workflows).toBe(1);
- });
-
- it("returns approval count", async () => {
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.approvals).toBe(1);
- });
-
- it("returns token usage", async () => {
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.tokens).toBeDefined();
- expect(result.tokens.input).toBe(5000);
- expect(result.tokens.output).toBe(2000);
- expect(result.tokens.total).toBe(7000);
- });
-
- it("returns total requests", async () => {
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.requests).toBe(100);
- });
-
- it("returns cost", async () => {
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.cost).toBe(0.15);
- });
-
- it("returns uptime", async () => {
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.uptime).toBeGreaterThan(0);
- });
-
- it("returns agentList array", async () => {
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.agentList).toHaveLength(1);
- expect(result.agentList[0].name).toBe("default");
- });
-
- it("returns skillList array", async () => {
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.skillList).toHaveLength(1);
- });
-
- it("returns handList array", async () => {
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.handList).toHaveLength(1);
- });
-
- it("returns workflowList array", async () => {
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.workflowList).toHaveLength(1);
- });
-
- it("handles trigger failures gracefully", async () => {
- mockTrigger.mockImplementation(async () => {
- throw new Error("fail");
- });
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result).toBeDefined();
- });
-
- it("limits sessionList to 50 entries", async () => {
- mockTrigger.mockImplementation(async (fnId: string, data?: any) => {
- if (fnId === "state::list" && data.scope === "sessions") {
- return { entries: Array(60).fill({ id: "s1" }) };
- }
- if (fnId === "agent::list") return { agents: [] };
- if (fnId === "skill::list") return { skills: [] };
- if (fnId === "llm::usage") return { stats: [] };
- if (fnId === "hand::list") return { hands: [] };
- if (fnId === "workflow::list") return { workflows: [] };
- if (fnId === "approval::list") return { pending: [] };
- return null;
- });
- const result = await call("dashboard::stats", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.sessionList.length).toBeLessThanOrEqual(50);
- });
-});
-
-describe("dashboard::events", () => {
- it("returns events from audit log", async () => {
- const result = await call("dashboard::events", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.events).toBeDefined();
- expect(Array.isArray(result.events)).toBe(true);
- });
-
- it("respects limit parameter", async () => {
- const result = await call("dashboard::events", {
- body: { limit: 10 },
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.events).toBeDefined();
- });
-
- it("defaults limit to 100", async () => {
- const result = await call("dashboard::events", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.events).toBeDefined();
- });
-
- it("handles empty audit log", async () => {
- const result = await call("dashboard::events", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.events).toEqual([]);
- });
-});
-
-describe("dashboard::logs", () => {
- it("returns system logs", async () => {
- const result = await call("dashboard::logs", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.logs).toBeDefined();
- expect(Array.isArray(result.logs)).toBe(true);
- });
-
- it("respects limit parameter", async () => {
- const result = await call("dashboard::logs", {
- body: { limit: 50 },
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.logs).toBeDefined();
- });
-
- it("defaults limit to 200", async () => {
- const result = await call("dashboard::logs", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.logs).toBeDefined();
- });
-
- it("filters by level when specified", async () => {
- mockTrigger.mockImplementation(async (fnId: string, data?: any) => {
- if (fnId === "state::list")
- return {
- entries: [
- { level: "error", message: "bad" },
- { level: "info", message: "ok" },
- ],
- };
- return null;
- });
- const result = await call("dashboard::logs", {
- body: { level: "error" },
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.logs.every((l: any) => l.level === "error")).toBe(true);
- });
-
- it("returns all levels when level is 'all'", async () => {
- mockTrigger.mockImplementation(async (fnId: string) => {
- if (fnId === "state::list")
- return {
- entries: [
- { level: "error", message: "bad" },
- { level: "info", message: "ok" },
- ],
- };
- return null;
- });
- const result = await call("dashboard::logs", {
- body: { level: "all" },
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.logs).toHaveLength(2);
- });
-
- it("handles empty log store", async () => {
- const result = await call("dashboard::logs", {
- headers: { authorization: "Bearer test-key" },
- });
- expect(result.logs).toEqual([]);
- });
-});
diff --git a/src/__tests__/helpers.ts b/src/__tests__/helpers.ts
deleted file mode 100644
index 9ce61a4..0000000
--- a/src/__tests__/helpers.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { vi } from "vitest";
-
-interface KvStore {
- [scope: string]: Map;
-}
-
-const kvStore: KvStore = {};
-
-function getScope(scope: string): Map {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-
-export function resetKv(): void {
- for (const key of Object.keys(kvStore)) {
- delete kvStore[key];
- }
-}
-
-export const mockTrigger = vi.fn(
- async (fnId: string, data?: any, _timeout?: number): Promise => {
- if (fnId === "state::get") {
- const scope = getScope(data.scope);
- return scope.get(data.key) ?? null;
- }
- if (fnId === "state::set") {
- const scope = getScope(data.scope);
- scope.set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "state::delete") {
- const scope = getScope(data.scope);
- scope.delete(data.key);
- return { ok: true };
- }
- if (fnId === "state::list") {
- const scope = getScope(data.scope);
- return [...scope.entries()].map(([key, value]) => ({ key, value }));
- }
- if (fnId === "state::update") {
- const scope = getScope(data.scope);
- const current: any = scope.get(data.key) || {};
- for (const op of data.operations || []) {
- if (op.type === "increment") {
- current[op.path] = (current[op.path] || 0) + op.value;
- }
- }
- scope.set(data.key, current);
- return current;
- }
- return null;
- },
-);
-
-export const mockTriggerVoid = vi.fn((_fnId: string, _data?: any): void => {});
-
-export const mockRegisterFunction = vi.fn(
- (_config: any, handler: Function) => handler,
-);
-
-export const mockRegisterTrigger = vi.fn((_config: any) => {});
-
-export const mockListFunctions = vi.fn(async () => []);
-
-export function createMockInit() {
- return {
- trigger: mockTrigger,
- triggerVoid: mockTriggerVoid,
- registerFunction: mockRegisterFunction,
- registerTrigger: mockRegisterTrigger,
- listFunctions: mockListFunctions,
- };
-}
-
-export function seedKv(scope: string, key: string, value: unknown): void {
- getScope(scope).set(key, value);
-}
-
-export function getKv(scope: string, key: string): unknown {
- return getScope(scope).get(key);
-}
-
-export function makeRequest(overrides: Partial<{
- headers: Record;
- body: any;
-}> = {}) {
- return {
- headers: overrides.headers || {},
- body: overrides.body || {},
- };
-}
-
-export function makeAuthRequest(apiKey: string, body?: any) {
- return makeRequest({
- headers: { authorization: `Bearer ${apiKey}` },
- body,
- });
-}
diff --git a/src/__tests__/knowledge-graph.test.ts b/src/__tests__/knowledge-graph.test.ts
deleted file mode 100644
index d9f517b..0000000
--- a/src/__tests__/knowledge-graph.test.ts
+++ /dev/null
@@ -1,274 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const kvStore: Record> = {};
-function getScope(scope: string) {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-function resetKv() {
- for (const key of Object.keys(kvStore)) delete kvStore[key];
-}
-function seedKv(scope: string, key: string, value: unknown) {
- getScope(scope).set(key, value);
-}
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "state::delete") {
- getScope(data.scope).delete(data.key);
- return { ok: true };
- }
- if (fnId === "state::list") {
- return [...getScope(data.scope).entries()].map(([key, value]) => ({
- key,
- value,
- }));
- }
- if (fnId === "state::update") {
- const scope = getScope(data.scope);
- const current: any = scope.get(data.key) || {};
- for (const op of data.operations || []) {
- if (op.type === "set") current[op.path] = op.value;
- if (op.type === "increment")
- current[op.path] = (current[op.path] || 0) + op.value;
- }
- scope.set(data.key, current);
- return current;
- }
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-beforeEach(() => {
- resetKv();
- mockTrigger.mockClear();
- mockTriggerVoid.mockClear();
-});
-
-beforeAll(async () => {
- await import("../knowledge-graph.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("kg::add_temporal", () => {
- it("stores entity and latest pointer", async () => {
- const result = await call("kg::add_temporal", {
- entity: "nodeA",
- type: "concept",
- properties: { lang: "ts" },
- relations: [],
- agentId: "agent-1",
- });
- expect(result.stored).toBe(true);
- expect(result.entity).toBe("nodeA");
- expect(result.version).toBe(1);
-
- const latest = getScope("kg_temporal:agent-1").get("nodeA:latest") as any;
- expect(latest.version).toBe(1);
- expect(latest.key).toBe("nodeA:1");
-
- const entry = getScope("kg_temporal:agent-1").get("nodeA:1") as any;
- expect(entry.entity).toBe("nodeA");
- expect(entry.type).toBe("concept");
- expect(entry.properties.lang).toBe("ts");
- });
-
- it("increments version", async () => {
- await call("kg::add_temporal", {
- entity: "nodeB",
- type: "concept",
- properties: { v: 1 },
- relations: [],
- agentId: "agent-1",
- });
- const r2 = await call("kg::add_temporal", {
- entity: "nodeB",
- type: "concept",
- properties: { v: 2 },
- relations: [],
- agentId: "agent-1",
- });
- expect(r2.version).toBe(2);
-
- const latest = getScope("kg_temporal:agent-1").get("nodeB:latest") as any;
- expect(latest.version).toBe(2);
- expect(latest.key).toBe("nodeB:2");
- });
-
- it("returns error for missing entity", async () => {
- const result = await call("kg::add_temporal", {
- entity: "",
- type: "concept",
- properties: {},
- relations: [],
- agentId: "agent-1",
- });
- expect(result.error).toBe("entity and agentId required");
- });
-
- it("returns error for missing agentId", async () => {
- const result = await call("kg::add_temporal", {
- entity: "nodeC",
- type: "concept",
- properties: {},
- relations: [],
- agentId: "",
- });
- expect(result.error).toBe("entity and agentId required");
- });
-});
-
-describe("kg::timeline", () => {
- it("returns versions sorted by createdAt", async () => {
- const scope = "kg_temporal:agent-1";
- seedKv(scope, "nodeX:1", {
- entity: "nodeX",
- type: "concept",
- properties: {},
- relations: [],
- validFrom: 1000,
- validUntil: null,
- createdAt: 3000,
- version: 1,
- agentId: "agent-1",
- });
- seedKv(scope, "nodeX:2", {
- entity: "nodeX",
- type: "concept",
- properties: {},
- relations: [],
- validFrom: 2000,
- validUntil: null,
- createdAt: 1000,
- version: 2,
- agentId: "agent-1",
- });
- seedKv(scope, "nodeX:3", {
- entity: "nodeX",
- type: "concept",
- properties: {},
- relations: [],
- validFrom: 3000,
- validUntil: null,
- createdAt: 2000,
- version: 3,
- agentId: "agent-1",
- });
- seedKv(scope, "nodeX:latest", { version: 3, key: "nodeX:3" });
-
- const result = await call("kg::timeline", {
- entity: "nodeX",
- agentId: "agent-1",
- });
- expect(result).toHaveLength(3);
- expect(result[0].createdAt).toBe(1000);
- expect(result[1].createdAt).toBe(2000);
- expect(result[2].createdAt).toBe(3000);
- });
-});
-
-describe("kg::diff", () => {
- it("detects added entities", async () => {
- const scope = "kg_temporal:agent-1";
- seedKv(scope, "nodeD:1", {
- entity: "nodeD",
- type: "concept",
- properties: { a: 1 },
- relations: [],
- validFrom: 1000,
- validUntil: null,
- createdAt: 1000,
- version: 1,
- agentId: "agent-1",
- });
- seedKv(scope, "nodeD:2", {
- entity: "nodeD",
- type: "concept",
- properties: { a: 2 },
- relations: [],
- validFrom: 2000,
- validUntil: null,
- createdAt: 5000,
- version: 2,
- agentId: "agent-1",
- });
- seedKv(scope, "nodeD:latest", { version: 2, key: "nodeD:2" });
-
- const result = await call("kg::diff", {
- entity: "nodeD",
- agentId: "agent-1",
- timestamp1: 2000,
- timestamp2: 6000,
- });
- expect(result.added).toHaveLength(1);
- expect(result.added[0].version).toBe(2);
- });
-});
-
-describe("kg::stats", () => {
- it("counts entities and relations", async () => {
- const scope = "kg_temporal:agent-1";
- seedKv(scope, "e1:1", {
- entity: "e1",
- type: "concept",
- properties: {},
- relations: [{ target: "e2", type: "link" }],
- validFrom: 1000,
- validUntil: null,
- createdAt: 1000,
- version: 1,
- agentId: "agent-1",
- });
- seedKv(scope, "e2:1", {
- entity: "e2",
- type: "concept",
- properties: {},
- relations: [],
- validFrom: 1000,
- validUntil: null,
- createdAt: 1000,
- version: 1,
- agentId: "agent-1",
- });
- seedKv(scope, "e1:latest", { version: 1, key: "e1:1" });
- seedKv(scope, "e2:latest", { version: 1, key: "e2:1" });
-
- const result = await call("kg::stats", { agentId: "agent-1" });
- expect(result.totalEntities).toBe(2);
- expect(result.totalRelations).toBe(1);
- expect(result.connectedComponents).toBeGreaterThanOrEqual(1);
- });
-
- it("returns 0 for empty graph", async () => {
- const result = await call("kg::stats", { agentId: "empty-agent" });
- expect(result.totalEntities).toBe(0);
- expect(result.totalRelations).toBe(0);
- expect(result.avgDegree).toBe(0);
- expect(result.connectedComponents).toBe(0);
- });
-});
diff --git a/src/__tests__/memory-reflection.test.ts b/src/__tests__/memory-reflection.test.ts
deleted file mode 100644
index 91ecfa1..0000000
--- a/src/__tests__/memory-reflection.test.ts
+++ /dev/null
@@ -1,254 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const kvStore: Record> = {};
-function getScope(scope: string) {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-function resetKv() {
- for (const key of Object.keys(kvStore)) delete kvStore[key];
-}
-function seedKv(scope: string, key: string, value: unknown) {
- getScope(scope).set(key, value);
-}
-
-const handlers: Record = {};
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "state::list") {
- return [...getScope(data.scope).entries()].map(([key, value]) => ({
- key,
- value,
- }));
- }
- if (fnId === "state::update") {
- const scope = getScope(data.scope);
- const current: any = scope.get(data.key) || {};
- for (const op of data.operations || []) {
- if (op.type === "increment")
- current[op.path] = (current[op.path] || 0) + op.value;
- if (op.type === "set") current[op.path] = op.value;
- if (op.type === "merge")
- current[op.path] = [...(current[op.path] || []), ...(op.value || [])];
- }
- scope.set(data.key, current);
- return current;
- }
- if (fnId === "memory::recall") {
- return [
- { role: "user", content: "I prefer TypeScript over JavaScript", score: 0.9 },
- { role: "assistant", content: "Noted, using TypeScript", score: 0.8 },
- ];
- }
- if (fnId === "llm::complete") {
- return {
- content: JSON.stringify({
- facts: [
- { content: "User prefers TypeScript", importance: 0.8, category: "preference" },
- { content: "Low importance item", importance: 0.3, category: "context" },
- ],
- profileUpdates: { preferences: { language: "TypeScript" } },
- }),
- };
- }
- if (fnId === "memory::user_profile::update") return { updated: true };
- if (fnId === "evolve::generate") return { functionId: "evolved::new_skill_v1" };
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
- Logger: class { info() {} warn() {} error() {} },
-}));
-
-vi.mock("@agentos/shared/config", () => ({
- ENGINE_URL: "ws://localhost:3111",
- OTEL_CONFIG: {},
- registerShutdown: vi.fn(),
-}));
-vi.mock("@agentos/shared/metrics", () => ({
- recordMetric: vi.fn(),
-}));
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- requireAuth: vi.fn(),
- stripCodeFences: (s: string) => s.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, ""),
-}));
-
-beforeEach(() => {
- resetKv();
- mockTrigger.mockClear();
- mockTriggerVoid.mockClear();
-});
-
-beforeAll(async () => {
- await import("../memory-reflection.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("reflect::check_turn", () => {
- it("increments turn counter", async () => {
- const result = await call("reflect::check_turn", {
- agentId: "a1",
- sessionId: "s1",
- });
- expect(result.turnCount).toBe(1);
- expect(result.shouldReflect).toBe(false);
- });
-
- it("triggers reflection on turn 5", async () => {
- seedKv("reflect:a1", "s1", { turnCount: 4 });
- const result = await call("reflect::check_turn", {
- agentId: "a1",
- sessionId: "s1",
- });
- expect(result.turnCount).toBe(5);
- expect(result.shouldReflect).toBe(true);
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "reflect::curate_memory",
- expect.objectContaining({ agentId: "a1" }),
- );
- });
-
- it("triggers skill discovery when iterations >= 5", async () => {
- const result = await call("reflect::check_turn", {
- agentId: "a1",
- sessionId: "s1",
- iterations: 7,
- });
- expect(result.shouldReviewSkills).toBe(true);
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "reflect::discover_skills",
- expect.objectContaining({ agentId: "a1", iterations: 7 }),
- );
- });
-
- it("does not trigger skill discovery when iterations < 5", async () => {
- const result = await call("reflect::check_turn", {
- agentId: "a1",
- sessionId: "s1",
- iterations: 2,
- });
- expect(result.shouldReviewSkills).toBe(false);
- });
-});
-
-describe("reflect::curate_memory", () => {
- it("extracts facts from LLM response and stores high-importance ones", async () => {
- const result = await call("reflect::curate_memory", {
- agentId: "a1",
- sessionId: "s1",
- });
- expect(result.saved).toBe(1);
- expect(result.totalFacts).toBe(2);
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "memory::store",
- expect.objectContaining({
- agentId: "a1",
- content: "[Curated] User prefers TypeScript",
- }),
- );
- });
-
- it("skips facts with low importance", async () => {
- const result = await call("reflect::curate_memory", {
- agentId: "a1",
- sessionId: "s1",
- });
- const storeCalls = mockTriggerVoid.mock.calls.filter(
- (c) => c[0] === "memory::store",
- );
- expect(storeCalls.length).toBe(1);
- expect(storeCalls[0][1].content).not.toContain("Low importance");
- });
-
- it("handles code-fenced JSON from LLM", async () => {
- mockTrigger.mockImplementationOnce(async (fnId: string, data?: any) => {
- if (fnId === "memory::recall") return [{ role: "user", content: "test" }];
- return null;
- });
- mockTrigger.mockImplementationOnce(async () => null);
- mockTrigger.mockImplementationOnce(async () => ({
- content: '```json\n{"facts": [{"content": "fact1", "importance": 0.9, "category": "learning"}], "profileUpdates": null}\n```',
- }));
-
- const result = await call("reflect::curate_memory", {
- agentId: "a2",
- sessionId: "s2",
- });
- expect(result.saved).toBe(1);
- });
-
- it("triggers profile update when profileUpdates present", async () => {
- await call("reflect::curate_memory", {
- agentId: "a1",
- sessionId: "s1",
- });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "memory::user_profile::update",
- expect.objectContaining({ agentId: "a1" }),
- );
- });
-});
-
-describe("reflect::discover_skills", () => {
- it("skips when iterations < 5", async () => {
- const result = await call("reflect::discover_skills", {
- agentId: "a1",
- sessionId: "s1",
- iterations: 3,
- });
- expect(result.created).toBe(false);
- });
-
- it("fires evolve::generate when LLM suggests skill creation", async () => {
- mockTrigger.mockImplementation(async (fnId: string, data?: any) => {
- if (fnId === "memory::recall") return [{ role: "user", content: "complex workflow" }];
- if (fnId === "llm::complete") {
- return {
- content: JSON.stringify({
- shouldCreate: true,
- name: "auto_deploy",
- goal: "Automate deployment steps",
- spec: "deploy to k8s",
- }),
- };
- }
- return null;
- });
-
- const result = await call("reflect::discover_skills", {
- agentId: "a1",
- sessionId: "s1",
- iterations: 10,
- });
- expect(result.created).toBe(true);
- expect(result.name).toBe("auto_deploy");
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "evolve::generate",
- expect.objectContaining({ name: "auto_deploy" }),
- );
- });
-});
diff --git a/src/__tests__/migration.test.ts b/src/__tests__/migration.test.ts
deleted file mode 100644
index a916a02..0000000
--- a/src/__tests__/migration.test.ts
+++ /dev/null
@@ -1,276 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const kvStore: Record> = {};
-function getScope(scope: string) {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-function resetKv() {
- for (const key of Object.keys(kvStore)) delete kvStore[key];
-}
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-const mockFiles: Record = {};
-const mockDirs: Set = new Set();
-
-vi.mock("fs/promises", () => ({
- readFile: vi.fn(async (path: string) => {
- if (mockFiles[path] !== undefined) return mockFiles[path];
- throw new Error("ENOENT");
- }),
- writeFile: vi.fn(async (path: string, content: string) => {
- mockFiles[path] = content;
- }),
- mkdir: vi.fn(async (path: string) => {
- mockDirs.add(path);
- }),
- readdir: vi.fn(async (dir: string) => {
- const entries = Object.keys(mockFiles)
- .filter((f) => f.startsWith(dir) && f !== dir)
- .map((f) => {
- const rel = f.slice(dir.length + 1);
- const name = rel.split("/")[0];
- return {
- name: name + (rel.includes(".") ? "" : ""),
- isDirectory: () => !name.includes("."),
- isFile: () => name.includes("."),
- };
- });
- return entries;
- }),
- access: vi.fn(async (path: string) => {
- if (!mockFiles[path] && !mockDirs.has(path)) throw new Error("ENOENT");
- }),
-}));
-
-vi.mock("child_process", () => ({
- execFile: vi.fn((cmd: string, args: string[], opts: any, cb: Function) => {
- cb(new Error("not found"), "", "");
- }),
-}));
-
-beforeEach(() => {
- resetKv();
- mockTrigger.mockClear();
- for (const key of Object.keys(mockFiles)) delete mockFiles[key];
- mockDirs.clear();
-});
-
-beforeAll(async () => {
- await import("../migration.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("migrate::openclaw", () => {
- it("returns empty report when no config found", async () => {
- const result = await call("migrate::openclaw", { dryRun: true });
- expect(result.framework).toBe("openclaw");
- expect(result.items).toHaveLength(0);
- expect(result.summary.total).toBe(0);
- });
-
- it("migrates agents from config", async () => {
- const home = process.env.HOME || "/root";
- mockFiles[`${home}/.openclaw/openclaw.json`] = JSON.stringify({
- agents: {
- researcher: {
- model: "claude-sonnet",
- system_prompt: "You are a researcher",
- tools: ["web_search", "file_read"],
- },
- },
- });
- const result = await call("migrate::openclaw", { dryRun: true });
- expect(result.items.length).toBe(1);
- expect(result.items[0].type).toBe("agent");
- expect(result.items[0].status).toBe("migrated");
- });
-
- it("migrates channels from config", async () => {
- const home = process.env.HOME || "/root";
- mockFiles[`${home}/.openclaw/openclaw.json`] = JSON.stringify({
- channels: {
- slack: { type: "slack", webhook: "hooks.slack.com" },
- },
- });
- const result = await call("migrate::openclaw", { dryRun: true });
- expect(result.items[0].type).toBe("channel");
- expect(result.items[0].status).toBe("migrated");
- });
-
- it("skips channels without type", async () => {
- const home = process.env.HOME || "/root";
- mockFiles[`${home}/.openclaw/openclaw.json`] = JSON.stringify({
- channels: { bad: {} },
- });
- const result = await call("migrate::openclaw", { dryRun: true });
- expect(result.items[0].status).toBe("skipped");
- expect(result.items[0].reason).toContain("No channel type");
- });
-
- it("skips disabled cron jobs", async () => {
- const home = process.env.HOME || "/root";
- mockFiles[`${home}/.openclaw/openclaw.json`] = JSON.stringify({
- cron: {
- daily: { schedule: "0 0 * * *", agent: "a1", enabled: false },
- },
- });
- const result = await call("migrate::openclaw", { dryRun: true });
- expect(result.items[0].status).toBe("skipped");
- });
-
- it("skips disabled skills", async () => {
- const home = process.env.HOME || "/root";
- mockFiles[`${home}/.openclaw/openclaw.json`] = JSON.stringify({
- skills: { myskill: { path: "/tmp/skill.md", enabled: false } },
- });
- const result = await call("migrate::openclaw", { dryRun: true });
- expect(result.items[0].status).toBe("skipped");
- });
-
- it("produces correct summary counts", async () => {
- const home = process.env.HOME || "/root";
- mockFiles[`${home}/.openclaw/openclaw.json`] = JSON.stringify({
- agents: { a1: { model: "gpt-4", tools: [] } },
- channels: { notype: {} },
- });
- const result = await call("migrate::openclaw", { dryRun: true });
- expect(result.summary.total).toBe(2);
- expect(result.summary.migrated).toBe(1);
- expect(result.summary.skipped).toBe(1);
- });
-});
-
-describe("migrate::openclaw - tool mapping", () => {
- it("maps web_search to tool::web_search", async () => {
- const home = process.env.HOME || "/root";
- mockFiles[`${home}/.openclaw/openclaw.json`] = JSON.stringify({
- agents: {
- mapper: { model: "gpt-4", tools: ["web_search"] },
- },
- });
- const result = await call("migrate::openclaw", { dryRun: false });
- const agentFile = mockFiles["agents/mapper/agent.toml"];
- expect(agentFile).toContain("tool::web_search");
- });
-
- it("maps unknown tools with custom:: prefix", async () => {
- const home = process.env.HOME || "/root";
- mockFiles[`${home}/.openclaw/openclaw.json`] = JSON.stringify({
- agents: {
- custom: { model: "gpt-4", tools: ["my_custom_tool"] },
- },
- });
- const result = await call("migrate::openclaw", { dryRun: false });
- const agentFile = mockFiles["agents/custom/agent.toml"];
- expect(agentFile).toContain("custom::my_custom_tool");
- });
-});
-
-describe("migrate::openclaw - model mapping", () => {
- it("maps gpt-4 to claude-sonnet-4-6", async () => {
- const home = process.env.HOME || "/root";
- mockFiles[`${home}/.openclaw/openclaw.json`] = JSON.stringify({
- agents: { test: { model: "gpt-4", tools: [] } },
- });
- await call("migrate::openclaw", { dryRun: false });
- const file = mockFiles["agents/test/agent.toml"];
- expect(file).toContain("claude-sonnet-4-6");
- });
-
- it("maps gpt-4o-mini to claude-haiku-3.5", async () => {
- const home = process.env.HOME || "/root";
- mockFiles[`${home}/.openclaw/openclaw.json`] = JSON.stringify({
- agents: { mini: { model: "gpt-4o-mini", tools: [] } },
- });
- await call("migrate::openclaw", { dryRun: false });
- const file = mockFiles["agents/mini/agent.toml"];
- expect(file).toContain("claude-haiku-3.5");
- });
-
- it("preserves unknown model names", async () => {
- const home = process.env.HOME || "/root";
- mockFiles[`${home}/.openclaw/openclaw.json`] = JSON.stringify({
- agents: { unknown: { model: "custom-model-v2", tools: [] } },
- });
- await call("migrate::openclaw", { dryRun: false });
- const file = mockFiles["agents/unknown/agent.toml"];
- expect(file).toContain("custom-model-v2");
- });
-});
-
-describe("migrate::scan - framework detection", () => {
- it("returns scan results for all frameworks", async () => {
- const result = await call("migrate::scan", {});
- expect(result.frameworks.length).toBeGreaterThan(10);
- expect(result.summary.scanned).toBeGreaterThan(0);
- });
-
- it("detects openclaw config when present", async () => {
- const home = process.env.HOME || "/root";
- mockFiles[`${home}/.openclaw/openclaw.json`] = "{}";
- const result = await call("migrate::scan", {});
- const openclaw = result.frameworks.find(
- (f: any) => f.framework === "openclaw",
- );
- expect(openclaw.detected).toBe(true);
- expect(openclaw.migratable).toBe(true);
- });
-
- it("reports not detected for missing frameworks", async () => {
- const result = await call("migrate::scan", {});
- const missing = result.frameworks.find(
- (f: any) => f.framework === "crewai",
- );
- expect(missing.detected).toBe(false);
- });
-});
-
-describe("migrate::langchain", () => {
- it("returns empty report when no Python files found", async () => {
- const result = await call("migrate::langchain", {
- dryRun: true,
- configDir: "/empty",
- });
- expect(result.framework).toBe("langchain");
- expect(result.items).toHaveLength(0);
- });
-});
-
-describe("migrate::report", () => {
- it("returns report with zero items when no reports exist", async () => {
- const result = await call("migrate::report", {});
- expect(result.markdown).toContain("Migration Report");
- expect(result.aggregated.totals.total).toBe(0);
- });
-});
diff --git a/src/__tests__/model-catalog-extended.test.ts b/src/__tests__/model-catalog-extended.test.ts
deleted file mode 100644
index 5b01393..0000000
--- a/src/__tests__/model-catalog-extended.test.ts
+++ /dev/null
@@ -1,502 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const handlers: Record = {};
-const mockTrigger = vi.fn();
-const mockTriggerVoid = vi.fn();
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => { handlers[config.id] = handler; },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-beforeEach(() => {});
-
-beforeAll(async () => {
- await import("../model-catalog.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("catalog::models - comprehensive", () => {
- it("returns all models when no filters", async () => {
- const result = await call("catalog::models", {});
- expect(result.length).toBeGreaterThan(20);
- });
-
- it("filters by frontier tier", async () => {
- const result = await call("catalog::models", { tier: "frontier" });
- expect(result.every((m: any) => m.tier === "frontier")).toBe(true);
- expect(result.length).toBeGreaterThan(0);
- });
-
- it("filters by smart tier", async () => {
- const result = await call("catalog::models", { tier: "smart" });
- expect(result.every((m: any) => m.tier === "smart")).toBe(true);
- });
-
- it("filters by balanced tier", async () => {
- const result = await call("catalog::models", { tier: "balanced" });
- expect(result.every((m: any) => m.tier === "balanced")).toBe(true);
- });
-
- it("filters by fast tier", async () => {
- const result = await call("catalog::models", { tier: "fast" });
- expect(result.every((m: any) => m.tier === "fast")).toBe(true);
- });
-
- it("filters by anthropic provider", async () => {
- const result = await call("catalog::models", { provider: "anthropic" });
- expect(result.every((m: any) => m.provider === "anthropic")).toBe(true);
- expect(result.length).toBeGreaterThanOrEqual(3);
- });
-
- it("filters by openai provider", async () => {
- const result = await call("catalog::models", { provider: "openai" });
- expect(result.every((m: any) => m.provider === "openai")).toBe(true);
- });
-
- it("filters by gemini provider", async () => {
- const result = await call("catalog::models", { provider: "gemini" });
- expect(result.every((m: any) => m.provider === "gemini")).toBe(true);
- });
-
- it("filters by deepseek provider", async () => {
- const result = await call("catalog::models", { provider: "deepseek" });
- expect(result.every((m: any) => m.provider === "deepseek")).toBe(true);
- });
-
- it("filters by xai provider", async () => {
- const result = await call("catalog::models", { provider: "xai" });
- expect(result.every((m: any) => m.provider === "xai")).toBe(true);
- });
-
- it("filters by cohere provider", async () => {
- const result = await call("catalog::models", { provider: "cohere" });
- expect(result.every((m: any) => m.provider === "cohere")).toBe(true);
- });
-
- it("filters by bedrock provider", async () => {
- const result = await call("catalog::models", { provider: "bedrock" });
- expect(result.every((m: any) => m.provider === "bedrock")).toBe(true);
- });
-
- it("filters by supportsTools true", async () => {
- const result = await call("catalog::models", { supportsTools: true });
- expect(result.every((m: any) => m.supportsTools === true)).toBe(true);
- });
-
- it("filters by supportsTools false", async () => {
- const result = await call("catalog::models", { supportsTools: false });
- expect(result.every((m: any) => m.supportsTools === false)).toBe(true);
- });
-
- it("combines tier and provider filters", async () => {
- const result = await call("catalog::models", { tier: "smart", provider: "anthropic" });
- expect(result.every((m: any) => m.tier === "smart" && m.provider === "anthropic")).toBe(true);
- });
-
- it("returns empty when no match", async () => {
- const result = await call("catalog::models", { provider: "nonexistent" });
- expect(result).toEqual([]);
- });
-
- it("each model has required fields", async () => {
- const result = await call("catalog::models", {});
- for (const model of result) {
- expect(model.id).toBeDefined();
- expect(model.provider).toBeDefined();
- expect(model.name).toBeDefined();
- expect(model.tier).toBeDefined();
- expect(model.contextWindow).toBeGreaterThan(0);
- expect(model.maxOutput).toBeGreaterThan(0);
- expect(typeof model.inputPrice).toBe("number");
- expect(typeof model.outputPrice).toBe("number");
- expect(typeof model.supportsTools).toBe("boolean");
- expect(typeof model.supportsVision).toBe("boolean");
- expect(typeof model.local).toBe("boolean");
- }
- });
-
- it("frontier models have higher prices", async () => {
- const frontier = await call("catalog::models", { tier: "frontier" });
- const fast = await call("catalog::models", { tier: "fast" });
- const avgFrontierPrice = frontier.reduce((s: number, m: any) => s + m.outputPrice, 0) / frontier.length;
- const avgFastPrice = fast.reduce((s: number, m: any) => s + m.outputPrice, 0) / fast.length;
- expect(avgFrontierPrice).toBeGreaterThan(avgFastPrice);
- });
-
- it("includes claude-opus-4-6", async () => {
- const result = await call("catalog::models", {});
- expect(result.some((m: any) => m.id === "claude-opus-4-6")).toBe(true);
- });
-
- it("includes gpt-4o", async () => {
- const result = await call("catalog::models", {});
- expect(result.some((m: any) => m.id === "gpt-4o")).toBe(true);
- });
-
- it("includes gemini-2.5-pro", async () => {
- const result = await call("catalog::models", {});
- expect(result.some((m: any) => m.id === "gemini-2.5-pro")).toBe(true);
- });
-});
-
-describe("catalog::resolve - comprehensive", () => {
- it("resolves opus alias", async () => {
- const result = await call("catalog::resolve", { model: "opus" });
- expect(result.id).toBe("claude-opus-4-6");
- });
-
- it("resolves sonnet alias", async () => {
- const result = await call("catalog::resolve", { model: "sonnet" });
- expect(result.id).toBe("claude-sonnet-4-6");
- });
-
- it("resolves haiku alias", async () => {
- const result = await call("catalog::resolve", { model: "haiku" });
- expect(result.id).toBe("claude-haiku-4-5");
- });
-
- it("resolves gpt4 alias", async () => {
- const result = await call("catalog::resolve", { model: "gpt4" });
- expect(result.id).toBe("gpt-4o");
- });
-
- it("resolves gpt4o alias", async () => {
- const result = await call("catalog::resolve", { model: "gpt4o" });
- expect(result.id).toBe("gpt-4o");
- });
-
- it("resolves flash alias", async () => {
- const result = await call("catalog::resolve", { model: "flash" });
- expect(result.id).toBe("gemini-2.5-flash");
- });
-
- it("resolves pro alias", async () => {
- const result = await call("catalog::resolve", { model: "pro" });
- expect(result.id).toBe("gemini-2.5-pro");
- });
-
- it("resolves deepseek alias", async () => {
- const result = await call("catalog::resolve", { model: "deepseek" });
- expect(result.id).toBe("deepseek-chat");
- });
-
- it("resolves ds alias", async () => {
- const result = await call("catalog::resolve", { model: "ds" });
- expect(result.id).toBe("deepseek-chat");
- });
-
- it("resolves r1 alias", async () => {
- const result = await call("catalog::resolve", { model: "r1" });
- expect(result.id).toBe("deepseek-reasoner");
- });
-
- it("resolves grok alias", async () => {
- const result = await call("catalog::resolve", { model: "grok" });
- expect(result.id).toBe("grok-2");
- });
-
- it("resolves grok3 alias", async () => {
- const result = await call("catalog::resolve", { model: "grok3" });
- expect(result.id).toBe("grok-3");
- });
-
- it("resolves fast alias", async () => {
- const result = await call("catalog::resolve", { model: "fast" });
- expect(result.id).toBe("claude-haiku-4-5");
- });
-
- it("resolves cheap alias", async () => {
- const result = await call("catalog::resolve", { model: "cheap" });
- expect(result.id).toBe("gpt-4o-mini");
- });
-
- it("resolves smart alias", async () => {
- const result = await call("catalog::resolve", { model: "smart" });
- expect(result.id).toBe("claude-sonnet-4-6");
- });
-
- it("resolves best alias", async () => {
- const result = await call("catalog::resolve", { model: "best" });
- expect(result.id).toBe("claude-opus-4-6");
- });
-
- it("resolves frontier alias", async () => {
- const result = await call("catalog::resolve", { model: "frontier" });
- expect(result.id).toBe("claude-opus-4-6");
- });
-
- it("resolves direct model id", async () => {
- const result = await call("catalog::resolve", { model: "claude-opus-4-6" });
- expect(result.id).toBe("claude-opus-4-6");
- });
-
- it("is case-insensitive", async () => {
- const result = await call("catalog::resolve", { model: "OPUS" });
- expect(result.id).toBe("claude-opus-4-6");
- });
-
- it("throws on unknown model", async () => {
- await expect(
- call("catalog::resolve", { model: "nonexistent-model-xyz" }),
- ).rejects.toThrow("Unknown model");
- });
-
- it("resolves bedrock alias", async () => {
- const result = await call("catalog::resolve", { model: "bedrock" });
- expect(result.id).toBe("bedrock-claude-sonnet");
- });
-
- it("resolves nova alias", async () => {
- const result = await call("catalog::resolve", { model: "nova" });
- expect(result.id).toBe("bedrock-nova-pro");
- });
-
- it("resolves copilot alias", async () => {
- const result = await call("catalog::resolve", { model: "copilot" });
- expect(result.id).toBe("copilot-gpt-4o");
- });
-
- it("resolves qwen alias", async () => {
- const result = await call("catalog::resolve", { model: "qwen" });
- expect(result.id).toBe("qwen-max");
- });
-
- it("resolves kimi alias", async () => {
- const result = await call("catalog::resolve", { model: "kimi" });
- expect(result.id).toBe("moonshot-v1-128k");
- });
-
- it("resolves ernie alias", async () => {
- const result = await call("catalog::resolve", { model: "ernie" });
- expect(result.id).toBe("ernie-4.0-turbo");
- });
-
- it("resolves together alias", async () => {
- const result = await call("catalog::resolve", { model: "together" });
- expect(result.id).toBe("together-llama-3.3-70b");
- });
-
- it("resolves fireworks alias", async () => {
- const result = await call("catalog::resolve", { model: "fireworks" });
- expect(result.id).toBe("fireworks-llama-3.3-70b");
- });
-
- it("resolves openrouter alias", async () => {
- const result = await call("catalog::resolve", { model: "openrouter" });
- expect(result.id).toBe("openrouter-auto");
- });
-
- it("resolves minimax alias", async () => {
- const result = await call("catalog::resolve", { model: "minimax" });
- expect(result.id).toBe("abab7-chat");
- });
-
- it("resolves glm alias", async () => {
- const result = await call("catalog::resolve", { model: "glm" });
- expect(result.id).toBe("glm-4-plus");
- });
-
- it("resolves zhipu alias", async () => {
- const result = await call("catalog::resolve", { model: "zhipu" });
- expect(result.id).toBe("glm-4-plus");
- });
-
- it("resolves command alias", async () => {
- const result = await call("catalog::resolve", { model: "command" });
- expect(result.id).toBe("command-a");
- });
-
- it("resolves jamba alias", async () => {
- const result = await call("catalog::resolve", { model: "jamba" });
- expect(result.id).toBe("jamba-1.5-large");
- });
-
- it("resolves cerebras alias", async () => {
- const result = await call("catalog::resolve", { model: "cerebras" });
- expect(result.id).toBe("cerebras-llama-3.3-70b");
- });
-
- it("resolves sambanova alias", async () => {
- const result = await call("catalog::resolve", { model: "sambanova" });
- expect(result.id).toBe("samba-llama-3.1-405b");
- });
-
- it("resolves hf alias", async () => {
- const result = await call("catalog::resolve", { model: "hf" });
- expect(result.id).toBe("hf-llama-3.3-70b");
- });
-
- it("resolves replicate alias", async () => {
- const result = await call("catalog::resolve", { model: "replicate" });
- expect(result.id).toBe("replicate-llama-3.3-70b");
- });
-
- it("resolves sonar alias", async () => {
- const result = await call("catalog::resolve", { model: "sonar" });
- expect(result.id).toBe("sonar-pro");
- });
-
- it("resolves llama alias", async () => {
- const result = await call("catalog::resolve", { model: "llama" });
- expect(result.id).toBe("llama-3.3-70b");
- });
-
- it("resolves mistral alias", async () => {
- const result = await call("catalog::resolve", { model: "mistral" });
- expect(result.id).toBe("mistral-large");
- });
-
- it("resolves moonshot alias", async () => {
- const result = await call("catalog::resolve", { model: "moonshot" });
- expect(result.id).toBe("moonshot-v1-128k");
- });
-
- it("resolves o3 alias", async () => {
- const result = await call("catalog::resolve", { model: "o3" });
- expect(result.id).toBe("o3");
- });
-
- it("resolves o4 alias", async () => {
- const result = await call("catalog::resolve", { model: "o4" });
- expect(result.id).toBe("o4-mini");
- });
-
- it("returns full model entry with all fields", async () => {
- const result = await call("catalog::resolve", { model: "opus" });
- expect(result.provider).toBe("anthropic");
- expect(result.name).toContain("Opus");
- expect(result.tier).toBe("frontier");
- expect(result.contextWindow).toBe(200000);
- expect(result.supportsTools).toBe(true);
- expect(result.supportsVision).toBe(true);
- });
-});
-
-describe("catalog::providers", () => {
- it("returns all providers", async () => {
- const result = await call("catalog::providers", {});
- expect(result.length).toBeGreaterThanOrEqual(27);
- });
-
- it("includes anthropic provider", async () => {
- const result = await call("catalog::providers", {});
- expect(result.some((p: any) => p.id === "anthropic")).toBe(true);
- });
-
- it("includes openai provider", async () => {
- const result = await call("catalog::providers", {});
- expect(result.some((p: any) => p.id === "openai")).toBe(true);
- });
-
- it("includes ollama as local provider", async () => {
- const result = await call("catalog::providers", {});
- const ollama = result.find((p: any) => p.id === "ollama");
- expect(ollama).toBeDefined();
- expect(ollama.local).toBe(true);
- });
-
- it("each provider has required fields", async () => {
- const result = await call("catalog::providers", {});
- for (const p of result) {
- expect(p.id).toBeDefined();
- expect(p.name).toBeDefined();
- expect(p.baseUrl).toBeDefined();
- expect(p.envKey).toBeDefined();
- expect(typeof p.local).toBe("boolean");
- expect(p.driver).toBeDefined();
- }
- });
-
- it("includes model count for each provider", async () => {
- const result = await call("catalog::providers", {});
- for (const p of result) {
- expect(typeof p.modelCount).toBe("number");
- }
- });
-
- it("includes availability status", async () => {
- const result = await call("catalog::providers", {});
- for (const p of result) {
- expect(typeof p.available).toBe("boolean");
- }
- });
-
- it("local providers are always available", async () => {
- const result = await call("catalog::providers", {});
- const locals = result.filter((p: any) => p.local);
- expect(locals.every((p: any) => p.available)).toBe(true);
- });
-
- it("includes bedrock provider", async () => {
- const result = await call("catalog::providers", {});
- const bedrock = result.find((p: any) => p.id === "bedrock");
- expect(bedrock).toBeDefined();
- expect(bedrock.driver).toBe("bedrock");
- });
-
- it("includes gemini provider with correct driver", async () => {
- const result = await call("catalog::providers", {});
- const gemini = result.find((p: any) => p.id === "gemini");
- expect(gemini.driver).toBe("gemini");
- });
-});
-
-describe("catalog::aliases", () => {
- it("returns alias map", async () => {
- const result = await call("catalog::aliases", {});
- expect(typeof result).toBe("object");
- });
-
- it("includes opus alias", async () => {
- const result = await call("catalog::aliases", {});
- expect(result.opus).toBe("claude-opus-4-6");
- });
-
- it("includes sonnet alias", async () => {
- const result = await call("catalog::aliases", {});
- expect(result.sonnet).toBe("claude-sonnet-4-6");
- });
-
- it("includes convenience aliases", async () => {
- const result = await call("catalog::aliases", {});
- expect(result.fast).toBeDefined();
- expect(result.cheap).toBeDefined();
- expect(result.smart).toBeDefined();
- expect(result.best).toBeDefined();
- });
-
- it("has more than 30 aliases", async () => {
- const result = await call("catalog::aliases", {});
- expect(Object.keys(result).length).toBeGreaterThan(30);
- });
-});
-
-describe("catalog::provider_test", () => {
- it("throws on unknown provider", async () => {
- await expect(
- call("catalog::provider_test", { providerId: "nonexistent" }),
- ).rejects.toThrow("Unknown provider");
- });
-
- it("returns not reachable when env key not set", async () => {
- const orig = process.env.ANTHROPIC_API_KEY;
- delete process.env.ANTHROPIC_API_KEY;
- const result = await call("catalog::provider_test", { providerId: "anthropic" });
- expect(result.reachable).toBe(false);
- expect(result.reason).toContain("not set");
- if (orig) process.env.ANTHROPIC_API_KEY = orig;
- });
-});
diff --git a/src/__tests__/model-catalog.test.ts b/src/__tests__/model-catalog.test.ts
deleted file mode 100644
index 0003aac..0000000
--- a/src/__tests__/model-catalog.test.ts
+++ /dev/null
@@ -1,194 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const kvStore: Record> = {};
-function getScope(scope: string) {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-function resetKv() {
- for (const key of Object.keys(kvStore)) delete kvStore[key];
-}
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- return null;
-});
-
-const handlers: Record = {};
-const mockTriggerVoid = vi.fn();
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-beforeEach(() => {
- resetKv();
- mockTrigger.mockClear();
-});
-
-beforeAll(async () => {
- await import("../model-catalog.js");
-});
-
-async function call(id: string, input?: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input || {});
-}
-
-describe("catalog::models", () => {
- it("returns all models when no filters", async () => {
- const result = await call("catalog::models");
- expect(result.length).toBeGreaterThan(10);
- });
-
- it("filters by tier", async () => {
- const result = await call("catalog::models", { tier: "frontier" });
- expect(result.length).toBeGreaterThan(0);
- expect(result.every((m: any) => m.tier === "frontier")).toBe(true);
- });
-
- it("filters by provider", async () => {
- const result = await call("catalog::models", { provider: "anthropic" });
- expect(result.length).toBe(3);
- expect(result.every((m: any) => m.provider === "anthropic")).toBe(true);
- });
-
- it("filters by supportsTools", async () => {
- const result = await call("catalog::models", { supportsTools: true });
- expect(result.every((m: any) => m.supportsTools === true)).toBe(true);
- });
-
- it("combines tier and provider filters", async () => {
- const result = await call("catalog::models", {
- tier: "smart",
- provider: "anthropic",
- });
- expect(result.length).toBe(1);
- expect(result[0].id).toBe("claude-sonnet-4-6");
- });
-
- it("returns empty array for non-matching filter", async () => {
- const result = await call("catalog::models", {
- provider: "nonexistent_provider",
- });
- expect(result).toHaveLength(0);
- });
-
- it("includes all expected model fields", async () => {
- const result = await call("catalog::models", { provider: "anthropic" });
- const model = result[0];
- expect(model).toHaveProperty("id");
- expect(model).toHaveProperty("provider");
- expect(model).toHaveProperty("name");
- expect(model).toHaveProperty("tier");
- expect(model).toHaveProperty("contextWindow");
- expect(model).toHaveProperty("maxOutput");
- expect(model).toHaveProperty("inputPrice");
- expect(model).toHaveProperty("outputPrice");
- expect(model).toHaveProperty("supportsTools");
- expect(model).toHaveProperty("supportsVision");
- });
-});
-
-describe("catalog::resolve - alias resolution", () => {
- it("resolves 'opus' alias to claude-opus-4-6", async () => {
- const result = await call("catalog::resolve", { model: "opus" });
- expect(result.id).toBe("claude-opus-4-6");
- expect(result.provider).toBe("anthropic");
- });
-
- it("resolves 'sonnet' alias", async () => {
- const result = await call("catalog::resolve", { model: "sonnet" });
- expect(result.id).toBe("claude-sonnet-4-6");
- });
-
- it("resolves 'haiku' alias", async () => {
- const result = await call("catalog::resolve", { model: "haiku" });
- expect(result.id).toBe("claude-haiku-4-5");
- });
-
- it("resolves 'gpt4' alias to gpt-4o", async () => {
- const result = await call("catalog::resolve", { model: "gpt4" });
- expect(result.id).toBe("gpt-4o");
- });
-
- it("resolves 'flash' alias to gemini-2.5-flash", async () => {
- const result = await call("catalog::resolve", { model: "flash" });
- expect(result.id).toBe("gemini-2.5-flash");
- });
-
- it("resolves semantic alias 'best' to claude-opus-4-6", async () => {
- const result = await call("catalog::resolve", { model: "best" });
- expect(result.id).toBe("claude-opus-4-6");
- });
-
- it("resolves semantic alias 'fast' to claude-haiku-4-5", async () => {
- const result = await call("catalog::resolve", { model: "fast" });
- expect(result.id).toBe("claude-haiku-4-5");
- });
-
- it("resolves direct model ID", async () => {
- const result = await call("catalog::resolve", { model: "gpt-4o" });
- expect(result.id).toBe("gpt-4o");
- });
-
- it("throws for unknown model", async () => {
- await expect(
- call("catalog::resolve", { model: "nonexistent-model-xyz" }),
- ).rejects.toThrow("Unknown model");
- });
-
- it("is case-insensitive for aliases", async () => {
- const result = await call("catalog::resolve", { model: "OPUS" });
- expect(result.id).toBe("claude-opus-4-6");
- });
-});
-
-describe("catalog::providers", () => {
- it("returns all providers", async () => {
- const result = await call("catalog::providers");
- expect(result.length).toBeGreaterThan(20);
- });
-
- it("includes model count per provider", async () => {
- const result = await call("catalog::providers");
- const anthropic = result.find((p: any) => p.id === "anthropic");
- expect(anthropic.modelCount).toBe(3);
- });
-
- it("marks local providers correctly", async () => {
- const result = await call("catalog::providers");
- const ollama = result.find((p: any) => p.id === "ollama");
- expect(ollama.local).toBe(true);
- });
-
- it("checks availability based on env vars", async () => {
- const result = await call("catalog::providers");
- const ollama = result.find((p: any) => p.id === "ollama");
- expect(ollama.available).toBe(true);
- });
-});
-
-describe("catalog::aliases", () => {
- it("returns all aliases", async () => {
- const result = await call("catalog::aliases");
- expect(result.opus).toBe("claude-opus-4-6");
- expect(result.sonnet).toBe("claude-sonnet-4-6");
- expect(result.fast).toBe("claude-haiku-4-5");
- });
-});
diff --git a/src/__tests__/recovery.test.ts b/src/__tests__/recovery.test.ts
deleted file mode 100644
index 59cd967..0000000
--- a/src/__tests__/recovery.test.ts
+++ /dev/null
@@ -1,221 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const kvStore: Record> = {};
-function getScope(scope: string) {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-function resetKv() {
- for (const key of Object.keys(kvStore)) delete kvStore[key];
-}
-function seedKv(scope: string, key: string, value: unknown) {
- getScope(scope).set(key, value);
-}
-
-const handlers: Record = {};
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "state::list") {
- return [...getScope(data.scope).entries()].map(([key, value]) => ({
- key,
- value,
- }));
- }
- if (fnId === "state::update") {
- const scope = getScope(data.scope);
- const current: any = scope.get(data.key) || {};
- for (const op of data.operations || []) {
- if (op.type === "increment")
- current[op.path] = (current[op.path] || 0) + op.value;
- }
- scope.set(data.key, current);
- return current;
- }
- if (fnId === "recovery::validate") {
- const handler = handlers["recovery::validate"];
- if (handler) return handler(data);
- return null;
- }
- if (fnId === "recovery::classify") {
- const handler = handlers["recovery::classify"];
- if (handler) return handler(data);
- return null;
- }
- if (fnId === "lifecycle::transition") {
- return { transitioned: true };
- }
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
- Logger: class { info() {} warn() {} error() {} },
-}));
-
-vi.mock("@agentos/shared/config", () => ({
- ENGINE_URL: "ws://localhost:3111",
- OTEL_CONFIG: {},
- registerShutdown: vi.fn(),
-}));
-vi.mock("@agentos/shared/metrics", () => ({
- recordMetric: vi.fn(),
-}));
-vi.mock("@agentos/shared/errors", () => ({
- safeCall: async (fn: Function, fallback: any, _context?: any) => {
- try {
- return await fn();
- } catch {
- return fallback;
- }
- },
-}));
-
-beforeEach(() => {
- resetKv();
- mockTrigger.mockClear();
- mockTriggerVoid.mockClear();
-});
-
-beforeAll(async () => {
- await import("../recovery.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("recovery::classify", () => {
- it("classifies healthy agent", async () => {
- seedKv("lifecycle:a1", "state", { state: "working" });
- seedKv("sessions:a1", "s1", { lastActiveAt: Date.now() });
-
- const result = await call("recovery::classify", { agentId: "a1" });
- expect(result.classification).toBe("healthy");
- });
-
- it("classifies degraded agent (stale)", async () => {
- seedKv("lifecycle:a1", "state", { state: "working" });
- seedKv("sessions:a1", "s1", {
- lastActiveAt: Date.now() - 60 * 60 * 1000,
- });
-
- const result = await call("recovery::classify", { agentId: "a1" });
- expect(result.classification).toBe("degraded");
- });
-
- it("classifies dead agent (failed lifecycle)", async () => {
- seedKv("lifecycle:a1", "state", { state: "failed" });
- seedKv("sessions:a1", "s1", { lastActiveAt: Date.now() });
-
- const result = await call("recovery::classify", { agentId: "a1" });
- expect(result.classification).toBe("dead");
- });
-
- it("classifies unrecoverable agent (terminal + circuit breaker)", async () => {
- seedKv("lifecycle:a1", "state", { state: "terminated" });
- seedKv("circuit_breakers", "a1", { state: "open" });
-
- const result = await call("recovery::classify", { agentId: "a1" });
- expect(result.classification).toBe("unrecoverable");
- });
-
- it("classifies degraded for blocked lifecycle", async () => {
- seedKv("lifecycle:a1", "state", { state: "blocked" });
- seedKv("sessions:a1", "s1", { lastActiveAt: Date.now() });
-
- const result = await call("recovery::classify", { agentId: "a1" });
- expect(result.classification).toBe("degraded");
- });
-});
-
-describe("recovery::recover", () => {
- it("sends wake-up for degraded agents", async () => {
- seedKv("lifecycle:a1", "state", { state: "blocked" });
- seedKv("sessions:a1", "s1", { lastActiveAt: Date.now() });
-
- const result = await call("recovery::recover", { agentId: "a1" });
- expect(result.action).toBe("wake_up");
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "tool::agent_send",
- expect.objectContaining({ targetAgentId: "a1" }),
- );
- });
-
- it("restarts dead agents", async () => {
- seedKv("lifecycle:a1", "state", { state: "failed" });
- seedKv("sessions:a1", "s1", { lastActiveAt: Date.now() });
-
- const result = await call("recovery::recover", { agentId: "a1" });
- expect(result.action).toBe("restart");
- expect(mockTrigger).toHaveBeenCalledWith(
- "lifecycle::transition",
- expect.objectContaining({ agentId: "a1", newState: "recovering" }),
- );
- });
-
- it("stops after max recovery attempts", async () => {
- seedKv("recovery_attempts", "a1", { count: 3 });
-
- const result = await call("recovery::recover", { agentId: "a1" });
- expect(result.action).toBe("exhausted");
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "hook::fire",
- expect.objectContaining({ type: "RecoveryExhausted" }),
- );
- });
-
- it("escalates unrecoverable agents", async () => {
- seedKv("lifecycle:a1", "state", { state: "failed" });
- seedKv("circuit_breakers", "a1", { state: "open" });
- seedKv("memory:a1", "_health", { healthy: false });
-
- const result = await call("recovery::recover", { agentId: "a1" });
- expect(result.action).toBe("escalate");
- });
-
- it("takes no action for healthy agents", async () => {
- seedKv("lifecycle:a1", "state", { state: "working" });
- seedKv("sessions:a1", "s1", { lastActiveAt: Date.now() });
-
- const result = await call("recovery::recover", { agentId: "a1" });
- expect(result.action).toBe("none");
- });
-});
-
-describe("recovery::scan", () => {
- it("scans all agents", async () => {
- seedKv("agents", "a1", { id: "a1" });
- seedKv("agents", "a2", { id: "a2" });
- seedKv("lifecycle:a1", "state", { state: "working" });
- seedKv("lifecycle:a2", "state", { state: "blocked" });
- seedKv("sessions:a1", "s1", { lastActiveAt: Date.now() });
-
- const result = await call("recovery::scan", {});
- expect(result.scannedAt).toBeDefined();
- expect(result.agents.length).toBe(2);
- });
-
- it("returns empty when no agents", async () => {
- const result = await call("recovery::scan", {});
- expect(result.agents.length).toBe(0);
- });
-});
diff --git a/src/__tests__/shared-errors.test.ts b/src/__tests__/shared-errors.test.ts
deleted file mode 100644
index 3572727..0000000
--- a/src/__tests__/shared-errors.test.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import { describe, it, expect, vi } from "vitest";
-import {
- AppError,
- classifyError,
- withRetry,
- safeCall,
- logError,
-} from "@agentos/shared/errors";
-import {
- safeInt,
- safeString,
- safeArray,
- safePagination,
-} from "@agentos/shared/validate";
-
-describe("AppError", () => {
- it("sets code, context, retryable", () => {
- const err = new AppError("boom", {
- code: "E_TEST",
- context: { key: "val" },
- retryable: true,
- });
- expect(err.message).toBe("boom");
- expect(err.code).toBe("E_TEST");
- expect(err.context).toEqual({ key: "val" });
- expect(err.retryable).toBe(true);
- expect(err.name).toBe("AppError");
- });
-
- it("defaults retryable to false", () => {
- const err = new AppError("fail", { code: "E_FAIL" });
- expect(err.retryable).toBe(false);
- });
-});
-
-describe("classifyError", () => {
- it("returns transient for retryable AppError", () => {
- const err = new AppError("retry me", { code: "E_TEMP", retryable: true });
- expect(classifyError(err)).toBe("transient");
- });
-
- it("returns permanent for non-retryable AppError", () => {
- const err = new AppError("no retry", { code: "E_PERM", retryable: false });
- expect(classifyError(err)).toBe("permanent");
- });
-
- it("returns transient for timeout Error", () => {
- expect(classifyError(new Error("Connection timeout"))).toBe("transient");
- });
-
- it("returns degraded for rate limit Error", () => {
- expect(classifyError(new Error("rate limit exceeded"))).toBe("degraded");
- });
-
- it("returns permanent for unknown error", () => {
- expect(classifyError("some string")).toBe("permanent");
- });
-});
-
-describe("withRetry", () => {
- it("succeeds on first try", async () => {
- const fn = vi.fn().mockResolvedValue("ok");
- const result = await withRetry(fn, 3, 1);
- expect(result).toBe("ok");
- expect(fn).toHaveBeenCalledTimes(1);
- });
-
- it("retries transient errors", async () => {
- let attempt = 0;
- const fn = vi.fn(async () => {
- attempt++;
- if (attempt < 3) throw new Error("Connection timeout");
- return "recovered";
- });
- const result = await withRetry(fn, 3, 1);
- expect(result).toBe("recovered");
- expect(fn).toHaveBeenCalledTimes(3);
- });
-
- it("does not retry permanent errors", async () => {
- const permanentErr = new AppError("fatal", {
- code: "E_FATAL",
- retryable: false,
- });
- const fn = vi.fn(async () => {
- throw permanentErr;
- });
- await expect(withRetry(fn, 3, 1)).rejects.toThrow("fatal");
- expect(fn).toHaveBeenCalledTimes(1);
- });
-});
-
-describe("safeCall", () => {
- it("returns result on success", async () => {
- const result = await safeCall(
- async () => 42,
- 0,
- { operation: "test" },
- );
- expect(result).toBe(42);
- });
-
- it("returns fallback on error", async () => {
- const result = await safeCall(
- async () => {
- throw new Error("oops");
- },
- "default",
- { operation: "test" },
- );
- expect(result).toBe("default");
- });
-});
-
-describe("logError", () => {
- it("writes to stderr", () => {
- const spy = vi.spyOn(console, "error").mockImplementation(() => {});
- logError(new Error("test error"), { operation: "unit-test" });
- expect(spy).toHaveBeenCalledTimes(1);
- const output = spy.mock.calls[0][0] as string;
- const parsed = JSON.parse(output);
- expect(parsed.level).toBe("error");
- expect(parsed.message).toBe("test error");
- expect(parsed.operation).toBe("unit-test");
- spy.mockRestore();
- });
-});
-
-describe("safeInt", () => {
- it("returns defaultVal for NaN", () => {
- expect(safeInt("abc", 0, 100, 50)).toBe(50);
- expect(safeInt(undefined, 0, 100, 50)).toBe(50);
- expect(safeInt(Infinity, 0, 100, 50)).toBe(50);
- });
-
- it("clamps to min/max", () => {
- expect(safeInt(-10, 0, 100, 50)).toBe(0);
- expect(safeInt(200, 0, 100, 50)).toBe(100);
- });
-
- it("truncates decimals", () => {
- expect(safeInt(3.7, 0, 100, 50)).toBe(3);
- expect(safeInt(9.9, 0, 100, 50)).toBe(9);
- });
-});
-
-describe("safeString", () => {
- it("returns empty for non-string", () => {
- expect(safeString(123)).toBe("");
- expect(safeString(null)).toBe("");
- expect(safeString(undefined)).toBe("");
- expect(safeString({})).toBe("");
- });
-
- it("trims and slices", () => {
- expect(safeString(" hello ")).toBe("hello");
- expect(safeString("abcdef", 3)).toBe("abc");
- expect(safeString(" abcdef ", 4)).toBe("abcd");
- });
-});
-
-describe("safeArray", () => {
- it("returns empty for non-array", () => {
- expect(safeArray("not array")).toEqual([]);
- expect(safeArray(null)).toEqual([]);
- expect(safeArray(42)).toEqual([]);
- });
-
- it("slices to maxLen", () => {
- expect(safeArray([1, 2, 3, 4, 5], 3)).toEqual([1, 2, 3]);
- });
-});
-
-describe("safePagination", () => {
- it("returns defaults for undefined", () => {
- const result = safePagination(undefined, undefined);
- expect(result.limit).toBe(100);
- expect(result.offset).toBe(0);
- });
-});
diff --git a/src/__tests__/shared-utils.test.ts b/src/__tests__/shared-utils.test.ts
deleted file mode 100644
index 9f5325c..0000000
--- a/src/__tests__/shared-utils.test.ts
+++ /dev/null
@@ -1,451 +0,0 @@
-import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
-import {
- splitMessage,
- assertNoSsrf,
- resolveAgent,
- requireAuth,
- verifySlackSignature,
- verifyTelegramUpdate,
- validateMcpCommand,
- stripSecretsFromEnv,
- sanitizeId,
-} from "@agentos/shared/utils";
-import { createHmac } from "crypto";
-
-describe("splitMessage", () => {
- it("returns single chunk when text fits within maxLen", () => {
- const result = splitMessage("hello world", 100);
- expect(result).toEqual(["hello world"]);
- });
-
- it("returns single chunk when text equals maxLen", () => {
- const text = "a".repeat(50);
- const result = splitMessage(text, 50);
- expect(result).toEqual([text]);
- });
-
- it("splits on newline boundaries", () => {
- const text = "line1\nline2\nline3";
- const result = splitMessage(text, 10);
- expect(result.length).toBeGreaterThan(1);
- expect(result.join("")).toBe(text);
- });
-
- it("falls back to maxLen split when no newline in first half", () => {
- const text = "a".repeat(100);
- const result = splitMessage(text, 30);
- expect(result[0]).toHaveLength(30);
- });
-
- it("handles empty string", () => {
- expect(splitMessage("", 10)).toEqual([""]);
- });
-
- it("preserves all content across chunks", () => {
- const text =
- "The quick brown fox jumps over the lazy dog\nAnother line here\nAnd another one";
- const chunks = splitMessage(text, 20);
- expect(chunks.join("")).toBe(text);
- });
-
- it("handles text with only newlines", () => {
- const text = "\n\n\n\n\n";
- const result = splitMessage(text, 3);
- expect(result.join("")).toBe(text);
- });
-
- it("handles single character maxLen", () => {
- const text = "abc";
- const result = splitMessage(text, 1);
- expect(result).toEqual(["a", "b", "c"]);
- });
-});
-
-describe("assertNoSsrf", () => {
- it("rejects localhost", async () => {
- await expect(assertNoSsrf("http://localhost:8080")).rejects.toThrow(
- "SSRF blocked",
- );
- });
-
- it("rejects 127.0.0.1", async () => {
- await expect(assertNoSsrf("http://127.0.0.1")).rejects.toThrow(
- "SSRF blocked",
- );
- });
-
- it("rejects 10.x.x.x", async () => {
- await expect(assertNoSsrf("http://10.0.0.1")).rejects.toThrow(
- "SSRF blocked",
- );
- });
-
- it("rejects 172.16.x.x", async () => {
- await expect(assertNoSsrf("http://172.16.0.1")).rejects.toThrow(
- "SSRF blocked",
- );
- });
-
- it("rejects 192.168.x.x", async () => {
- await expect(assertNoSsrf("http://192.168.1.1")).rejects.toThrow(
- "SSRF blocked",
- );
- });
-
- it("rejects 169.254.x.x link-local", async () => {
- await expect(assertNoSsrf("http://169.254.169.254")).rejects.toThrow(
- "SSRF blocked",
- );
- });
-
- it("rejects metadata.google.internal", async () => {
- await expect(
- assertNoSsrf("http://metadata.google.internal"),
- ).rejects.toThrow("SSRF blocked");
- });
-
- it("rejects 0.0.0.0", async () => {
- await expect(assertNoSsrf("http://0.0.0.0")).rejects.toThrow(
- "SSRF blocked",
- );
- });
-
- it("rejects .local domains", async () => {
- await expect(assertNoSsrf("http://myhost.local")).rejects.toThrow(
- "SSRF blocked",
- );
- });
-
- it("rejects invalid URLs", async () => {
- await expect(assertNoSsrf("not-a-url")).rejects.toThrow("Invalid URL");
- });
-
- it("allows public URLs", async () => {
- await expect(assertNoSsrf("https://example.com")).resolves.toBeUndefined();
- });
-
- it("rejects 172.31.x.x (end of private range)", async () => {
- await expect(assertNoSsrf("http://172.31.255.255")).rejects.toThrow(
- "SSRF blocked",
- );
- });
-
- it("rejects 100.64.x.x shared address space", async () => {
- await expect(assertNoSsrf("http://100.64.0.1")).rejects.toThrow(
- "SSRF blocked",
- );
- });
-
- it("rejects 0.x.x.x addresses", async () => {
- await expect(assertNoSsrf("http://0.0.0.1")).rejects.toThrow(
- "SSRF blocked",
- );
- });
-});
-
-describe("resolveAgent", () => {
- it("returns agentId from mapping", async () => {
- const trigger = vi.fn().mockResolvedValue({ agentId: "agent-42" });
- const sdk = { trigger };
- const result = await resolveAgent(sdk, "slack", "C123");
- expect(result).toBe("agent-42");
- expect(trigger).toHaveBeenCalledWith({
- function_id: "state::get",
- payload: { scope: "channel_agents", key: "slack:C123" },
- });
- });
-
- it("returns 'default' when no mapping exists", async () => {
- const trigger = vi.fn().mockResolvedValue(null);
- const sdk = { trigger };
- const result = await resolveAgent(sdk, "slack", "C999");
- expect(result).toBe("default");
- });
-
- it("returns 'default' when trigger fails", async () => {
- const trigger = vi.fn().mockRejectedValue(new Error("network"));
- const sdk = { trigger };
- const result = await resolveAgent(sdk, "discord", "guild-1");
- expect(result).toBe("default");
- });
-});
-
-describe("requireAuth", () => {
- const originalEnv = process.env.AGENTOS_API_KEY;
-
- afterEach(() => {
- if (originalEnv !== undefined) {
- process.env.AGENTOS_API_KEY = originalEnv;
- } else {
- delete process.env.AGENTOS_API_KEY;
- }
- });
-
- it("throws 500 when AGENTOS_API_KEY not set", () => {
- delete process.env.AGENTOS_API_KEY;
- const req = { headers: { authorization: "Bearer test" } };
- expect(() => requireAuth(req)).toThrow("AGENTOS_API_KEY not configured");
- });
-
- it("throws 401 when no authorization header", () => {
- process.env.AGENTOS_API_KEY = "secret123";
- const req = { headers: {} };
- expect(() => requireAuth(req)).toThrow("Unauthorized");
- });
-
- it("throws 401 when token is wrong", () => {
- process.env.AGENTOS_API_KEY = "secret123";
- const req = { headers: { authorization: "Bearer wrong" } };
- expect(() => requireAuth(req)).toThrow("Unauthorized");
- });
-
- it("passes when token matches", () => {
- process.env.AGENTOS_API_KEY = "secret123";
- const req = { headers: { authorization: "Bearer secret123" } };
- expect(() => requireAuth(req)).not.toThrow();
- });
-
- it("handles Bearer prefix case-insensitively", () => {
- process.env.AGENTOS_API_KEY = "mykey";
- const req = { headers: { authorization: "bearer mykey" } };
- expect(() => requireAuth(req)).not.toThrow();
- });
-
- it("sets statusCode 500 on missing key error", () => {
- delete process.env.AGENTOS_API_KEY;
- try {
- requireAuth({ headers: {} });
- } catch (e: any) {
- expect(e.statusCode).toBe(500);
- }
- });
-
- it("sets statusCode 401 on auth failure", () => {
- process.env.AGENTOS_API_KEY = "key";
- try {
- requireAuth({ headers: { authorization: "Bearer bad" } });
- } catch (e: any) {
- expect(e.statusCode).toBe(401);
- }
- });
-});
-
-describe("verifySlackSignature", () => {
- const secret = "test_signing_secret";
-
- function makeSlackRequest(body: string, timestamp: string) {
- const baseString = `v0:${timestamp}:${body}`;
- const sig =
- "v0=" + createHmac("sha256", secret).update(baseString).digest("hex");
- return {
- headers: {
- "x-slack-request-timestamp": timestamp,
- "x-slack-signature": sig,
- },
- body,
- };
- }
-
- it("validates correct signature", () => {
- const ts = String(Math.floor(Date.now() / 1000));
- const req = makeSlackRequest('{"text":"hello"}', ts);
- expect(() => verifySlackSignature(req, secret)).not.toThrow();
- });
-
- it("rejects missing headers", () => {
- expect(() => verifySlackSignature({ headers: {} }, secret)).toThrow(
- "Missing Slack signature headers",
- );
- });
-
- it("rejects stale timestamp", () => {
- const ts = String(Math.floor(Date.now() / 1000) - 600);
- const req = makeSlackRequest("body", ts);
- expect(() => verifySlackSignature(req, secret)).toThrow(
- "Stale Slack timestamp",
- );
- });
-
- it("rejects wrong signature", () => {
- const ts = String(Math.floor(Date.now() / 1000));
- const req = {
- headers: {
- "x-slack-request-timestamp": ts,
- "x-slack-signature": "v0=incorrect",
- },
- body: "test",
- };
- expect(() => verifySlackSignature(req, secret)).toThrow(
- "Invalid Slack signature",
- );
- });
-});
-
-describe("verifyTelegramUpdate", () => {
- it("returns true when token matches header", () => {
- const req = { headers: { "x-telegram-bot-api-secret-token": "mysecret" } };
- expect(verifyTelegramUpdate("mysecret", req)).toBe(true);
- });
-
- it("returns false when token does not match", () => {
- const req = { headers: { "x-telegram-bot-api-secret-token": "wrong" } };
- expect(verifyTelegramUpdate("mysecret", req)).toBe(false);
- });
-
- it("returns false when no secret token provided", () => {
- const req = { headers: { "x-telegram-bot-api-secret-token": "test" } };
- expect(verifyTelegramUpdate("", req)).toBe(false);
- });
-
- it("returns false when no header present", () => {
- const req = { headers: {} };
- expect(verifyTelegramUpdate("mysecret", req)).toBe(false);
- });
-});
-
-describe("validateMcpCommand", () => {
- it("allows 'npx'", () => {
- expect(() => validateMcpCommand("npx")).not.toThrow();
- });
-
- it("allows 'node'", () => {
- expect(() => validateMcpCommand("node")).not.toThrow();
- });
-
- it("allows 'python3'", () => {
- expect(() => validateMcpCommand("python3")).not.toThrow();
- });
-
- it("allows 'bun'", () => {
- expect(() => validateMcpCommand("bun")).not.toThrow();
- });
-
- it("allows 'deno'", () => {
- expect(() => validateMcpCommand("deno")).not.toThrow();
- });
-
- it("allows full path to allowed command", () => {
- expect(() => validateMcpCommand("/usr/bin/node")).not.toThrow();
- });
-
- it("rejects disallowed commands", () => {
- expect(() => validateMcpCommand("bash")).toThrow("MCP command not allowed");
- });
-
- it("rejects 'rm'", () => {
- expect(() => validateMcpCommand("rm")).toThrow("MCP command not allowed");
- });
-
- it("rejects 'curl'", () => {
- expect(() => validateMcpCommand("curl")).toThrow("MCP command not allowed");
- });
-});
-
-describe("stripSecretsFromEnv", () => {
- it("includes PATH", () => {
- const result = stripSecretsFromEnv();
- if (process.env.PATH) {
- expect(result.PATH).toBe(process.env.PATH);
- }
- });
-
- it("includes HOME", () => {
- const result = stripSecretsFromEnv();
- if (process.env.HOME) {
- expect(result.HOME).toBe(process.env.HOME);
- }
- });
-
- it("excludes unknown keys", () => {
- const original = process.env.MY_SECRET_TOKEN;
- process.env.MY_SECRET_TOKEN = "hidden";
- const result = stripSecretsFromEnv();
- expect(result.MY_SECRET_TOKEN).toBeUndefined();
- if (original !== undefined) {
- process.env.MY_SECRET_TOKEN = original;
- } else {
- delete process.env.MY_SECRET_TOKEN;
- }
- });
-
- it("excludes API keys", () => {
- const original = process.env.AGENTOS_API_KEY;
- process.env.AGENTOS_API_KEY = "secret";
- const result = stripSecretsFromEnv();
- expect(result.AGENTOS_API_KEY).toBeUndefined();
- if (original !== undefined) {
- process.env.AGENTOS_API_KEY = original;
- } else {
- delete process.env.AGENTOS_API_KEY;
- }
- });
-
- it("returns only safe keys", () => {
- const result = stripSecretsFromEnv();
- const safeKeys = new Set([
- "PATH",
- "HOME",
- "USER",
- "SHELL",
- "LANG",
- "LC_ALL",
- "TERM",
- "NODE_PATH",
- "NODE_ENV",
- "PYTHONPATH",
- ]);
- for (const key of Object.keys(result)) {
- expect(safeKeys.has(key)).toBe(true);
- }
- });
-});
-
-describe("sanitizeId", () => {
- it("returns valid alphanumeric id", () => {
- expect(sanitizeId("abc123")).toBe("abc123");
- });
-
- it("allows hyphens", () => {
- expect(sanitizeId("my-agent")).toBe("my-agent");
- });
-
- it("allows underscores", () => {
- expect(sanitizeId("my_agent")).toBe("my_agent");
- });
-
- it("allows colons", () => {
- expect(sanitizeId("agent:v1")).toBe("agent:v1");
- });
-
- it("allows dots", () => {
- expect(sanitizeId("agent.v1.0")).toBe("agent.v1.0");
- });
-
- it("rejects empty string", () => {
- expect(() => sanitizeId("")).toThrow("Invalid ID format");
- });
-
- it("rejects strings with spaces", () => {
- expect(() => sanitizeId("has space")).toThrow("Invalid ID format");
- });
-
- it("rejects strings with special characters", () => {
- expect(() => sanitizeId("agent@v1")).toThrow("Invalid ID format");
- });
-
- it("rejects strings longer than 256 chars", () => {
- expect(() => sanitizeId("a".repeat(257))).toThrow("Invalid ID format");
- });
-
- it("accepts exactly 256 char string", () => {
- expect(sanitizeId("a".repeat(256))).toBe("a".repeat(256));
- });
-
- it("sets statusCode 400 on error", () => {
- try {
- sanitizeId("");
- } catch (e: any) {
- expect(e.statusCode).toBe(400);
- }
- });
-});
diff --git a/src/__tests__/skills-extended.test.ts b/src/__tests__/skills-extended.test.ts
deleted file mode 100644
index 3ac81e7..0000000
--- a/src/__tests__/skills-extended.test.ts
+++ /dev/null
@@ -1,354 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const kvStore: Record> = {};
-function getScope(scope: string) {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-function resetKv() {
- for (const key of Object.keys(kvStore)) delete kvStore[key];
-}
-function seedKv(scope: string, key: string, value: unknown) {
- getScope(scope).set(key, value);
-}
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "state::delete") {
- getScope(data.scope).delete(data.key);
- return { ok: true };
- }
- if (fnId === "state::list") {
- return [...getScope(data.scope).entries()].map(([key, value]) => ({
- key,
- value,
- }));
- }
- if (fnId === "skillkit::search") {
- return { results: [{ id: "sk-1", name: "Found Skill" }] };
- }
- return null;
-});
-
-const handlers: Record = {};
-const mockTriggerVoid = vi.fn();
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- requireAuth: vi.fn(),
-}));
-
-beforeEach(() => {
- resetKv();
- mockTrigger.mockClear();
-});
-
-beforeAll(async () => {
- await import("../skills.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-function authReq(body: any) {
- return { headers: { authorization: "Bearer test" }, body, ...body };
-}
-
-describe("skill::list extended", () => {
- it("returns skills with all required fields", async () => {
- const result = await call("skill::list", authReq({}));
- for (const skill of result.slice(0, 5)) {
- expect(skill.id).toBeDefined();
- expect(skill.name).toBeDefined();
- expect(skill.category).toBeDefined();
- expect(skill.tags).toBeDefined();
- expect(Array.isArray(skill.tags)).toBe(true);
- }
- });
-
- it("filters cloud category returns multiple skills", async () => {
- const result = await call("skill::list", authReq({ category: "cloud" }));
- expect(result.length).toBeGreaterThanOrEqual(5);
- const ids = result.map((s: any) => s.id);
- expect(ids).toContain("aws");
- expect(ids).toContain("kubernetes");
- expect(ids).toContain("terraform");
- });
-
- it("filters devops category", async () => {
- const result = await call("skill::list", authReq({ category: "devops" }));
- expect(result.length).toBeGreaterThanOrEqual(2);
- expect(result.some((s: any) => s.id === "ci-cd")).toBe(true);
- });
-
- it("filters languages category", async () => {
- const result = await call("skill::list", authReq({ category: "languages" }));
- expect(result.length).toBeGreaterThanOrEqual(5);
- const ids = result.map((s: any) => s.id);
- expect(ids).toContain("python-expert");
- expect(ids).toContain("typescript-expert");
- });
-
- it("filters data category", async () => {
- const result = await call("skill::list", authReq({ category: "data" }));
- expect(result.length).toBeGreaterThanOrEqual(3);
- });
-
- it("filters security category", async () => {
- const result = await call("skill::list", authReq({ category: "security" }));
- expect(result.length).toBeGreaterThanOrEqual(2);
- expect(result.some((s: any) => s.id === "security-audit")).toBe(true);
- });
-
- it("filters by aws tag", async () => {
- const result = await call("skill::list", authReq({ tag: "aws" }));
- expect(result.length).toBeGreaterThanOrEqual(1);
- expect(result[0].tags).toContain("aws");
- });
-
- it("filters by python tag", async () => {
- const result = await call("skill::list", authReq({ tag: "python" }));
- expect(result.length).toBeGreaterThanOrEqual(1);
- });
-
- it("combines installed and bundled without duplicates", async () => {
- seedKv("skills", "unique-custom", {
- id: "unique-custom",
- name: "Unique",
- category: "custom",
- tags: [],
- source: "installed",
- });
- const result = await call("skill::list", authReq({}));
- const customCount = result.filter((s: any) => s.id === "unique-custom").length;
- expect(customCount).toBe(1);
- });
-});
-
-describe("skill::install extended", () => {
- it("generates UUID when no name or id", async () => {
- const result = await call("skill::install", authReq({}));
- expect(result.installed).toBe(true);
- expect(result.id).toBeDefined();
- expect(result.id.length).toBeGreaterThan(0);
- });
-
- it("preserves content in stored skill", async () => {
- await call("skill::install", authReq({
- id: "content-test",
- content: "Skill instructions here",
- }));
- const stored = getScope("skills").get("content-test") as any;
- expect(stored.content).toBe("Skill instructions here");
- });
-
- it("preserves tags in stored skill", async () => {
- await call("skill::install", authReq({
- id: "tags-test",
- tags: ["devops", "k8s"],
- }));
- const stored = getScope("skills").get("tags-test") as any;
- expect(stored.tags).toEqual(["devops", "k8s"]);
- });
-
- it("defaults empty tags array", async () => {
- await call("skill::install", authReq({ id: "no-tags" }));
- const stored = getScope("skills").get("no-tags") as any;
- expect(stored.tags).toEqual([]);
- });
-
- it("defaults description to empty string", async () => {
- await call("skill::install", authReq({ id: "no-desc" }));
- const stored = getScope("skills").get("no-desc") as any;
- expect(stored.description).toBe("");
- });
-
- it("sets version to 1.0.0", async () => {
- await call("skill::install", authReq({ id: "version-test" }));
- const stored = getScope("skills").get("version-test") as any;
- expect(stored.version).toBe("1.0.0");
- });
-
- it("sets installedAt timestamp", async () => {
- const before = Date.now();
- await call("skill::install", authReq({ id: "ts-test" }));
- const stored = getScope("skills").get("ts-test") as any;
- expect(stored.installedAt).toBeGreaterThanOrEqual(before);
- });
-});
-
-describe("skill::uninstall extended", () => {
- it("removes installed skill from state", async () => {
- seedKv("skills", "to-remove", { id: "to-remove", source: "installed" });
- await call("skill::uninstall", authReq({ id: "to-remove" }));
- expect(getScope("skills").has("to-remove")).toBe(false);
- });
-
- it("rejects uninstall of docker bundled skill", async () => {
- await expect(
- call("skill::uninstall", authReq({ id: "docker" })),
- ).rejects.toThrow("Cannot uninstall bundled skill");
- });
-
- it("rejects uninstall of terraform bundled skill", async () => {
- await expect(
- call("skill::uninstall", authReq({ id: "terraform" })),
- ).rejects.toThrow("Cannot uninstall bundled skill");
- });
-
- it("rejects uninstall of git-expert bundled skill", async () => {
- await expect(
- call("skill::uninstall", authReq({ id: "git-expert" })),
- ).rejects.toThrow("Cannot uninstall bundled skill");
- });
-});
-
-describe("skill::get", () => {
- it("returns bundled skill by id", async () => {
- const result = await call("skill::get", { id: "aws" });
- expect(result.name).toBe("AWS Expert");
- expect(result.source).toBe("bundled");
- });
-
- it("returns installed skill from state", async () => {
- seedKv("skills", "custom-get", {
- id: "custom-get",
- name: "Custom Get",
- source: "installed",
- });
- const result = await call("skill::get", { id: "custom-get" });
- expect(result.name).toBe("Custom Get");
- });
-
- it("returns kubernetes bundled skill", async () => {
- const result = await call("skill::get", { id: "kubernetes" });
- expect(result.name).toBe("Kubernetes Expert");
- });
-});
-
-describe("skill::search extended", () => {
- it("search is case-insensitive", async () => {
- const result = await call("skill::search", authReq({ query: "docker" }));
- expect(result.some((s: any) => s.id === "docker")).toBe(true);
- const result2 = await call("skill::search", authReq({ query: "DOCKER" }));
- expect(result2.some((s: any) => s.id === "docker")).toBe(true);
- });
-
- it("matches partial words in description", async () => {
- const result = await call("skill::search", authReq({ query: "orchestr" }));
- expect(result.length).toBeGreaterThan(0);
- });
-
- it("searches across installed and bundled", async () => {
- seedKv("skills", "custom-searchable", {
- id: "custom-searchable",
- name: "Custom Searchable",
- description: "unique-xyzzy",
- tags: [],
- });
- const result = await call("skill::search", authReq({ query: "unique-xyzzy" }));
- expect(result.some((s: any) => s.id === "custom-searchable")).toBe(true);
- });
-});
-
-describe("skill::parse extended", () => {
- it("handles multi-line body", async () => {
- const content = `---
-name: multi-line
----
-Line 1
-Line 2
-Line 3`;
- const result = await call("skill::parse", { content });
- expect(result.content).toBe("Line 1\nLine 2\nLine 3");
- });
-
- it("handles frontmatter with colons in value", async () => {
- const content = `---
-name: colon-test
-description: HTTP server: port 8080
----
-Body.`;
- const result = await call("skill::parse", { content });
- expect(result.description).toBe("HTTP server: port 8080");
- });
-
- it("defaults version to 1.0.0", async () => {
- const content = `---
-name: no-version
----
-Content.`;
- const result = await call("skill::parse", { content });
- expect(result.version).toBe("1.0.0");
- });
-
- it("parses comma-separated tags", async () => {
- const content = `---
-name: tagged
-tags: [a, b, c]
----
-Body.`;
- const result = await call("skill::parse", { content });
- expect(result.tags).toEqual(["a", "b", "c"]);
- });
-});
-
-describe("skill::marketplace_search", () => {
- it("registers the handler", () => {
- expect(handlers["skill::marketplace_search"]).toBeDefined();
- });
-
- it("rejects queries shorter than 2 chars", async () => {
- await expect(
- call("skill::marketplace_search", authReq({ query: "a" })),
- ).rejects.toThrow("at least 2 characters");
- });
-
- it("returns results from skillkit", async () => {
- const result = await call("skill::marketplace_search", authReq({
- query: "kubernetes",
- limit: 5,
- }));
- expect(result.results).toBeDefined();
- });
-
- it("defaults limit to 10", async () => {
- await call("skill::marketplace_search", authReq({ query: "test" }));
- const skCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "skillkit::search",
- );
- expect(skCalls[0][1].limit).toBe(10);
- });
-
- it("handles skillkit unavailability gracefully", async () => {
- mockTrigger.mockImplementationOnce(async (fnId: string) => {
- if (fnId === "skillkit::search") throw new Error("Connection refused");
- return null;
- });
- const result = await call("skill::marketplace_search", authReq({
- query: "test-unavail",
- }));
- expect(result.error).toContain("SkillKit unavailable");
- });
-});
diff --git a/src/__tests__/skills.test.ts b/src/__tests__/skills.test.ts
deleted file mode 100644
index d1a6a36..0000000
--- a/src/__tests__/skills.test.ts
+++ /dev/null
@@ -1,271 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-
-const kvStore: Record> = {};
-function getScope(scope: string) {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-function resetKv() {
- for (const key of Object.keys(kvStore)) delete kvStore[key];
-}
-function seedKv(scope: string, key: string, value: unknown) {
- getScope(scope).set(key, value);
-}
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "state::delete") {
- getScope(data.scope).delete(data.key);
- return { ok: true };
- }
- if (fnId === "state::list") {
- return [...getScope(data.scope).entries()].map(([key, value]) => ({
- key,
- value,
- }));
- }
- return null;
-});
-
-const mockTriggerVoid = vi.fn();
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- requireAuth: vi.fn(),
-}));
-
-beforeEach(() => {
- resetKv();
- mockTrigger.mockClear();
-});
-
-beforeAll(async () => {
- await import("../skills.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-function authReq(body: any) {
- return { headers: { authorization: "Bearer test" }, body, ...body };
-}
-
-describe("skill::list", () => {
- it("returns bundled skills", async () => {
- const result = await call("skill::list", authReq({}));
- expect(result.length).toBeGreaterThan(30);
- expect(result.some((s: any) => s.source === "bundled")).toBe(true);
- });
-
- it("filters by category", async () => {
- const result = await call("skill::list", authReq({ category: "cloud" }));
- expect(result.length).toBeGreaterThan(0);
- expect(result.every((s: any) => s.category === "cloud")).toBe(true);
- });
-
- it("filters by tag", async () => {
- const result = await call("skill::list", authReq({ tag: "kubernetes" }));
- expect(result.length).toBeGreaterThan(0);
- expect(result.every((s: any) => s.tags.includes("kubernetes"))).toBe(true);
- });
-
- it("includes installed skills alongside bundled", async () => {
- seedKv("skills", "custom-1", {
- id: "custom-1",
- name: "Custom Skill",
- description: "Custom",
- content: "",
- category: "custom",
- tags: [],
- version: "1.0.0",
- source: "installed",
- });
- const result = await call("skill::list", authReq({}));
- const custom = result.find((s: any) => s.id === "custom-1");
- expect(custom).toBeDefined();
- expect(custom.source).toBe("installed");
- });
-
- it("returns empty when filtering by nonexistent category", async () => {
- const result = await call(
- "skill::list",
- authReq({ category: "nonexistent" }),
- );
- expect(result).toHaveLength(0);
- });
-});
-
-describe("skill::install", () => {
- it("installs a custom skill", async () => {
- const result = await call(
- "skill::install",
- authReq({
- name: "My Skill",
- description: "Test skill",
- content: "skill content here",
- category: "custom",
- tags: ["test"],
- }),
- );
- expect(result.installed).toBe(true);
- expect(result.id).toBe("my-skill");
- });
-
- it("generates ID from name", async () => {
- const result = await call(
- "skill::install",
- authReq({
- name: "API Testing Expert",
- }),
- );
- expect(result.id).toBe("api-testing-expert");
- });
-
- it("uses provided ID over generated one", async () => {
- const result = await call(
- "skill::install",
- authReq({
- id: "explicit-id",
- name: "Some Name",
- }),
- );
- expect(result.id).toBe("explicit-id");
- });
-
- it("sets source to 'installed'", async () => {
- await call("skill::install", authReq({ name: "Installed Skill" }));
- const stored = getScope("skills").get("installed-skill") as any;
- expect(stored.source).toBe("installed");
- expect(stored.installedAt).toBeDefined();
- });
-
- it("defaults category to 'custom'", async () => {
- await call("skill::install", authReq({ name: "No Category" }));
- const stored = getScope("skills").get("no-category") as any;
- expect(stored.category).toBe("custom");
- });
-});
-
-describe("skill::uninstall", () => {
- it("removes an installed skill", async () => {
- seedKv("skills", "removable", {
- id: "removable",
- source: "installed",
- });
- const result = await call("skill::uninstall", authReq({ id: "removable" }));
- expect(result.uninstalled).toBe(true);
- });
-
- it("prevents uninstalling bundled skills", async () => {
- await expect(
- call("skill::uninstall", authReq({ id: "aws" })),
- ).rejects.toThrow("Cannot uninstall bundled skill");
- });
-
- it("prevents uninstalling 'kubernetes' bundled skill", async () => {
- await expect(
- call("skill::uninstall", authReq({ id: "kubernetes" })),
- ).rejects.toThrow("Cannot uninstall bundled skill");
- });
-});
-
-describe("skill::search", () => {
- it("searches by name", async () => {
- const result = await call("skill::search", authReq({ query: "Docker" }));
- expect(result.length).toBeGreaterThan(0);
- expect(result.some((s: any) => s.id === "docker")).toBe(true);
- });
-
- it("searches by description", async () => {
- const result = await call(
- "skill::search",
- authReq({ query: "containers" }),
- );
- expect(result.length).toBeGreaterThan(0);
- });
-
- it("searches by tags", async () => {
- const result = await call(
- "skill::search",
- authReq({ query: "kubernetes" }),
- );
- expect(result.length).toBeGreaterThan(0);
- });
-
- it("includes installed skills in search", async () => {
- seedKv("skills", "custom-search", {
- id: "custom-search",
- name: "Search Test",
- description: "searchable content",
- tags: ["findme"],
- });
- const result = await call("skill::search", authReq({ query: "findme" }));
- expect(result.some((s: any) => s.id === "custom-search")).toBe(true);
- });
-
- it("returns empty for no match", async () => {
- const result = await call(
- "skill::search",
- authReq({ query: "zzz_no_match_xyz" }),
- );
- expect(result).toHaveLength(0);
- });
-});
-
-describe("skill::parse - SKILL.md parsing", () => {
- it("parses valid SKILL.md with frontmatter", async () => {
- const content = `---
-name: test-skill
-description: A test skill
-version: 2.0.0
-tags: [testing, unit]
----
-This is the skill content.`;
- const result = await call("skill::parse", { content });
- expect(result.name).toBe("test-skill");
- expect(result.description).toBe("A test skill");
- expect(result.version).toBe("2.0.0");
- expect(result.tags).toEqual(["testing", "unit"]);
- expect(result.content).toBe("This is the skill content.");
- });
-
- it("handles content without frontmatter", async () => {
- const result = await call("skill::parse", {
- content: "Just plain content",
- });
- expect(result.name).toBe("unknown");
- expect(result.content).toBe("Just plain content");
- });
-
- it("handles empty tags", async () => {
- const content = `---
-name: minimal
----
-Content here.`;
- const result = await call("skill::parse", { content });
- expect(result.name).toBe("minimal");
- expect(result.tags).toEqual([]);
- });
-});
diff --git a/src/__tests__/tool-profiles.test.ts b/src/__tests__/tool-profiles.test.ts
deleted file mode 100644
index 112f50a..0000000
--- a/src/__tests__/tool-profiles.test.ts
+++ /dev/null
@@ -1,206 +0,0 @@
-import { describe, it, expect } from "vitest";
-import {
- TOOL_PROFILES,
- matchToolProfile,
- filterToolsByProfile,
-} from "../tool-profiles.js";
-
-describe("TOOL_PROFILES", () => {
- it("has chat profile", () => {
- expect(TOOL_PROFILES.chat).toBeDefined();
- expect(TOOL_PROFILES.chat.length).toBeGreaterThan(0);
- });
-
- it("has code profile", () => {
- expect(TOOL_PROFILES.code).toBeDefined();
- expect(TOOL_PROFILES.code.length).toBeGreaterThan(0);
- });
-
- it("has research profile", () => {
- expect(TOOL_PROFILES.research).toBeDefined();
- expect(TOOL_PROFILES.research.length).toBeGreaterThan(0);
- });
-
- it("has ops profile", () => {
- expect(TOOL_PROFILES.ops).toBeDefined();
- expect(TOOL_PROFILES.ops.length).toBeGreaterThan(0);
- });
-
- it("has data profile", () => {
- expect(TOOL_PROFILES.data).toBeDefined();
- expect(TOOL_PROFILES.data.length).toBeGreaterThan(0);
- });
-
- it("has full profile", () => {
- expect(TOOL_PROFILES.full).toBeDefined();
- expect(TOOL_PROFILES.full.length).toBeGreaterThan(0);
- });
-
- it("chat profile includes web_search", () => {
- expect(TOOL_PROFILES.chat).toContain("tool::web_search");
- });
-
- it("chat profile includes memory", () => {
- expect(TOOL_PROFILES.chat).toContain("memory::recall");
- expect(TOOL_PROFILES.chat).toContain("memory::store");
- });
-
- it("code profile includes file wildcard", () => {
- expect(TOOL_PROFILES.code.some(p => p.includes("file_*"))).toBe(true);
- });
-
- it("code profile includes shell_exec", () => {
- expect(TOOL_PROFILES.code).toContain("tool::shell_exec");
- });
-
- it("research profile includes browser wildcard", () => {
- expect(TOOL_PROFILES.research.some(p => p.includes("browser_*"))).toBe(true);
- });
-
- it("ops profile includes system wildcard", () => {
- expect(TOOL_PROFILES.ops.some(p => p.includes("system_*"))).toBe(true);
- });
-
- it("data profile includes json wildcard", () => {
- expect(TOOL_PROFILES.data.some(p => p.includes("json_*"))).toBe(true);
- });
-
- it("data profile includes csv wildcard", () => {
- expect(TOOL_PROFILES.data.some(p => p.includes("csv_*"))).toBe(true);
- });
-
- it("full profile includes tool wildcard", () => {
- expect(TOOL_PROFILES.full).toContain("tool::*");
- });
-
- it("full profile includes memory wildcard", () => {
- expect(TOOL_PROFILES.full).toContain("memory::*");
- });
-});
-
-describe("matchToolProfile", () => {
- it("matches exact tool id", () => {
- expect(matchToolProfile("tool::web_search", ["tool::web_search"])).toBe(true);
- });
-
- it("does not match different tool id", () => {
- expect(matchToolProfile("tool::web_search", ["tool::file_read"])).toBe(false);
- });
-
- it("matches wildcard pattern", () => {
- expect(matchToolProfile("tool::web_search", ["tool::*"])).toBe(true);
- });
-
- it("matches partial wildcard", () => {
- expect(matchToolProfile("tool::file_read", ["tool::file_*"])).toBe(true);
- });
-
- it("does not match partial wildcard for different prefix", () => {
- expect(matchToolProfile("tool::web_search", ["tool::file_*"])).toBe(false);
- });
-
- it("matches memory wildcard", () => {
- expect(matchToolProfile("memory::recall", ["memory::*"])).toBe(true);
- });
-
- it("does not match cross-namespace", () => {
- expect(matchToolProfile("tool::file_read", ["memory::*"])).toBe(false);
- });
-
- it("returns false for empty patterns", () => {
- expect(matchToolProfile("tool::test", [])).toBe(false);
- });
-
- it("matches when any pattern matches", () => {
- expect(
- matchToolProfile("tool::web_fetch", [
- "tool::file_*",
- "tool::web_*",
- ]),
- ).toBe(true);
- });
-
- it("handles complex wildcards", () => {
- expect(matchToolProfile("tool::code_analyze", ["tool::code_*"])).toBe(true);
- });
-
- it("matches exact without wildcard needed", () => {
- expect(matchToolProfile("memory::store", ["memory::store"])).toBe(true);
- });
-});
-
-describe("filterToolsByProfile", () => {
- const allTools = [
- { function_id: "tool::web_search", description: "Search" },
- { function_id: "tool::web_fetch", description: "Fetch" },
- { function_id: "tool::file_read", description: "Read files" },
- { function_id: "tool::file_write", description: "Write files" },
- { function_id: "tool::shell_exec", description: "Execute shell" },
- { function_id: "tool::code_analyze", description: "Analyze code" },
- { function_id: "tool::json_transform", description: "Transform JSON" },
- { function_id: "tool::csv_parse", description: "Parse CSV" },
- { function_id: "tool::system_info", description: "System info" },
- { function_id: "tool::browser_navigate", description: "Navigate" },
- { function_id: "memory::recall", description: "Recall" },
- { function_id: "memory::store", description: "Store" },
- ];
-
- it("returns all tools for full profile", () => {
- const result = filterToolsByProfile(allTools, "full");
- expect(result.length).toBe(allTools.length);
- });
-
- it("returns all tools for unknown profile", () => {
- const result = filterToolsByProfile(allTools, "nonexistent");
- expect(result.length).toBe(allTools.length);
- });
-
- it("filters chat profile correctly", () => {
- const result = filterToolsByProfile(allTools, "chat");
- expect(result.some(t => t.function_id === "tool::web_search")).toBe(true);
- expect(result.some(t => t.function_id === "tool::web_fetch")).toBe(true);
- expect(result.some(t => t.function_id === "memory::recall")).toBe(true);
- expect(result.some(t => t.function_id === "memory::store")).toBe(true);
- expect(result.some(t => t.function_id === "tool::shell_exec")).toBe(false);
- });
-
- it("filters code profile correctly", () => {
- const result = filterToolsByProfile(allTools, "code");
- expect(result.some(t => t.function_id === "tool::file_read")).toBe(true);
- expect(result.some(t => t.function_id === "tool::file_write")).toBe(true);
- expect(result.some(t => t.function_id === "tool::shell_exec")).toBe(true);
- expect(result.some(t => t.function_id === "tool::code_analyze")).toBe(true);
- expect(result.some(t => t.function_id === "tool::web_search")).toBe(false);
- });
-
- it("filters research profile correctly", () => {
- const result = filterToolsByProfile(allTools, "research");
- expect(result.some(t => t.function_id === "tool::web_search")).toBe(true);
- expect(result.some(t => t.function_id === "tool::browser_navigate")).toBe(true);
- expect(result.some(t => t.function_id === "memory::recall")).toBe(true);
- });
-
- it("filters ops profile correctly", () => {
- const result = filterToolsByProfile(allTools, "ops");
- expect(result.some(t => t.function_id === "tool::shell_exec")).toBe(true);
- expect(result.some(t => t.function_id === "tool::system_info")).toBe(true);
- });
-
- it("filters data profile correctly", () => {
- const result = filterToolsByProfile(allTools, "data");
- expect(result.some(t => t.function_id === "tool::json_transform")).toBe(true);
- expect(result.some(t => t.function_id === "tool::csv_parse")).toBe(true);
- expect(result.some(t => t.function_id === "tool::file_read")).toBe(true);
- });
-
- it("handles empty tools array", () => {
- const result = filterToolsByProfile([], "chat");
- expect(result).toEqual([]);
- });
-
- it("handles tools with id instead of function_id", () => {
- const tools = [{ id: "tool::web_search" }, { id: "memory::recall" }];
- const result = filterToolsByProfile(tools, "chat");
- expect(result.some(t => t.id === "tool::web_search")).toBe(true);
- });
-});
diff --git a/src/__tests__/tools-extended.test.ts b/src/__tests__/tools-extended.test.ts
deleted file mode 100644
index 4c7d106..0000000
--- a/src/__tests__/tools-extended.test.ts
+++ /dev/null
@@ -1,1313 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-import os from "os";
-
-const kvStore: Record> = {};
-function getScope(scope: string) {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-function resetKv() {
- for (const key of Object.keys(kvStore)) delete kvStore[key];
-}
-function seedKv(scope: string, key: string, value: unknown) {
- getScope(scope).set(key, value);
-}
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "state::delete") {
- getScope(data.scope).delete(data.key);
- return { ok: true };
- }
- if (fnId === "state::list") {
- return [...getScope(data.scope).entries()].map(([key, value]) => ({
- key,
- value,
- }));
- }
- if (fnId === "agent::chat")
- return { content: "mock llm response", model: "test-model" };
- if (fnId === "enqueue") return { ok: true };
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- assertNoSsrf: vi.fn(async (url: string) => {
- const parsed = new URL(url);
- if (
- parsed.hostname === "localhost" ||
- parsed.hostname === "127.0.0.1" ||
- parsed.hostname === "10.0.0.1" ||
- parsed.hostname === "169.254.169.254"
- ) {
- throw new Error(`SSRF blocked: ${parsed.hostname}`);
- }
- }),
-}));
-
-vi.mock("fs/promises", () => ({
- readFile: vi.fn(async (p: string) => {
- if (p.includes("not-found")) throw new Error("ENOENT");
- if (p.includes(".py"))
- return "def hello():\n pass\nclass Foo:\n pass\n# comment\n\n";
- return "function foo() {}\nconst bar = () => {}\n// comment\nclass Baz {}\n\nexport function qux() {}\n";
- }),
- writeFile: vi.fn(async () => {}),
- readdir: vi.fn(async () => []),
- stat: vi.fn(async () => ({ size: 100, mtime: new Date() })),
-}));
-
-vi.mock("child_process", () => {
- const { promisify } = require("util");
- const fn: any = vi.fn(
- (cmd: string, args: string[], opts: any, cb?: Function) => {
- if (!cb && typeof opts === "function") {
- cb = opts;
- }
- cb?.(null, "mock stdout output", "");
- },
- );
- fn[promisify.custom] = vi.fn(async () => ({
- stdout: "mock stdout output",
- stderr: "",
- }));
- return { execFile: fn };
-});
-
-beforeEach(() => {
- resetKv();
- mockTrigger.mockClear();
- mockTriggerVoid.mockClear();
-});
-
-beforeAll(async () => {
- await import("../tools-extended.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("tool::schedule_reminder", () => {
- it("creates reminder with valid time", async () => {
- const result = await call("tool::schedule_reminder", {
- label: "standup",
- time: "2030-01-01T09:00:00Z",
- });
- expect(result.id).toBeDefined();
- expect(result.label).toBe("standup");
- expect(result.time).toBe("2030-01-01T09:00:00.000Z");
- });
-
- it("stores reminder in state", async () => {
- await call("tool::schedule_reminder", {
- label: "test",
- time: "2030-06-15T12:00:00Z",
- });
- const calls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "state::set" && c[1]?.scope === "reminders",
- );
- expect(calls.length).toBe(1);
- });
-
- it("rejects invalid time format", async () => {
- await expect(
- call("tool::schedule_reminder", { label: "bad", time: "not-a-date" }),
- ).rejects.toThrow("Invalid time format");
- });
-
- it("accepts optional agentId", async () => {
- const result = await call("tool::schedule_reminder", {
- label: "agent-task",
- time: "2030-01-01T00:00:00Z",
- agentId: "agent-42",
- });
- expect(result.id).toBeDefined();
- });
-
- it("returns ISO time string", async () => {
- const result = await call("tool::schedule_reminder", {
- label: "iso",
- time: "2030-03-15",
- });
- expect(result.time).toMatch(/^\d{4}-\d{2}-\d{2}T/);
- });
-});
-
-describe("tool::cron_create", () => {
- it("creates cron job with valid inputs", async () => {
- const result = await call("tool::cron_create", {
- name: "daily-report",
- schedule: "0 9 * * *",
- functionId: "report::generate",
- });
- expect(result.created).toBe(true);
- expect(result.name).toBe("daily-report");
- });
-
- it("stores cron job in state", async () => {
- await call("tool::cron_create", {
- name: "test-cron",
- schedule: "*/5 * * * *",
- functionId: "test::run",
- });
- const stored = getScope("cron_jobs").get("test-cron") as any;
- expect(stored).toBeDefined();
- expect(stored.functionId).toBe("test::run");
- });
-
- it("rejects invalid cron name with spaces", async () => {
- await expect(
- call("tool::cron_create", {
- name: "bad name",
- schedule: "* * * * *",
- functionId: "test::fn",
- }),
- ).rejects.toThrow("Invalid cron name");
- });
-
- it("rejects empty cron name", async () => {
- await expect(
- call("tool::cron_create", {
- name: "",
- schedule: "* * * * *",
- functionId: "test::fn",
- }),
- ).rejects.toThrow("Invalid cron name");
- });
-
- it("rejects invalid functionId with spaces", async () => {
- await expect(
- call("tool::cron_create", {
- name: "valid",
- schedule: "* * * * *",
- functionId: "bad function id",
- }),
- ).rejects.toThrow("Invalid function ID");
- });
-
- it("accepts payload parameter", async () => {
- const result = await call("tool::cron_create", {
- name: "with-payload",
- schedule: "0 0 * * 0",
- functionId: "weekly::task",
- payload: { key: "value" },
- });
- expect(result.created).toBe(true);
- });
-});
-
-describe("tool::cron_list", () => {
- it("returns empty array when no cron jobs", async () => {
- const result = await call("tool::cron_list", {});
- expect(result).toEqual([]);
- });
-
- it("returns stored cron jobs", async () => {
- seedKv("cron_jobs", "job1", { name: "job1", schedule: "* * * * *" });
- seedKv("cron_jobs", "job2", { name: "job2", schedule: "0 9 * * *" });
- const result = await call("tool::cron_list", {});
- expect(result).toHaveLength(2);
- });
-});
-
-describe("tool::cron_delete", () => {
- it("deletes a cron job", async () => {
- seedKv("cron_jobs", "to-delete", { name: "to-delete" });
- const result = await call("tool::cron_delete", { name: "to-delete" });
- expect(result.deleted).toBe(true);
- expect(result.name).toBe("to-delete");
- });
-});
-
-describe("tool::todo_create", () => {
- it("creates a todo with defaults", async () => {
- const result = await call("tool::todo_create", { title: "Test task" });
- expect(result.id).toBeDefined();
- expect(result.title).toBe("Test task");
- expect(result.priority).toBe("medium");
- expect(result.status).toBe("pending");
- });
-
- it("creates todo with all fields", async () => {
- const result = await call("tool::todo_create", {
- title: "Urgent task",
- description: "Do this now",
- priority: "high",
- assignee: "alice",
- });
- expect(result.priority).toBe("high");
- expect(result.assignee).toBe("alice");
- expect(result.description).toBe("Do this now");
- });
-
- it("stores todo in state", async () => {
- const result = await call("tool::todo_create", { title: "Stored" });
- const stored = getScope("todos").get(result.id);
- expect(stored).toBeDefined();
- });
-
- it("sets createdAt and updatedAt timestamps", async () => {
- const before = Date.now();
- const result = await call("tool::todo_create", { title: "Timed" });
- expect(result.createdAt).toBeGreaterThanOrEqual(before);
- expect(result.updatedAt).toBeGreaterThanOrEqual(before);
- });
-});
-
-describe("tool::todo_list", () => {
- it("returns empty array when no todos", async () => {
- const result = await call("tool::todo_list", {});
- expect(result).toEqual([]);
- });
-
- it("filters by status", async () => {
- seedKv("todos", "1", { status: "pending", title: "A" });
- seedKv("todos", "2", { status: "done", title: "B" });
- const result = await call("tool::todo_list", { status: "pending" });
- expect(result).toHaveLength(1);
- expect(result[0].title).toBe("A");
- });
-
- it("filters by assignee", async () => {
- seedKv("todos", "1", { assignee: "alice", title: "A" });
- seedKv("todos", "2", { assignee: "bob", title: "B" });
- const result = await call("tool::todo_list", { assignee: "bob" });
- expect(result).toHaveLength(1);
- expect(result[0].title).toBe("B");
- });
-
- it("returns all when no filters", async () => {
- seedKv("todos", "1", { status: "pending", title: "A" });
- seedKv("todos", "2", { status: "done", title: "B" });
- const result = await call("tool::todo_list", {});
- expect(result).toHaveLength(2);
- });
-});
-
-describe("tool::todo_update", () => {
- it("updates todo status", async () => {
- seedKv("todos", "t1", {
- id: "t1",
- title: "Task",
- status: "pending",
- updatedAt: 0,
- });
- const result = await call("tool::todo_update", {
- id: "t1",
- status: "done",
- });
- expect(result.status).toBe("done");
- expect(result.updatedAt).toBeGreaterThan(0);
- });
-
- it("throws when todo not found", async () => {
- await expect(
- call("tool::todo_update", { id: "nonexistent", status: "done" }),
- ).rejects.toThrow("Todo not found");
- });
-
- it("updates multiple fields at once", async () => {
- seedKv("todos", "t2", { id: "t2", title: "Old", priority: "low" });
- const result = await call("tool::todo_update", {
- id: "t2",
- title: "New",
- priority: "high",
- assignee: "carol",
- });
- expect(result.title).toBe("New");
- expect(result.priority).toBe("high");
- expect(result.assignee).toBe("carol");
- });
-});
-
-describe("tool::todo_delete", () => {
- it("deletes a todo", async () => {
- seedKv("todos", "del-1", { title: "Delete me" });
- const result = await call("tool::todo_delete", { id: "del-1" });
- expect(result.deleted).toBe(true);
- expect(result.id).toBe("del-1");
- });
-});
-
-describe("tool::image_analyze", () => {
- it("analyzes image with default prompt", async () => {
- const result = await call("tool::image_analyze", {
- url: "https://example.com/image.png",
- });
- expect(result.url).toBe("https://example.com/image.png");
- expect(result.description).toBeDefined();
- });
-
- it("uses custom prompt when provided", async () => {
- const result = await call("tool::image_analyze", {
- url: "https://example.com/img.jpg",
- prompt: "Count the objects",
- });
- expect(result.description).toBeDefined();
- });
-
- it("blocks SSRF on localhost URLs", async () => {
- await expect(
- call("tool::image_analyze", { url: "http://localhost/secret.png" }),
- ).rejects.toThrow(/SSRF/);
- });
-});
-
-describe("tool::audio_transcribe", () => {
- it("returns not_implemented status", async () => {
- const result = await call("tool::audio_transcribe", {
- url: "https://example.com/audio.mp3",
- });
- expect(result.status).toBe("not_implemented");
- expect(result.transcript).toBeNull();
- });
-
- it("blocks SSRF on private URLs", async () => {
- await expect(
- call("tool::audio_transcribe", { url: "http://10.0.0.1/audio.wav" }),
- ).rejects.toThrow(/SSRF/);
- });
-});
-
-describe("tool::tts_speak", () => {
- it("returns not_implemented status", async () => {
- const result = await call("tool::tts_speak", { text: "Hello world" });
- expect(result.status).toBe("not_implemented");
- expect(result.audioUrl).toBeNull();
- });
-
- it("accepts voice parameter", async () => {
- const result = await call("tool::tts_speak", {
- text: "test",
- voice: "alloy",
- });
- expect(result.status).toBe("not_implemented");
- });
-});
-
-describe("tool::media_download", () => {
- it("blocks SSRF on private IPs", async () => {
- await expect(
- call("tool::media_download", {
- url: "http://127.0.0.1/file.zip",
- outputPath: "out.zip",
- }),
- ).rejects.toThrow(/SSRF/);
- });
-
- it("blocks path traversal", async () => {
- await expect(
- call("tool::media_download", {
- url: "https://example.com/file.zip",
- outputPath: "../../etc/passwd",
- }),
- ).rejects.toThrow(/Path traversal/);
- });
-});
-
-describe("tool::kg_add", () => {
- it("adds a new entity", async () => {
- const result = await call("tool::kg_add", {
- entity: "React",
- type: "framework",
- });
- expect(result.entity).toBe("react");
- expect(result.type).toBe("framework");
- expect(result.relationsCount).toBe(0);
- });
-
- it("adds entity with relation", async () => {
- const result = await call("tool::kg_add", {
- entity: "TypeScript",
- type: "language",
- relation: "compiles_to",
- target: "JavaScript",
- });
- expect(result.relationsCount).toBe(1);
- });
-
- it("normalizes entity key to lowercase with underscores", async () => {
- const result = await call("tool::kg_add", {
- entity: "Hello World",
- type: "test",
- });
- expect(result.entity).toBe("hello_world");
- });
-
- it("merges properties on existing entity", async () => {
- seedKv("knowledge_graph", "react", {
- entity: "React",
- type: "framework",
- relations: [],
- properties: { version: "19" },
- });
- await call("tool::kg_add", {
- entity: "React",
- type: "framework",
- properties: { creator: "Meta" },
- });
- const stored = getScope("knowledge_graph").get("react") as any;
- expect(stored.properties.version).toBe("19");
- expect(stored.properties.creator).toBe("Meta");
- });
-
- it("avoids duplicate relations", async () => {
- await call("tool::kg_add", {
- entity: "A",
- type: "node",
- relation: "connects",
- target: "B",
- });
- await call("tool::kg_add", {
- entity: "A",
- type: "node",
- relation: "connects",
- target: "B",
- });
- const stored = getScope("knowledge_graph").get("a") as any;
- expect(stored.relations).toHaveLength(1);
- });
-});
-
-describe("tool::kg_query", () => {
- it("returns empty for non-existent entity", async () => {
- const result = await call("tool::kg_query", { entity: "nonexistent" });
- expect(result.nodes).toHaveLength(0);
- });
-
- it("returns node at depth 0", async () => {
- seedKv("knowledge_graph", "root", {
- entity: "root",
- type: "node",
- relations: [],
- });
- const result = await call("tool::kg_query", { entity: "root" });
- expect(result.nodes).toHaveLength(1);
- expect(result.nodes[0]._depth).toBe(0);
- });
-
- it("caps depth at 5", async () => {
- const result = await call("tool::kg_query", { entity: "x", depth: 100 });
- expect(result.totalVisited).toBeLessThanOrEqual(100);
- });
-
- it("filters by relation type", async () => {
- seedKv("knowledge_graph", "parent", {
- entity: "parent",
- type: "node",
- relations: [
- { relation: "child_of", target: "child1" },
- { relation: "sibling_of", target: "sibling1" },
- ],
- });
- seedKv("knowledge_graph", "child1", {
- entity: "child1",
- type: "node",
- relations: [],
- });
- const result = await call("tool::kg_query", {
- entity: "parent",
- relation: "child_of",
- });
- expect(result.nodes.length).toBeLessThanOrEqual(2);
- });
-});
-
-describe("tool::kg_visualize", () => {
- it("returns empty mermaid for non-existent entity", async () => {
- const result = await call("tool::kg_visualize", { entity: "missing" });
- expect(result.mermaid).toContain("graph TD");
- expect(result.edgeCount).toBe(0);
- });
-
- it("generates mermaid edges", async () => {
- seedKv("knowledge_graph", "a", {
- entity: "a",
- relations: [{ relation: "knows", target: "b" }],
- });
- seedKv("knowledge_graph", "b", {
- entity: "b",
- relations: [],
- });
- const result = await call("tool::kg_visualize", { entity: "a" });
- expect(result.mermaid).toContain("a -->|knows| b");
- expect(result.edgeCount).toBe(1);
- });
-
- it("caps depth at 4", async () => {
- const result = await call("tool::kg_visualize", {
- entity: "x",
- depth: 100,
- });
- expect(result.nodeCount).toBeLessThanOrEqual(50);
- });
-});
-
-describe("tool::memory_store", () => {
- it("stores memory with defaults", async () => {
- const result = await call("tool::memory_store", {
- key: "mem1",
- content: "Test memory content",
- });
- expect(result.stored).toBe(true);
- expect(result.key).toBe("mem1");
- });
-
- it("stores memory with tags and importance", async () => {
- const result = await call("tool::memory_store", {
- key: "tagged",
- content: "Important data",
- tags: ["project", "deadline"],
- importance: 9,
- });
- expect(result.stored).toBe(true);
- const stored = getScope("memories").get("tagged") as any;
- expect(stored.tags).toEqual(["project", "deadline"]);
- expect(stored.importance).toBe(9);
- });
-
- it("generates UUID key when none provided", async () => {
- const result = await call("tool::memory_store", {
- key: "",
- content: "auto-keyed",
- });
- expect(result.key).toBeDefined();
- expect(result.key.length).toBeGreaterThan(0);
- });
-
- it("initializes accessCount to 0", async () => {
- await call("tool::memory_store", { key: "count-test", content: "data" });
- const stored = getScope("memories").get("count-test") as any;
- expect(stored.accessCount).toBe(0);
- });
-});
-
-describe("tool::memory_recall", () => {
- it("returns found:false for missing key", async () => {
- const result = await call("tool::memory_recall", { key: "missing" });
- expect(result.found).toBe(false);
- });
-
- it("returns memory and increments access count", async () => {
- seedKv("memories", "existing", {
- content: "Hello",
- accessCount: 0,
- tags: [],
- });
- const result = await call("tool::memory_recall", { key: "existing" });
- expect(result.found).toBe(true);
- expect(result.content).toBe("Hello");
- expect(result.accessCount).toBe(1);
- });
-
- it("sets lastAccessed timestamp", async () => {
- seedKv("memories", "ts-test", { content: "test", accessCount: 0 });
- const before = Date.now();
- const result = await call("tool::memory_recall", { key: "ts-test" });
- expect(result.lastAccessed).toBeGreaterThanOrEqual(before);
- });
-});
-
-describe("tool::memory_search", () => {
- it("returns empty results for no matches", async () => {
- const result = await call("tool::memory_search", { query: "xyz" });
- expect(result.results).toEqual([]);
- expect(result.total).toBe(0);
- });
-
- it("finds memories by content", async () => {
- seedKv("memories", "m1", { content: "React hooks tutorial", tags: [] });
- seedKv("memories", "m2", { content: "Vue composition API", tags: [] });
- const result = await call("tool::memory_search", { query: "react" });
- expect(result.total).toBe(1);
- });
-
- it("finds memories by tag", async () => {
- seedKv("memories", "m1", {
- content: "data",
- tags: ["typescript", "testing"],
- });
- const result = await call("tool::memory_search", { query: "typescript" });
- expect(result.total).toBe(1);
- });
-
- it("respects limit parameter", async () => {
- for (let i = 0; i < 30; i++) {
- seedKv("memories", `m-${i}`, { content: `match item ${i}`, tags: [] });
- }
- const result = await call("tool::memory_search", {
- query: "match",
- limit: 5,
- });
- expect(result.results.length).toBeLessThanOrEqual(5);
- });
-
- it("defaults limit to 20", async () => {
- for (let i = 0; i < 30; i++) {
- seedKv("memories", `m-${i}`, { content: `test item ${i}`, tags: [] });
- }
- const result = await call("tool::memory_search", { query: "test" });
- expect(result.results.length).toBeLessThanOrEqual(20);
- });
-});
-
-describe("tool::agent_list", () => {
- it("returns empty array when no agents", async () => {
- const result = await call("tool::agent_list", {});
- expect(result).toEqual([]);
- });
-
- it("returns agents with name, id, tags", async () => {
- seedKv("agents", "a1", {
- name: "researcher",
- tags: ["research"],
- createdAt: 100,
- });
- const result = await call("tool::agent_list", {});
- expect(result).toHaveLength(1);
- expect(result[0].name).toBe("researcher");
- expect(result[0].id).toBe("a1");
- });
-
- it("filters out agents without names", async () => {
- seedKv("agents", "a1", { tags: [] });
- seedKv("agents", "a2", { name: "valid", tags: [] });
- const result = await call("tool::agent_list", {});
- expect(result).toHaveLength(1);
- });
-});
-
-describe("tool::agent_delegate", () => {
- it("delegates task to agent", async () => {
- const result = await call("tool::agent_delegate", {
- agentId: "researcher",
- task: "Find information about X",
- });
- expect(result.agentId).toBe("researcher");
- expect(result.response).toBe("mock llm response");
- });
-
- it("includes context in message when provided", async () => {
- await call("tool::agent_delegate", {
- agentId: "coder",
- task: "Write tests",
- context: "TypeScript project",
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls.length).toBeGreaterThan(0);
- expect(chatCalls[0][1].message).toContain("Context: TypeScript project");
- });
-});
-
-describe("tool::channel_send", () => {
- it("sends message to channel", async () => {
- const result = await call("tool::channel_send", {
- channel: "slack",
- channelId: "C123",
- message: "Hello team",
- });
- expect(result.sent).toBe(true);
- expect(result.channel).toBe("slack");
- });
-
- it("uses triggerVoid for enqueue", async () => {
- await call("tool::channel_send", {
- channel: "discord",
- channelId: "guild-1",
- message: "test",
- });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "enqueue",
- expect.objectContaining({
- topic: "discord.outbound",
- }),
- );
- });
-});
-
-describe("tool::env_get", () => {
- it("returns allowed env var", async () => {
- const result = await call("tool::env_get", { name: "PATH" });
- expect(result.name).toBe("PATH");
- expect(result.value).toBeDefined();
- });
-
- it("returns HOME env var", async () => {
- const result = await call("tool::env_get", { name: "HOME" });
- expect(result.name).toBe("HOME");
- });
-
- it("blocks access to SECRET env vars", async () => {
- await expect(
- call("tool::env_get", { name: "AWS_SECRET_ACCESS_KEY" }),
- ).rejects.toThrow("Access denied");
- });
-
- it("blocks access to API keys", async () => {
- await expect(
- call("tool::env_get", { name: "ANTHROPIC_API_KEY" }),
- ).rejects.toThrow("Access denied");
- });
-
- it("lists allowed vars in error message", async () => {
- try {
- await call("tool::env_get", { name: "FORBIDDEN" });
- } catch (e: any) {
- expect(e.message).toContain("PATH");
- expect(e.message).toContain("HOME");
- }
- });
-});
-
-describe("tool::system_info", () => {
- it("returns system information", async () => {
- const result = await call("tool::system_info", {});
- expect(result.platform).toBe(os.platform());
- expect(result.arch).toBe(os.arch());
- expect(typeof result.cpus).toBe("number");
- expect(result.totalMemory).toBeGreaterThan(0);
- expect(result.nodeVersion).toMatch(/^v\d+/);
- });
-
- it("includes hostname", async () => {
- const result = await call("tool::system_info", {});
- expect(result.hostname).toBe(os.hostname());
- });
-
- it("includes uptime", async () => {
- const result = await call("tool::system_info", {});
- expect(result.uptime).toBeGreaterThanOrEqual(0);
- });
-});
-
-describe("tool::process_list", () => {
- it("returns process list", async () => {
- const result = await call("tool::process_list", {});
- expect(result.processes).toBeDefined();
- expect(Array.isArray(result.processes)).toBe(true);
- });
-
- it("truncates to max 50 lines", async () => {
- const result = await call("tool::process_list", {});
- expect(result.processes.length).toBeLessThanOrEqual(50);
- });
-});
-
-describe("tool::code_analyze", () => {
- it("analyzes TypeScript file", async () => {
- const result = await call("tool::code_analyze", { filePath: "test.ts" });
- expect(result.totalLines).toBeGreaterThan(0);
- expect(result.extension).toBe(".ts");
- expect(typeof result.functions).toBe("number");
- expect(typeof result.classes).toBe("number");
- expect(typeof result.comments).toBe("number");
- });
-
- it("analyzes Python file", async () => {
- const result = await call("tool::code_analyze", { filePath: "test.py" });
- expect(result.extension).toBe(".py");
- });
-
- it("counts code, blank, and comment lines", async () => {
- const result = await call("tool::code_analyze", { filePath: "test.ts" });
- expect(result.codeLines).toBe(
- result.totalLines - result.blankLines - result.comments,
- );
- });
-
- it("rejects path traversal", async () => {
- await expect(
- call("tool::code_analyze", { filePath: "../../etc/passwd" }),
- ).rejects.toThrow(/Path traversal/);
- });
-});
-
-describe("tool::code_format", () => {
- it("formats TypeScript file with prettier", async () => {
- const result = await call("tool::code_format", { filePath: "test.ts" });
- expect(result.formatted).toBe(true);
- });
-
- it("uses rustfmt for .rs files", async () => {
- const result = await call("tool::code_format", { filePath: "test.rs" });
- expect(result).toBeDefined();
- });
-
- it("uses black for .py files", async () => {
- const result = await call("tool::code_format", { filePath: "test.py" });
- expect(result).toBeDefined();
- });
-
- it("rejects path traversal", async () => {
- await expect(
- call("tool::code_format", { filePath: "../../../etc/hosts" }),
- ).rejects.toThrow(/Path traversal/);
- });
-});
-
-describe("tool::code_lint", () => {
- it("lints TypeScript with eslint", async () => {
- const result = await call("tool::code_lint", { filePath: "test.ts" });
- expect(result.path).toBeDefined();
- });
-
- it("uses clippy for .rs files", async () => {
- const result = await call("tool::code_lint", { filePath: "main.rs" });
- expect(result).toBeDefined();
- });
-
- it("rejects path traversal", async () => {
- await expect(
- call("tool::code_lint", { filePath: "../../secret.py" }),
- ).rejects.toThrow(/Path traversal/);
- });
-});
-
-describe("tool::code_test", () => {
- it("runs npm test command", async () => {
- const result = await call("tool::code_test", {
- command: ["npm", "test"],
- });
- expect(result.exitCode).toBe(0);
- });
-
- it("rejects empty command array", async () => {
- await expect(call("tool::code_test", { command: [] })).rejects.toThrow(
- "command must be a non-empty array",
- );
- });
-
- it("rejects disallowed test command", async () => {
- await expect(
- call("tool::code_test", { command: ["rm", "-rf", "/"] }),
- ).rejects.toThrow("Test command not allowed");
- });
-
- it("allows cargo test", async () => {
- const result = await call("tool::code_test", {
- command: ["cargo", "test"],
- });
- expect(result).toBeDefined();
- });
-
- it("allows python test", async () => {
- const result = await call("tool::code_test", {
- command: ["python3", "-m", "pytest"],
- });
- expect(result).toBeDefined();
- });
-
- it("rejects path traversal in cwd", async () => {
- await expect(
- call("tool::code_test", {
- command: ["npm", "test"],
- cwd: "../../../",
- }),
- ).rejects.toThrow(/Path traversal/);
- });
-
- it("rejects bash as test command", async () => {
- await expect(
- call("tool::code_test", { command: ["bash", "-c", "evil"] }),
- ).rejects.toThrow("Test command not allowed");
- });
-});
-
-describe("tool::json_transform", () => {
- it("returns parsed data with identity expression", async () => {
- const result = await call("tool::json_transform", {
- data: '{"name":"test"}',
- expression: ".",
- });
- expect(result.result).toEqual({ name: "test" });
- });
-
- it("extracts keys", async () => {
- const result = await call("tool::json_transform", {
- data: '{"a":1,"b":2}',
- expression: ".keys",
- });
- expect(result.result).toEqual(["a", "b"]);
- });
-
- it("extracts values", async () => {
- const result = await call("tool::json_transform", {
- data: '{"a":1,"b":2}',
- expression: ".values",
- });
- expect(result.result).toEqual([1, 2]);
- });
-
- it("returns length of array", async () => {
- const result = await call("tool::json_transform", {
- data: "[1,2,3]",
- expression: ".length",
- });
- expect(result.result).toBe(3);
- });
-
- it("returns length of object keys", async () => {
- const result = await call("tool::json_transform", {
- data: '{"a":1,"b":2,"c":3}',
- expression: ".length",
- });
- expect(result.result).toBe(3);
- });
-
- it("navigates nested path", async () => {
- const result = await call("tool::json_transform", {
- data: '{"a":{"b":{"c":42}}}',
- expression: ".a.b.c",
- });
- expect(result.result).toBe(42);
- });
-
- it("throws on invalid JSON", async () => {
- await expect(
- call("tool::json_transform", {
- data: "not-json",
- expression: ".",
- }),
- ).rejects.toThrow();
- });
-});
-
-describe("tool::csv_parse", () => {
- it("parses CSV with headers", async () => {
- const result = await call("tool::csv_parse", {
- content: "name,age\nAlice,30\nBob,25",
- });
- expect(result.headers).toEqual(["name", "age"]);
- expect(result.rowCount).toBe(2);
- expect(result.rows[0]).toEqual({ name: "Alice", age: "30" });
- });
-
- it("parses CSV without headers", async () => {
- const result = await call("tool::csv_parse", {
- content: "Alice,30\nBob,25",
- hasHeader: false,
- });
- expect(result.headers).toEqual([]);
- expect(result.rows[0]).toEqual(["Alice", "30"]);
- });
-
- it("supports custom delimiter", async () => {
- const result = await call("tool::csv_parse", {
- content: "name|age\nAlice|30",
- delimiter: "|",
- });
- expect(result.rows[0]).toEqual({ name: "Alice", age: "30" });
- });
-
- it("handles empty content", async () => {
- const result = await call("tool::csv_parse", { content: "" });
- expect(result.rows).toEqual([]);
- });
-
- it("strips quotes from values", async () => {
- const result = await call("tool::csv_parse", {
- content: '"name","age"\n"Alice","30"',
- });
- expect(result.rows[0]).toEqual({ name: "Alice", age: "30" });
- });
-});
-
-describe("tool::yaml_parse", () => {
- it("parses simple key-value pairs", async () => {
- const result = await call("tool::yaml_parse", {
- content: "name: test\nversion: 1.0",
- });
- expect(result.result.name).toBe("test");
- expect(result.result.version).toBe(1.0);
- });
-
- it("parses boolean values", async () => {
- const result = await call("tool::yaml_parse", {
- content: "enabled: true\ndisabled: false",
- });
- expect(result.result.enabled).toBe(true);
- expect(result.result.disabled).toBe(false);
- });
-
- it("parses null value", async () => {
- const result = await call("tool::yaml_parse", {
- content: "value: null",
- });
- expect(result.result.value).toBeNull();
- });
-
- it("skips comments", async () => {
- const result = await call("tool::yaml_parse", {
- content: "# comment\nkey: value",
- });
- expect(result.result.key).toBe("value");
- });
-
- it("skips empty lines", async () => {
- const result = await call("tool::yaml_parse", {
- content: "a: 1\n\nb: 2",
- });
- expect(result.result.a).toBe(1);
- expect(result.result.b).toBe(2);
- });
-});
-
-describe("tool::regex_test", () => {
- it("finds matches with global flag", async () => {
- const result = await call("tool::regex_test", {
- pattern: "\\d+",
- text: "abc 123 def 456",
- });
- expect(result.count).toBe(2);
- expect(result.matches[0].match).toBe("123");
- expect(result.matches[1].match).toBe("456");
- });
-
- it("returns empty for no matches", async () => {
- const result = await call("tool::regex_test", {
- pattern: "xyz",
- text: "abc def",
- });
- expect(result.count).toBe(0);
- expect(result.matches).toEqual([]);
- });
-
- it("captures groups", async () => {
- const result = await call("tool::regex_test", {
- pattern: "(\\w+)@(\\w+)",
- text: "user@host",
- });
- expect(result.matches[0].captures).toEqual(["user", "host"]);
- });
-
- it("strips invalid flags", async () => {
- const result = await call("tool::regex_test", {
- pattern: "a",
- text: "abc",
- flags: "gxyz",
- });
- expect(result.flags).toBe("gy");
- });
-
- it("uses g flag by default", async () => {
- const result = await call("tool::regex_test", {
- pattern: "a",
- text: "aaa",
- });
- expect(result.count).toBe(3);
- });
-
- it("limits iterations to 1000", async () => {
- const result = await call("tool::regex_test", {
- pattern: ".",
- text: "a".repeat(2000),
- });
- expect(result.count).toBeLessThanOrEqual(1000);
- });
-});
-
-describe("tool::uuid_generate", () => {
- it("generates a single UUID by default", async () => {
- const result = await call("tool::uuid_generate", {});
- expect(result.uuids).toHaveLength(1);
- expect(result.uuids[0]).toMatch(
- /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
- );
- });
-
- it("generates multiple UUIDs", async () => {
- const result = await call("tool::uuid_generate", { count: 5 });
- expect(result.uuids).toHaveLength(5);
- expect(result.count).toBe(5);
- });
-
- it("caps count at 100", async () => {
- const result = await call("tool::uuid_generate", { count: 500 });
- expect(result.uuids).toHaveLength(100);
- expect(result.count).toBe(100);
- });
-
- it("generates unique UUIDs", async () => {
- const result = await call("tool::uuid_generate", { count: 10 });
- const unique = new Set(result.uuids);
- expect(unique.size).toBe(10);
- });
-});
-
-describe("tool::hash_compute", () => {
- it("computes sha256 hash by default", async () => {
- const result = await call("tool::hash_compute", { input: "hello" });
- expect(result.algorithm).toBe("sha256");
- expect(result.encoding).toBe("hex");
- expect(result.hash).toBe(
- "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
- );
- });
-
- it("computes md5 hash", async () => {
- const result = await call("tool::hash_compute", {
- input: "hello",
- algorithm: "md5",
- });
- expect(result.algorithm).toBe("md5");
- expect(result.hash).toBe("5d41402abc4b2a76b9719d911017c592");
- });
-
- it("computes sha1 hash", async () => {
- const result = await call("tool::hash_compute", {
- input: "hello",
- algorithm: "sha1",
- });
- expect(result.algorithm).toBe("sha1");
- expect(result.hash).toBe("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
- });
-
- it("supports base64 encoding", async () => {
- const result = await call("tool::hash_compute", {
- input: "test",
- encoding: "base64",
- });
- expect(result.encoding).toBe("base64");
- expect(result.hash).toBeDefined();
- });
-
- it("rejects unsupported algorithm", async () => {
- await expect(
- call("tool::hash_compute", { input: "test", algorithm: "ripemd160" }),
- ).rejects.toThrow("Unsupported algorithm");
- });
-
- it("lists allowed algorithms in error", async () => {
- try {
- await call("tool::hash_compute", { input: "x", algorithm: "bad" });
- } catch (e: any) {
- expect(e.message).toContain("sha256");
- expect(e.message).toContain("md5");
- }
- });
-
- it("supports sha512", async () => {
- const result = await call("tool::hash_compute", {
- input: "test",
- algorithm: "sha512",
- });
- expect(result.algorithm).toBe("sha512");
- expect(result.hash.length).toBe(128);
- });
-});
-
-describe("tool::image_generate_prompt", () => {
- it("generates prompt from description", async () => {
- const result = await call("tool::image_generate_prompt", {
- description: "A sunset over mountains",
- });
- expect(result.originalDescription).toBe("A sunset over mountains");
- expect(result.prompt).toBeDefined();
- });
-
- it("includes style when provided", async () => {
- await call("tool::image_generate_prompt", {
- description: "A cat",
- style: "watercolor",
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls.length).toBeGreaterThan(0);
- });
-});
-
-describe("tool::disk_usage", () => {
- it("returns disk usage for workspace", async () => {
- const result = await call("tool::disk_usage", {});
- expect(result.output).toBeDefined();
- });
-
- it("accepts custom path", async () => {
- const result = await call("tool::disk_usage", { path: "/tmp" });
- expect(result.path).toBe("/tmp");
- });
-});
-
-describe("tool::network_check", () => {
- it("rejects private addresses", async () => {
- await expect(
- call("tool::network_check", { host: "127.0.0.1" }),
- ).rejects.toThrow("Cannot check private/reserved addresses");
- });
-
- it("rejects 192.168.x.x", async () => {
- await expect(
- call("tool::network_check", { host: "192.168.1.1" }),
- ).rejects.toThrow("Cannot check private/reserved addresses");
- });
-
- it("rejects 10.x.x.x", async () => {
- await expect(
- call("tool::network_check", { host: "10.0.0.1" }),
- ).rejects.toThrow("Cannot check private/reserved addresses");
- });
-
- it("rejects 172.16.x.x", async () => {
- await expect(
- call("tool::network_check", { host: "172.16.0.1" }),
- ).rejects.toThrow("Cannot check private/reserved addresses");
- });
-
- it("rejects 169.254.x.x", async () => {
- await expect(
- call("tool::network_check", { host: "169.254.169.254" }),
- ).rejects.toThrow("Cannot check private/reserved addresses");
- });
-});
-
-describe("tool::code_explain", () => {
- it("explains code file", async () => {
- const result = await call("tool::code_explain", { filePath: "test.ts" });
- expect(result.explanation).toBeDefined();
- });
-
- it("includes question in prompt when provided", async () => {
- await call("tool::code_explain", {
- filePath: "test.ts",
- question: "What does this do?",
- });
- const chatCalls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "agent::chat",
- );
- expect(chatCalls.length).toBeGreaterThan(0);
- });
-
- it("rejects path traversal", async () => {
- await expect(
- call("tool::code_explain", { filePath: "../../etc/shadow" }),
- ).rejects.toThrow(/Path traversal/);
- });
-});
diff --git a/src/__tests__/tools.test.ts b/src/__tests__/tools.test.ts
deleted file mode 100644
index b1190f3..0000000
--- a/src/__tests__/tools.test.ts
+++ /dev/null
@@ -1,446 +0,0 @@
-import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
-import path from "path";
-
-const kvStore: Record> = {};
-function getScope(scope: string) {
- if (!kvStore[scope]) kvStore[scope] = new Map();
- return kvStore[scope];
-}
-function resetKv() {
- for (const key of Object.keys(kvStore)) delete kvStore[key];
-}
-function seedKv(scope: string, key: string, value: unknown) {
- getScope(scope).set(key, value);
-}
-
-const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise => {
- if (fnId === "state::get") return getScope(data.scope).get(data.key) ?? null;
- if (fnId === "state::set") {
- getScope(data.scope).set(data.key, data.value);
- return { ok: true };
- }
- if (fnId === "state::delete") {
- getScope(data.scope).delete(data.key);
- return { ok: true };
- }
- if (fnId === "state::list") {
- return [...getScope(data.scope).entries()].map(([key, value]) => ({
- key,
- value,
- }));
- }
- if (fnId === "agent::create") return { id: data.id };
- if (fnId === "agent::chat") return { content: "mock response" };
- return null;
-});
-const mockTriggerVoid = vi.fn();
-
-const handlers: Record = {};
-vi.mock("iii-sdk", () => ({
- registerWorker: () => ({
- registerFunction: (config: any, handler: Function) => {
- handlers[config.id] = handler;
- },
- registerTrigger: vi.fn(),
- trigger: (req: any) =>
- req.action
- ? mockTriggerVoid(req.function_id, req.payload)
- : mockTrigger(req.function_id, req.payload),
- shutdown: vi.fn(),
- }),
- TriggerAction: { Void: () => ({}) },
- Logger: class { info() {} warn() {} error() {} },
-}));
-
-vi.mock("@agentos/shared/utils", () => ({
- httpOk: (req: any, data: any) => data,
- assertNoSsrf: vi.fn(async (url: string) => {
- const parsed = new URL(url);
- if (
- parsed.hostname === "localhost" ||
- parsed.hostname === "127.0.0.1" ||
- parsed.hostname === "10.0.0.1" ||
- parsed.hostname === "169.254.169.254"
- ) {
- throw new Error(`SSRF blocked: ${parsed.hostname}`);
- }
- }),
-}));
-
-vi.mock("fs/promises", () => ({
- readFile: vi.fn(async (p: string) => {
- if (p.includes("not-found")) throw new Error("ENOENT");
- return "file content";
- }),
- writeFile: vi.fn(async () => {}),
- readdir: vi.fn(async () => [
- { name: "file1.ts", isDirectory: () => false },
- { name: "src", isDirectory: () => true },
- ]),
- stat: vi.fn(async () => ({
- size: 1234,
- mtime: new Date("2026-01-01"),
- })),
-}));
-
-vi.mock("child_process", () => ({
- execFile: vi.fn((cmd: string, args: string[], opts: any, cb?: Function) => {
- if (!cb && typeof opts === "function") {
- cb = opts;
- }
- if (cmd === "rm" || cmd === "sudo" || cmd === "chmod") {
- const err: any = new Error("not allowed");
- err.code = 1;
- cb?.(err, "", "not allowed");
- return;
- }
- cb?.(null, "mock stdout", "");
- }),
-}));
-
-const WORKSPACE_ROOT = process.cwd();
-
-beforeEach(() => {
- resetKv();
- mockTrigger.mockClear();
- mockTriggerVoid.mockClear();
-});
-
-beforeAll(async () => {
- await import("../tools.js");
-});
-
-async function call(id: string, input: any) {
- const handler = handlers[id];
- if (!handler) throw new Error(`Handler ${id} not registered`);
- return handler(input);
-}
-
-describe("tool::file_read - path containment", () => {
- it("reads a file within workspace", async () => {
- const result = await call("tool::file_read", { path: "readme.md" });
- expect(result.content).toBe("file content");
- });
-
- it("rejects path traversal with ../", async () => {
- await expect(
- call("tool::file_read", { path: "../../etc/passwd" }),
- ).rejects.toThrow(/path traversal/i);
- });
-
- it("rejects absolute paths outside workspace", async () => {
- await expect(
- call("tool::file_read", { path: "/etc/passwd" }),
- ).rejects.toThrow(/path traversal/i);
- });
-
- it("limits content with maxBytes", async () => {
- const result = await call("tool::file_read", {
- path: "test.txt",
- maxBytes: 4,
- });
- expect(result.content).toBe("file");
- });
-
- it("returns file size in result", async () => {
- const result = await call("tool::file_read", { path: "test.txt" });
- expect(result.size).toBe(12);
- });
-});
-
-describe("tool::file_write - path containment", () => {
- it("writes file within workspace", async () => {
- const result = await call("tool::file_write", {
- path: "output.txt",
- content: "hello",
- });
- expect(result.written).toBe(true);
- expect(result.size).toBe(5);
- });
-
- it("rejects path traversal on write", async () => {
- await expect(
- call("tool::file_write", {
- path: "../../../tmp/evil.sh",
- content: "bad",
- }),
- ).rejects.toThrow(/path traversal/i);
- });
-});
-
-describe("tool::file_list", () => {
- it("lists directory contents", async () => {
- const result = await call("tool::file_list", { path: "." });
- expect(result.entries).toHaveLength(2);
- expect(result.entries[0].name).toBe("file1.ts");
- });
-
- it("rejects path traversal on list", async () => {
- await expect(call("tool::file_list", { path: "../../" })).rejects.toThrow(
- /path traversal/i,
- );
- });
-});
-
-describe("tool::apply_patch", () => {
- it("applies a basic patch", async () => {
- const result = await call("tool::apply_patch", {
- path: "file.ts",
- patch: "@@ -1,1 +1,1 @@\n+new line",
- });
- expect(result.patched).toBe(true);
- });
-
- it("rejects path traversal on patch", async () => {
- await expect(
- call("tool::apply_patch", {
- path: "../../../etc/hosts",
- patch: "",
- }),
- ).rejects.toThrow(/path traversal/i);
- });
-});
-
-describe("tool::shell_exec - command allowlist", () => {
- it("allows 'git' command", async () => {
- const result = await call("tool::shell_exec", {
- argv: ["git", "status"],
- });
- expect(result).toBeDefined();
- });
-
- it("allows 'node' command", async () => {
- const result = await call("tool::shell_exec", {
- argv: ["node", "--version"],
- });
- expect(result).toBeDefined();
- });
-
- it("allows 'npm' command", async () => {
- const result = await call("tool::shell_exec", {
- argv: ["npm", "list"],
- });
- expect(result).toBeDefined();
- });
-
- it("rejects 'rm' command", async () => {
- await expect(
- call("tool::shell_exec", { argv: ["rm", "-rf", "/"] }),
- ).rejects.toThrow("Command not allowed");
- });
-
- it("rejects 'sudo' command", async () => {
- await expect(
- call("tool::shell_exec", { argv: ["sudo", "anything"] }),
- ).rejects.toThrow("Command not allowed");
- });
-
- it("rejects 'chmod' command", async () => {
- await expect(
- call("tool::shell_exec", { argv: ["chmod", "777", "file"] }),
- ).rejects.toThrow("Command not allowed");
- });
-
- it("rejects 'bash' command", async () => {
- await expect(
- call("tool::shell_exec", { argv: ["bash", "-c", "malicious"] }),
- ).rejects.toThrow("Command not allowed");
- });
-
- it("rejects 'sh' command", async () => {
- await expect(
- call("tool::shell_exec", { argv: ["sh", "-c", "evil"] }),
- ).rejects.toThrow("Command not allowed");
- });
-
- it("rejects empty argv", async () => {
- await expect(call("tool::shell_exec", { argv: [] })).rejects.toThrow(
- "argv must be a non-empty array",
- );
- });
-
- it("rejects undefined argv", async () => {
- await expect(call("tool::shell_exec", {})).rejects.toThrow(
- "argv must be a non-empty array",
- );
- });
-
- it("allows 'python3' command", async () => {
- const result = await call("tool::shell_exec", {
- argv: ["python3", "--version"],
- });
- expect(result).toBeDefined();
- });
-
- it("allows 'cargo' command", async () => {
- const result = await call("tool::shell_exec", {
- argv: ["cargo", "build"],
- });
- expect(result).toBeDefined();
- });
-
- it("allows 'ls' command", async () => {
- const result = await call("tool::shell_exec", { argv: ["ls", "-la"] });
- expect(result).toBeDefined();
- });
-
- it("audits successful shell execution", async () => {
- await call("tool::shell_exec", { argv: ["git", "log"] });
- expect(mockTriggerVoid).toHaveBeenCalledWith(
- "security::audit",
- expect.objectContaining({ type: "shell_exec" }),
- );
- });
-
- it("truncates stdout to 100k chars", async () => {
- const result = await call("tool::shell_exec", {
- argv: ["echo", "hello"],
- });
- expect(typeof result.stdout).toBe("string");
- });
-
- it("uses basename of command for allowlist check", async () => {
- await expect(
- call("tool::shell_exec", { argv: ["/usr/bin/rm", "-rf", "/"] }),
- ).rejects.toThrow("Command not allowed");
- });
-
- it("rejects path traversal in cwd", async () => {
- await expect(
- call("tool::shell_exec", {
- argv: ["ls"],
- cwd: "../../../",
- }),
- ).rejects.toThrow(/path traversal/i);
- });
-});
-
-describe("tool::web_fetch - SSRF prevention", () => {
- it("blocks localhost URLs", async () => {
- await expect(
- call("tool::web_fetch", { url: "http://localhost:8080/secret" }),
- ).rejects.toThrow(/SSRF/);
- });
-
- it("blocks 127.0.0.1 URLs", async () => {
- await expect(
- call("tool::web_fetch", { url: "http://127.0.0.1/admin" }),
- ).rejects.toThrow(/SSRF/);
- });
-
- it("blocks private network 10.x URLs", async () => {
- await expect(
- call("tool::web_fetch", { url: "http://10.0.0.1/internal" }),
- ).rejects.toThrow(/SSRF/);
- });
-
- it("blocks cloud metadata endpoint", async () => {
- await expect(
- call("tool::web_fetch", {
- url: "http://169.254.169.254/latest/meta-data",
- }),
- ).rejects.toThrow(/SSRF/);
- });
-});
-
-describe("tool::agent_spawn - depth limit", () => {
- it("spawns a sub-agent with depth 1 from root", async () => {
- const result = await call("tool::agent_spawn", {
- template: "researcher",
- message: "help me",
- });
- expect(result.agentId).toBeDefined();
- expect(result.depth).toBe(1);
- });
-
- it("increments depth from parent", async () => {
- seedKv("agent_depth", "parent-1", { depth: 2 });
- const result = await call("tool::agent_spawn", {
- template: "coder",
- parentId: "parent-1",
- message: "code this",
- });
- expect(result.depth).toBe(3);
- });
-
- it("rejects when max depth (5) is exceeded", async () => {
- seedKv("agent_depth", "deep-agent", { depth: 5 });
- await expect(
- call("tool::agent_spawn", {
- template: "sub",
- parentId: "deep-agent",
- message: "go deeper",
- }),
- ).rejects.toThrow("Max agent depth");
- });
-
- it("allows exactly at depth 4 (parent depth 4, child becomes 5)", async () => {
- seedKv("agent_depth", "agent-4", { depth: 4 });
- const result = await call("tool::agent_spawn", {
- template: "sub",
- parentId: "agent-4",
- message: "last level",
- });
- expect(result.depth).toBe(5);
- });
-
- it("records depth in state for spawned agent", async () => {
- await call("tool::agent_spawn", {
- template: "test",
- message: "hi",
- });
- const calls = mockTrigger.mock.calls.filter(
- (c) => c[0] === "state::set" && c[1]?.scope === "agent_depth",
- );
- expect(calls.length).toBe(1);
- expect(calls[0][1].value.depth).toBe(1);
- });
-});
-
-describe("tool::agent_send", () => {
- it("sends message when agent has wildcard capability", async () => {
- seedKv("agents", "sender-1", {
- capabilities: { tools: ["*"] },
- });
- const result = await call("tool::agent_send", {
- agentId: "sender-1",
- targetAgentId: "target-1",
- message: "hello",
- });
- expect(result.sent).toBe(true);
- });
-
- it("sends message when agent has agent::send capability", async () => {
- seedKv("agents", "sender-2", {
- capabilities: { tools: ["agent::send"] },
- });
- const result = await call("tool::agent_send", {
- agentId: "sender-2",
- targetAgentId: "target-2",
- message: "hi",
- });
- expect(result.sent).toBe(true);
- });
-
- it("denies when agent lacks send capability", async () => {
- seedKv("agents", "sender-3", {
- capabilities: { tools: ["tool::file_read"] },
- });
- await expect(
- call("tool::agent_send", {
- agentId: "sender-3",
- targetAgentId: "target-3",
- message: "blocked",
- }),
- ).rejects.toThrow("does not have agent::send capability");
- });
-
- it("allows send when no agentId provided (system context)", async () => {
- const result = await call("tool::agent_send", {
- agentId: "",
- targetAgentId: "target-4",
- message: "system message",
- });
- expect(result.sent).toBe(true);
- });
-});
diff --git a/src/__tests__/types-extended.test.ts b/src/__tests__/types-extended.test.ts
deleted file mode 100644
index 91aad20..0000000
--- a/src/__tests__/types-extended.test.ts
+++ /dev/null
@@ -1,279 +0,0 @@
-import { describe, it, expect } from "vitest";
-import type {
- AgentConfig,
- AgentPersona,
- ChatRequest,
- ChatResponse,
- Division,
- TokenUsage,
- ToolCall,
-} from "../types.js";
-
-describe("AgentConfig type", () => {
- it("accepts minimal config with only name", () => {
- const config: AgentConfig = { name: "Test Agent" };
- expect(config.name).toBe("Test Agent");
- expect(config.id).toBeUndefined();
- expect(config.description).toBeUndefined();
- });
-
- it("accepts full config with all fields", () => {
- const config: AgentConfig = {
- id: "agent-1",
- name: "Full Agent",
- description: "An agent with all fields",
- model: { provider: "anthropic", model: "claude-opus-4-6", maxTokens: 8192 },
- systemPrompt: "Be helpful",
- toolProfile: "default",
- capabilities: {
- tools: ["*"],
- memoryScopes: ["self"],
- networkHosts: ["example.com"],
- },
- resources: { maxTokensPerHour: 100000 },
- tags: ["test", "full"],
- createdAt: Date.now(),
- };
- expect(config.id).toBe("agent-1");
- expect(config.model?.provider).toBe("anthropic");
- expect(config.capabilities?.tools).toContain("*");
- expect(config.resources?.maxTokensPerHour).toBe(100000);
- });
-
- it("allows model without all sub-fields", () => {
- const config: AgentConfig = {
- name: "Partial Model",
- model: { provider: "openai" },
- };
- expect(config.model?.model).toBeUndefined();
- expect(config.model?.maxTokens).toBeUndefined();
- });
-
- it("allows capabilities without optional scopes", () => {
- const config: AgentConfig = {
- name: "Minimal Caps",
- capabilities: { tools: ["file::read"] },
- };
- expect(config.capabilities?.memoryScopes).toBeUndefined();
- expect(config.capabilities?.networkHosts).toBeUndefined();
- });
-
- it("allows empty tags array", () => {
- const config: AgentConfig = { name: "No Tags", tags: [] };
- expect(config.tags).toHaveLength(0);
- });
-});
-
-describe("ChatRequest type", () => {
- it("requires agentId and message", () => {
- const req: ChatRequest = {
- agentId: "agent-1",
- message: "Hello",
- };
- expect(req.agentId).toBe("agent-1");
- expect(req.message).toBe("Hello");
- });
-
- it("accepts optional sessionId", () => {
- const req: ChatRequest = {
- agentId: "agent-1",
- message: "Hi",
- sessionId: "session-abc",
- };
- expect(req.sessionId).toBe("session-abc");
- });
-
- it("accepts optional systemPrompt", () => {
- const req: ChatRequest = {
- agentId: "agent-1",
- message: "Test",
- systemPrompt: "Be concise",
- };
- expect(req.systemPrompt).toBe("Be concise");
- });
-});
-
-describe("ChatResponse type", () => {
- it("includes content and iterations", () => {
- const res: ChatResponse = {
- content: "Response text",
- iterations: 3,
- };
- expect(res.content).toBe("Response text");
- expect(res.iterations).toBe(3);
- });
-
- it("includes optional model and usage", () => {
- const res: ChatResponse = {
- content: "Full response",
- model: "claude-opus-4-6",
- usage: { input: 100, output: 50, total: 150 },
- iterations: 1,
- };
- expect(res.model).toBe("claude-opus-4-6");
- expect(res.usage?.total).toBe(150);
- });
-});
-
-describe("TokenUsage type", () => {
- it("tracks input, output, and total", () => {
- const usage: TokenUsage = { input: 200, output: 100, total: 300 };
- expect(usage.input + usage.output).toBe(usage.total);
- });
-
- it("handles zero usage", () => {
- const usage: TokenUsage = { input: 0, output: 0, total: 0 };
- expect(usage.total).toBe(0);
- });
-});
-
-describe("ToolCall type", () => {
- it("has callId, id, and arguments", () => {
- const tc: ToolCall = {
- callId: "call-1",
- id: "memory::store",
- arguments: { content: "data", agentId: "a1" },
- };
- expect(tc.callId).toBe("call-1");
- expect(tc.id).toBe("memory::store");
- expect(tc.arguments.content).toBe("data");
- });
-
- it("handles empty arguments", () => {
- const tc: ToolCall = {
- callId: "call-2",
- id: "agent::list",
- arguments: {},
- };
- expect(Object.keys(tc.arguments)).toHaveLength(0);
- });
-
- it("handles nested arguments", () => {
- const tc: ToolCall = {
- callId: "call-3",
- id: "tool::complex",
- arguments: {
- config: { nested: true, depth: 2 },
- items: [1, 2, 3],
- },
- };
- expect((tc.arguments.config as any).nested).toBe(true);
- });
-
- it("extracts capability namespace from id", () => {
- const tc: ToolCall = {
- callId: "call-4",
- id: "security::check_capability",
- arguments: {},
- };
- const namespace = tc.id.split("::")[0];
- expect(namespace).toBe("security");
- });
-
- it("handles id without namespace separator", () => {
- const tc: ToolCall = {
- callId: "call-5",
- id: "simple_tool",
- arguments: {},
- };
- const namespace = tc.id.split("::")[0];
- expect(namespace).toBe("simple_tool");
- });
-});
-
-describe("Division type", () => {
- it("accepts all valid division values", () => {
- const divisions: Division[] = [
- "engineering",
- "quality",
- "research",
- "operations",
- "communication",
- "support",
- "personal",
- "design",
- "marketing",
- ];
- expect(divisions).toHaveLength(9);
- });
-});
-
-describe("AgentPersona type", () => {
- it("accepts empty persona", () => {
- const persona: AgentPersona = {};
- expect(persona.division).toBeUndefined();
- expect(persona.communicationStyle).toBeUndefined();
- });
-
- it("accepts persona with only division", () => {
- const persona: AgentPersona = { division: "engineering" };
- expect(persona.division).toBe("engineering");
- });
-
- it("accepts full persona with all fields", () => {
- const persona: AgentPersona = {
- division: "quality",
- communicationStyle: "Constructive and specific",
- criticalRules: ["Never approve insecure code", "Provide alternatives"],
- workflow: {
- phases: ["Scan", "Analyze", "Comment", "Verify", "Approve"],
- },
- successMetrics: {
- metrics: ["Bugs caught >90%", "Review turnaround <4h"],
- },
- learning: {
- patterns: ["Common bug patterns", "Effective review phrasing"],
- },
- };
- expect(persona.division).toBe("quality");
- expect(persona.criticalRules).toHaveLength(2);
- expect(persona.workflow?.phases).toHaveLength(5);
- expect(persona.successMetrics?.metrics).toHaveLength(2);
- expect(persona.learning?.patterns).toHaveLength(2);
- });
-});
-
-describe("AgentConfig with persona", () => {
- it("accepts config without persona (backward compat)", () => {
- const config: AgentConfig = { name: "Legacy Agent" };
- expect(config.persona).toBeUndefined();
- });
-
- it("accepts config with persona", () => {
- const config: AgentConfig = {
- name: "Enriched Agent",
- persona: {
- division: "engineering",
- communicationStyle: "Direct and technical",
- criticalRules: ["Always write tests"],
- workflow: { phases: ["Analyze", "Implement", "Test"] },
- successMetrics: { metrics: ["Coverage >80%"] },
- learning: { patterns: ["Refactoring approaches"] },
- },
- tags: ["coding"],
- };
- expect(config.persona?.division).toBe("engineering");
- expect(config.persona?.workflow?.phases).toContain("Implement");
- expect(config.persona?.successMetrics?.metrics).toHaveLength(1);
- });
-
- it("filters agents by division", () => {
- const agents: AgentConfig[] = [
- { name: "coder", persona: { division: "engineering" } },
- { name: "reviewer", persona: { division: "quality" } },
- { name: "architect", persona: { division: "engineering" } },
- { name: "assistant", persona: { division: "personal" } },
- { name: "legacy" },
- ];
-
- const engineering = agents.filter(
- (a) => a.persona?.division === "engineering",
- );
- expect(engineering).toHaveLength(2);
- expect(engineering.map((a) => a.name)).toEqual(["coder", "architect"]);
-
- const withoutDivision = agents.filter((a) => !a.persona?.division);
- expect(withoutDivision).toHaveLength(1);
- expect(withoutDivision[0].name).toBe("legacy");
- });
-});
diff --git a/src/api.ts b/src/api.ts
deleted file mode 100644
index dd2728a..0000000
--- a/src/api.ts
+++ /dev/null
@@ -1,476 +0,0 @@
-import { registerWorker } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { readFileSync } from "node:fs";
-import { join } from "node:path";
-import { requireAuth, sanitizeId } from "@agentos/shared/utils";
-import { SECURITY_HEADERS } from "@agentos/shared/security-headers";
-import { safeInt } from "@agentos/shared/validate";
-import { safeCall } from "@agentos/shared/errors";
-import { shutdownManager } from "@agentos/shared/shutdown";
-
-function withSecHeaders(response: { status_code: number; body: any }): {
- status_code: number;
- body: any;
- headers: Record;
-} {
- return { ...response, headers: { ...SECURITY_HEADERS } };
-}
-
-const sdk = registerWorker(ENGINE_URL, { workerName: "api", otel: OTEL_CONFIG });
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const PKG_VERSION = (() => {
- try {
- const pkg = JSON.parse(
- readFileSync(join(process.cwd(), "package.json"), "utf8"),
- );
- return pkg.version || "0.0.0";
- } catch {
- return "0.0.0";
- }
-})();
-
-function authGuard(req: any): { status_code: number; body: any } | null {
- try {
- if (req.headers) requireAuth(req);
- return null;
- } catch (err: any) {
- if (err.statusCode === 401) {
- return { status_code: 401, body: { error: "Unauthorized" } };
- }
- throw err;
- }
-}
-
-async function rateGuard(
- req: any,
- operation: string,
-): Promise<{ status_code: number; body: any } | null> {
- const rateResult: any = await safeCall(
- () =>
- trigger({
- function_id: "rate::check",
- payload: {
- ip: req.headers?.["x-forwarded-for"] || req.remote_addr || "unknown",
- operation,
- },
- }),
- { allowed: true },
- { operation: "rate_check", functionId: "api::rateGuard" },
- );
- if (!rateResult.allowed) {
- return {
- status_code: 429,
- body: { error: "Rate limit exceeded", retryAfter: rateResult.retryAfter },
- };
- }
- return null;
-}
-
-registerFunction(
- {
- id: "api::chat_completions",
- description: "OpenAI-compatible chat completions",
- metadata: { category: "api" },
- request_format: [
- { name: "model", type: "string", required: true, description: "Model identifier" },
- {
- name: "messages",
- type: "array",
- required: true,
- description: "Array of chat messages",
- items: {
- name: "message",
- type: "object",
- body: [
- { name: "role", type: "string", required: true },
- { name: "content", type: "string", required: true },
- ],
- },
- },
- ],
- response_format: [
- { name: "id", type: "string", description: "Completion ID" },
- { name: "object", type: "string", description: "Object type" },
- { name: "created", type: "number", description: "Unix timestamp" },
- { name: "model", type: "string", description: "Model used" },
- {
- name: "choices",
- type: "array",
- description: "Completion choices",
- items: {
- name: "choice",
- type: "object",
- body: [
- { name: "index", type: "number" },
- {
- name: "message",
- type: "object",
- body: [
- { name: "role", type: "string" },
- { name: "content", type: "string" },
- ],
- },
- { name: "finish_reason", type: "string" },
- ],
- },
- },
- {
- name: "usage",
- type: "object",
- description: "Token usage",
- body: [
- { name: "prompt_tokens", type: "number" },
- { name: "completion_tokens", type: "number" },
- { name: "total_tokens", type: "number" },
- ],
- },
- ],
- },
- async (req) => {
- const authErr = authGuard(req);
- if (authErr) return authErr;
- const rateErr = await rateGuard(req, "message");
- if (rateErr) return rateErr;
-
- const { model, messages } = req.body || req;
- const lastMessage = messages?.[messages.length - 1]?.content || "";
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId: "default",
- message: lastMessage,
- sessionId: `api:${Date.now()}`,
- },
- });
-
- return withSecHeaders({
- status_code: 200,
- body: {
- id: `chatcmpl-${crypto.randomUUID().slice(0, 8)}`,
- object: "chat.completion",
- created: Math.floor(Date.now() / 1000),
- model: response.model || model || "claude-sonnet-4-6",
- choices: [
- {
- index: 0,
- message: { role: "assistant", content: response.content },
- finish_reason: "stop",
- },
- ],
- usage: {
- prompt_tokens: response.usage?.input || 0,
- completion_tokens: response.usage?.output || 0,
- total_tokens: response.usage?.total || 0,
- },
- },
- });
- },
-);
-
-registerFunction(
- {
- id: "api::agent_message",
- description: "Send message to a specific agent",
- metadata: { category: "api" },
- request_format: [
- { name: "message", type: "string", required: true, description: "Message to send" },
- { name: "sessionId", type: "string", required: false, description: "Optional session ID" },
- ],
- response_format: [
- { name: "content", type: "string", description: "Agent response content" },
- { name: "model", type: "string", description: "Model used" },
- {
- name: "usage",
- type: "object",
- description: "Token usage",
- body: [
- { name: "input", type: "number" },
- { name: "output", type: "number" },
- { name: "total", type: "number" },
- ],
- },
- ],
- },
- async (req) => {
- const authErr = authGuard(req);
- if (authErr) return authErr;
- const rateErr = await rateGuard(req, "message");
- if (rateErr) return rateErr;
-
- const agentId = sanitizeId(req.path_params?.id);
- const { message, sessionId } = req.body || req;
-
- const response = await trigger({
- function_id: "agent::chat",
- payload: { agentId, message, sessionId },
- });
-
- return withSecHeaders({ status_code: 200, body: response });
- },
-);
-
-registerFunction(
- {
- id: "api::list_agents",
- description: "List all agents",
- metadata: { category: "api" },
- },
- async (req: any) => {
- const authErr = authGuard(req);
- if (authErr) return authErr;
- const rateErr = await rateGuard(req, "read");
- if (rateErr) return rateErr;
-
- const agents = await trigger({ function_id: "agent::list", payload: {} });
- return withSecHeaders({ status_code: 200, body: agents });
- },
-);
-
-registerFunction(
- {
- id: "api::create_agent",
- description: "Create a new agent",
- metadata: { category: "api" },
- request_format: [
- { name: "name", type: "string", required: true, description: "Agent name" },
- { name: "systemPrompt", type: "string", required: true, description: "System prompt for the agent" },
- { name: "model", type: "string", required: true, description: "LLM model identifier" },
- ],
- response_format: [
- { name: "id", type: "string", description: "Created agent ID" },
- { name: "name", type: "string", description: "Agent name" },
- { name: "createdAt", type: "number", description: "Creation timestamp" },
- ],
- },
- async (req) => {
- const authErr = authGuard(req);
- if (authErr) return authErr;
- const rateErr = await rateGuard(req, "write");
- if (rateErr) return rateErr;
-
- const result = await trigger({
- function_id: "agent::create",
- payload: req.body || req,
- });
- return withSecHeaders({ status_code: 201, body: result });
- },
-);
-
-registerFunction(
- {
- id: "api::get_agent",
- description: "Get agent by ID",
- metadata: { category: "api" },
- },
- async (req) => {
- const authErr = authGuard(req);
- if (authErr) return authErr;
- const rateErr = await rateGuard(req, "read");
- if (rateErr) return rateErr;
-
- const agent = await trigger({
- function_id: "state::get",
- payload: {
- scope: "agents",
- key: sanitizeId(req.path_params?.id),
- },
- });
- if (!agent)
- return withSecHeaders({
- status_code: 404,
- body: { error: "Agent not found" },
- });
- return withSecHeaders({ status_code: 200, body: agent });
- },
-);
-
-registerFunction(
- {
- id: "api::delete_agent",
- description: "Delete an agent",
- metadata: { category: "api" },
- },
- async (req) => {
- const authErr = authGuard(req);
- if (authErr) return authErr;
- const rateErr = await rateGuard(req, "write");
- if (rateErr) return rateErr;
-
- await trigger({
- function_id: "agent::delete",
- payload: { agentId: sanitizeId(req.path_params?.id) },
- });
- return withSecHeaders({ status_code: 204, body: null });
- },
-);
-
-registerFunction(
- {
- id: "api::agent_sessions",
- description: "List sessions for an agent",
- metadata: { category: "api" },
- },
- async (req) => {
- const authErr = authGuard(req);
- if (authErr) return authErr;
- const rateErr = await rateGuard(req, "read");
- if (rateErr) return rateErr;
-
- const sessions = await trigger({
- function_id: "state::list",
- payload: { scope: `sessions:${sanitizeId(req.path_params?.id)}` },
- });
- return withSecHeaders({ status_code: 200, body: sessions });
- },
-);
-
-registerFunction(
- {
- id: "api::health",
- description: "Health check endpoint",
- metadata: { category: "api" },
- response_format: [
- { name: "status", type: "string", description: "Health status" },
- { name: "version", type: "string", description: "Application version" },
- { name: "workers", type: "number", description: "Active worker count" },
- { name: "uptime", type: "number", description: "Process uptime in seconds" },
- ],
- },
- async (req: any) => {
- if (shutdownManager.isShuttingDown()) {
- return withSecHeaders({
- status_code: 503,
- body: {
- status: "shutting_down",
- version: PKG_VERSION,
- inFlight: shutdownManager.inFlightCount(),
- },
- });
- }
-
- const authErr = authGuard(req);
- if (authErr) return authErr;
- const rateErr = await rateGuard(req, "read");
- if (rateErr) return rateErr;
-
- const workers: any = await safeCall(
- () => trigger({ function_id: "engine::workers::list", payload: {} }),
- [],
- { operation: "list_workers", functionId: "api::health" },
- );
- return withSecHeaders({
- status_code: 200,
- body: {
- status: "healthy",
- version: PKG_VERSION,
- workers: workers.length,
- uptime: process.uptime(),
- },
- });
- },
-);
-
-registerFunction(
- {
- id: "api::costs",
- description: "Get cost breakdown",
- metadata: { category: "api" },
- },
- async (req: any) => {
- const authErr = authGuard(req);
- if (authErr) return authErr;
- const rateErr = await rateGuard(req, "read");
- if (rateErr) return rateErr;
-
- const today = new Date().toISOString().slice(0, 10);
- const costs = await safeCall(
- () => trigger({
- function_id: "state::get",
- payload: { scope: "costs", key: today },
- }),
- null,
- { operation: "get_costs", functionId: "api::costs" },
- );
- return withSecHeaders({
- status_code: 200,
- body: costs || { totalCost: 0 },
- });
- },
-);
-
-registerFunction(
- {
- id: "api::memory_query",
- description: "Query agent memory",
- metadata: { category: "api" },
- },
- async (req) => {
- const authErr = authGuard(req);
- if (authErr) return authErr;
- const rateErr = await rateGuard(req, "read");
- if (rateErr) return rateErr;
-
- const agentId = sanitizeId(req.path_params?.id);
- const { query, limit: rawLimit } = req.query_params || req.body || {};
- const limit = safeInt(rawLimit, 1, 200, 10);
- const results = await trigger({
- function_id: "memory::recall",
- payload: { agentId, query, limit },
- });
- return withSecHeaders({ status_code: 200, body: results });
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "api::chat_completions",
- config: { api_path: "v1/chat/completions", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "api::list_agents",
- config: { api_path: "api/agents", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "api::create_agent",
- config: { api_path: "api/agents", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "api::get_agent",
- config: { api_path: "api/agents/:id", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "api::delete_agent",
- config: { api_path: "api/agents/:id", http_method: "DELETE" },
-});
-registerTrigger({
- type: "http",
- function_id: "api::agent_message",
- config: { api_path: "api/agents/:id/message", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "api::agent_sessions",
- config: { api_path: "api/agents/:id/sessions", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "api::memory_query",
- config: { api_path: "api/agents/:id/memory", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "api::health",
- config: { api_path: "api/health", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "api::costs",
- config: { api_path: "api/costs", http_method: "GET" },
-});
diff --git a/src/artifact-dag.ts b/src/artifact-dag.ts
deleted file mode 100644
index dbfd467..0000000
--- a/src/artifact-dag.ts
+++ /dev/null
@@ -1,345 +0,0 @@
-import { registerWorker, TriggerAction, Logger } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { requireAuth, sanitizeId } from "@agentos/shared/utils";
-import { createRecordMetric } from "@agentos/shared/metrics";
-import { safeCall } from "@agentos/shared/errors";
-import { createHash, randomUUID } from "node:crypto";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "artifact-dag",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-const triggerVoid = (id: string, payload: unknown) =>
- trigger({ function_id: id, payload, action: TriggerAction.Void() });
-
-const log = new Logger();
-const recordMetric = createRecordMetric(triggerVoid);
-
-const MAX_CONTENT_SIZE = 512_000;
-
-function contentHash(content: unknown): string {
- const str = typeof content === "string" ? content : JSON.stringify(content);
- return createHash("sha256").update(str).digest("hex").slice(0, 16);
-}
-
-registerFunction(
- {
- id: "artifact::push",
- description: "Push a content artifact as a DAG node",
- metadata: { category: "artifact" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const {
- agentId,
- content,
- label,
- parentIds,
- swarmId,
- metadata: extraMeta,
- } = req.body || req;
-
- if (!agentId || content === undefined || !label) {
- throw Object.assign(
- new Error("agentId, content, and label are required"),
- { statusCode: 400 },
- );
- }
-
- const contentStr =
- typeof content === "string" ? content : JSON.stringify(content);
- if (contentStr.length > MAX_CONTENT_SIZE) {
- throw Object.assign(
- new Error(`Content exceeds ${MAX_CONTENT_SIZE} byte limit`),
- { statusCode: 400 },
- );
- }
-
- const safeParentIds = (parentIds || []).map((id: string) => sanitizeId(id));
- for (const pid of safeParentIds) {
- const parent = await trigger({ function_id: "state::get", payload: {
- scope: "artifacts",
- key: pid,
- } });
- if (!parent) {
- throw Object.assign(new Error(`Parent artifact ${pid} not found`), {
- statusCode: 404,
- });
- }
- }
-
- const nodeId = randomUUID();
- const hash = contentHash(content);
-
- const node = {
- id: nodeId,
- agentId: sanitizeId(agentId),
- swarmId: swarmId ? sanitizeId(swarmId) : undefined,
- parentIds: safeParentIds,
- content,
- contentHash: hash,
- label,
- createdAt: Date.now(),
- metadata: extraMeta || {},
- };
-
- await trigger({ function_id: "state::set", payload: {
- scope: "artifacts",
- key: nodeId,
- value: node,
- } });
-
- if (swarmId) {
- triggerVoid("publish", {
- topic: `artifact:${sanitizeId(swarmId)}`,
- data: { type: "artifact_pushed", nodeId, agentId: node.agentId, label },
- });
- }
-
- log.info("Artifact pushed", { nodeId, agentId: node.agentId, label });
- recordMetric("artifact_pushed", 1, { agentId: node.agentId }, "counter");
-
- return { nodeId, contentHash: hash, parentIds: safeParentIds };
- },
-);
-
-registerFunction(
- {
- id: "artifact::fetch",
- description: "Fetch an artifact node by ID",
- metadata: { category: "artifact" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const { nodeId } = req.body || req.query || req;
- if (!nodeId) {
- throw Object.assign(new Error("nodeId is required"), {
- statusCode: 400,
- });
- }
-
- const node = await trigger({ function_id: "state::get", payload: {
- scope: "artifacts",
- key: sanitizeId(nodeId),
- } });
- if (!node) {
- throw Object.assign(new Error("Artifact not found"), {
- statusCode: 404,
- });
- }
-
- return node;
- },
-);
-
-registerFunction(
- {
- id: "artifact::children",
- description: "Get child nodes of a DAG artifact",
- metadata: { category: "artifact" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const { nodeId } = req.body || req.query || req;
- if (!nodeId) {
- throw Object.assign(new Error("nodeId is required"), {
- statusCode: 400,
- });
- }
-
- const safeId = sanitizeId(nodeId);
- const all: any[] = await safeCall(
- () => trigger({ function_id: "state::list", payload: { scope: "artifacts" } }),
- [],
- { operation: "list_artifacts" },
- );
-
- return (Array.isArray(all) ? all : [])
- .map((e: any) => e.value || e)
- .filter(
- (n: any) => Array.isArray(n.parentIds) && n.parentIds.includes(safeId),
- );
- },
-);
-
-registerFunction(
- {
- id: "artifact::leaves",
- description: "Find frontier artifact nodes with no children",
- metadata: { category: "artifact" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const { swarmId, agentId } = req.body || req.query || req;
-
- const all: any[] = await safeCall(
- () => trigger({ function_id: "state::list", payload: { scope: "artifacts" } }),
- [],
- { operation: "list_artifacts" },
- );
-
- let nodes = (Array.isArray(all) ? all : []).map(
- (e: any) => e.value || e,
- );
-
- if (swarmId) {
- nodes = nodes.filter((n: any) => n.swarmId === swarmId);
- }
- if (agentId) {
- nodes = nodes.filter((n: any) => n.agentId === agentId);
- }
-
- const allParentIds = new Set();
- for (const n of nodes) {
- for (const pid of n.parentIds || []) {
- allParentIds.add(pid);
- }
- }
-
- const leaves = nodes.filter((n: any) => !allParentIds.has(n.id));
-
- return leaves.map((n: any) => ({
- id: n.id,
- agentId: n.agentId,
- label: n.label,
- contentHash: n.contentHash,
- parentIds: n.parentIds,
- createdAt: n.createdAt,
- }));
- },
-);
-
-registerFunction(
- {
- id: "artifact::diff",
- description: "Compare two artifact nodes",
- metadata: { category: "artifact" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const { nodeIdA, nodeIdB } = req.body || req;
- if (!nodeIdA || !nodeIdB) {
- throw Object.assign(
- new Error("nodeIdA and nodeIdB are required"),
- { statusCode: 400 },
- );
- }
-
- const [nodeA, nodeB] = await Promise.all([
- trigger({ function_id: "state::get", payload: {
- scope: "artifacts",
- key: sanitizeId(nodeIdA),
- } }),
- trigger({ function_id: "state::get", payload: {
- scope: "artifacts",
- key: sanitizeId(nodeIdB),
- } }),
- ]);
-
- if (!nodeA) {
- throw Object.assign(new Error(`Artifact ${nodeIdA} not found`), {
- statusCode: 404,
- });
- }
- if (!nodeB) {
- throw Object.assign(new Error(`Artifact ${nodeIdB} not found`), {
- statusCode: 404,
- });
- }
-
- const a = nodeA as any;
- const b = nodeB as any;
-
- const strA =
- typeof a.content === "string" ? a.content : JSON.stringify(a.content);
- const strB =
- typeof b.content === "string" ? b.content : JSON.stringify(b.content);
-
- return {
- nodeIdA,
- nodeIdB,
- contentMatch: a.contentHash === b.contentHash,
- sizeA: strA.length,
- sizeB: strB.length,
- agentA: a.agentId,
- agentB: b.agentId,
- createdAtA: a.createdAt,
- createdAtB: b.createdAt,
- };
- },
-);
-
-registerFunction(
- {
- id: "artifact::history",
- description: "Get all artifacts by agent or swarm",
- metadata: { category: "artifact" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const { agentId, swarmId, limit } = req.body || req.query || req;
-
- const all: any[] = await safeCall(
- () => trigger({ function_id: "state::list", payload: { scope: "artifacts" } }),
- [],
- { operation: "list_artifacts" },
- );
-
- let nodes = (Array.isArray(all) ? all : []).map(
- (e: any) => e.value || e,
- );
-
- if (agentId) {
- nodes = nodes.filter((n: any) => n.agentId === agentId);
- }
- if (swarmId) {
- nodes = nodes.filter((n: any) => n.swarmId === swarmId);
- }
-
- nodes.sort((a: any, b: any) => (b.createdAt || 0) - (a.createdAt || 0));
-
- const cap = typeof limit === "number" && limit > 0 ? limit : 50;
- return nodes.slice(0, cap).map((n: any) => ({
- id: n.id,
- agentId: n.agentId,
- swarmId: n.swarmId,
- label: n.label,
- contentHash: n.contentHash,
- parentIds: n.parentIds,
- createdAt: n.createdAt,
- }));
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "artifact::push",
- config: { api_path: "api/artifact/push", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "artifact::fetch",
- config: { api_path: "api/artifact/:nodeId", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "artifact::children",
- config: { api_path: "api/artifact/:nodeId/children", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "artifact::leaves",
- config: { api_path: "api/artifact/leaves", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "artifact::diff",
- config: { api_path: "api/artifact/diff", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "artifact::history",
- config: { api_path: "api/artifact/history", http_method: "GET" },
-});
diff --git a/src/channels/dingtalk.ts b/src/channels/dingtalk.ts
deleted file mode 100644
index c2e931a..0000000
--- a/src/channels/dingtalk.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { createHmac } from "crypto";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-dingtalk",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-const API_URL = "https://oapi.dingtalk.com/robot/send";
-
-registerFunction(
- {
- id: "channel::dingtalk::webhook",
- description: "Handle DingTalk robot webhook",
- },
- async (req) => {
- const body = req.body || req;
- const { text, conversationId, senderNick, senderId } = body;
-
- const content = text?.content?.trim();
- if (!content) return { status_code: 200, body: { ok: true } };
-
- const agentId = await resolveAgent(sdk, "dingtalk", conversationId);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: content,
- sessionId: `dingtalk:${conversationId}`,
- },
- });
-
- if (!response?.content) {
- return { status_code: 200, body: { ok: true } };
- }
- await sendMessage(response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "dingtalk", conversationId, senderId },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::dingtalk::webhook",
- config: { api_path: "webhook/dingtalk", http_method: "POST" },
-});
-
-async function sendMessage(text: string) {
- const token = await getSecret("DINGTALK_TOKEN");
- if (!token) {
- throw new Error("DINGTALK_TOKEN not configured");
- }
- const secret = await getSecret("DINGTALK_SECRET");
- if (!secret) {
- throw new Error("DINGTALK_SECRET not configured");
- }
- const timestamp = Date.now();
- const sign = createHmac("sha256", secret)
- .update(`${timestamp}\n${secret}`)
- .digest("base64");
- const chunks = splitMessage(text, 4096);
- for (const chunk of chunks) {
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 10_000);
- try {
- const res = await fetch(
- `${API_URL}?access_token=${encodeURIComponent(token)}×tamp=${timestamp}&sign=${encodeURIComponent(sign)}`,
- {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ msgtype: "text", text: { content: chunk } }),
- signal: controller.signal,
- },
- );
- if (!res.ok) {
- const body = await res.text().catch(() => "");
- throw new Error(
- `DingTalk send failed (${res.status}): ${body.slice(0, 300)}`,
- );
- }
- } finally {
- clearTimeout(timer);
- }
- }
-}
diff --git a/src/channels/discourse.ts b/src/channels/discourse.ts
deleted file mode 100644
index 55f3e98..0000000
--- a/src/channels/discourse.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-discourse",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-registerFunction(
- {
- id: "channel::discourse::webhook",
- description: "Handle Discourse webhook",
- },
- async (req) => {
- const body = req.body || req;
- const post = body.post;
-
- if (!post?.raw) return { status_code: 200, body: { ok: true } };
-
- const topicId = post.topic_id;
- const text = post.raw;
- const username = post.username;
-
- const agentId = await resolveAgent(sdk, "discourse", String(topicId));
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `discourse:${topicId}`,
- },
- });
-
- await sendMessage(topicId, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "discourse", topicId, username },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::discourse::webhook",
- config: { api_path: "webhook/discourse", http_method: "POST" },
-});
-
-async function sendMessage(topicId: number, text: string) {
- const baseUrl = await getSecret("DISCOURSE_URL");
- if (!baseUrl) {
- throw new Error("DISCOURSE_URL not configured");
- }
- const apiKey = await getSecret("DISCOURSE_API_KEY");
- if (!apiKey) {
- throw new Error("DISCOURSE_API_KEY not configured");
- }
- const apiUsername = await getSecret("DISCOURSE_API_USERNAME");
- const res = await fetch(`${baseUrl}/posts.json`, {
- method: "POST",
- headers: {
- "Api-Key": apiKey,
- "Api-Username": apiUsername || "system",
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- topic_id: topicId,
- raw: text,
- }),
- });
- if (!res.ok) {
- const body = await res.text().catch(() => "");
- throw new Error(
- `Discourse send failed (${res.status}): ${body.slice(0, 300)}`,
- );
- }
-}
diff --git a/src/channels/feishu.ts b/src/channels/feishu.ts
deleted file mode 100644
index 315ac9b..0000000
--- a/src/channels/feishu.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-feishu",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-const API_URL = "https://open.feishu.cn/open-apis";
-
-let tenantToken = "";
-let tenantTokenExpiry = 0;
-let tenantTokenPromise: Promise | null = null;
-
-registerFunction(
- {
- id: "channel::feishu::webhook",
- description: "Handle Feishu/Lark event callback",
- },
- async (req) => {
- const body = req.body || req;
-
- if (body.challenge)
- return { status_code: 200, body: { challenge: body.challenge } };
-
- const event = body.event;
- if (!event?.message?.content)
- return { status_code: 200, body: { ok: true } };
-
- const chatId = event.message.chat_id;
- const content = JSON.parse(event.message.content);
- const text = content.text || "";
- const userId = event.sender?.sender_id?.user_id;
-
- const agentId = await resolveAgent(sdk, "feishu", chatId);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `feishu:${chatId}`,
- },
- });
-
- await sendMessage(chatId, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "feishu", chatId, userId },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::feishu::webhook",
- config: { api_path: "webhook/feishu", http_method: "POST" },
-});
-
-async function getTenantToken(): Promise {
- if (tenantToken && Date.now() < tenantTokenExpiry) return tenantToken;
- if (tenantTokenPromise) return tenantTokenPromise;
- tenantTokenPromise = refreshTenantToken();
- try {
- return await tenantTokenPromise;
- } finally {
- tenantTokenPromise = null;
- }
-}
-
-async function refreshTenantToken(): Promise {
- const appId = await getSecret("FEISHU_APP_ID");
- if (!appId) {
- throw new Error("FEISHU_APP_ID not configured");
- }
- const appSecret = await getSecret("FEISHU_APP_SECRET");
- if (!appSecret) {
- throw new Error("FEISHU_APP_SECRET not configured");
- }
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 15_000);
- try {
- const res = await fetch(`${API_URL}/auth/v3/tenant_access_token/internal`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
- signal: controller.signal,
- });
- if (!res.ok) {
- const body = await res.text().catch(() => "");
- throw new Error(
- `Feishu token fetch failed (${res.status}): ${body.slice(0, 300)}`,
- );
- }
- const data = (await res.json()) as {
- code?: number;
- msg?: string;
- tenant_access_token?: string;
- };
- if (data.code !== 0) {
- throw new Error(
- `Feishu token API error (code=${data.code}): ${data.msg || "unknown"}`,
- );
- }
- if (!data.tenant_access_token) {
- throw new Error("Feishu token response missing tenant_access_token");
- }
- tenantToken = data.tenant_access_token;
- tenantTokenExpiry = Date.now() + 5400_000;
- return tenantToken;
- } catch (err) {
- tenantToken = "";
- tenantTokenExpiry = 0;
- throw err;
- } finally {
- clearTimeout(timer);
- }
-}
-
-async function sendMessage(chatId: string, text: string) {
- const token = await getTenantToken();
- const chunks = splitMessage(text, 4096);
- for (const chunk of chunks) {
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 15_000);
- try {
- const res = await fetch(
- `${API_URL}/im/v1/messages?receive_id_type=chat_id`,
- {
- method: "POST",
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- receive_id: chatId,
- msg_type: "text",
- content: JSON.stringify({ text: chunk }),
- }),
- signal: controller.signal,
- },
- );
- if (!res.ok) {
- const body = await res.text().catch(() => "");
- throw new Error(
- `Feishu send failed (${res.status}): ${body.slice(0, 300)}`,
- );
- }
- const resData = (await res.json()) as { code?: number; msg?: string };
- if (resData.code !== 0) {
- throw new Error(
- `Feishu send API error (code=${resData.code}): ${resData.msg || "unknown"}`,
- );
- }
- } finally {
- clearTimeout(timer);
- }
- }
-}
diff --git a/src/channels/flock.ts b/src/channels/flock.ts
deleted file mode 100644
index e52c68a..0000000
--- a/src/channels/flock.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-flock",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-const API_URL = "https://api.flock.com/v2/chat.sendMessage";
-
-registerFunction(
- { id: "channel::flock::webhook", description: "Handle Flock webhook" },
- async (req) => {
- const body = req.body || req;
- const { name, text, to, userId } = body;
-
- if (!text) return { status_code: 200, body: { ok: true } };
-
- const channelId = to;
- const agentId = await resolveAgent(sdk, "flock", channelId);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `flock:${channelId}`,
- },
- });
-
- await sendMessage(channelId, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "flock", channelId, userId },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::flock::webhook",
- config: { api_path: "webhook/flock", http_method: "POST" },
-});
-
-async function sendMessage(to: string, text: string) {
- const token = await getSecret("FLOCK_TOKEN");
- if (!token) {
- throw new Error("FLOCK_TOKEN not configured");
- }
- const chunks = splitMessage(text, 4096);
- for (const chunk of chunks) {
- await fetch(API_URL, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- token,
- to,
- text: chunk,
- }),
- });
- }
-}
diff --git a/src/channels/gitter.ts b/src/channels/gitter.ts
deleted file mode 100644
index 59d33b4..0000000
--- a/src/channels/gitter.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-gitter",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-const API_URL = "https://api.gitter.im/v1";
-
-registerFunction(
- { id: "channel::gitter::webhook", description: "Handle Gitter webhook" },
- async (req) => {
- const body = req.body || req;
- const { model } = body;
-
- if (!model?.text) return { status_code: 200, body: { ok: true } };
-
- const roomId = model.roomId || body.roomId;
- const text = model.text;
- const userId = model.fromUser?.id;
-
- const agentId = await resolveAgent(sdk, "gitter", roomId);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `gitter:${roomId}`,
- },
- });
-
- await sendMessage(roomId, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "gitter", roomId, userId },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::gitter::webhook",
- config: { api_path: "webhook/gitter", http_method: "POST" },
-});
-
-async function sendMessage(roomId: string, text: string) {
- const token = await getSecret("GITTER_TOKEN");
- if (!token) {
- throw new Error("GITTER_TOKEN not configured");
- }
- const chunks = splitMessage(text, 4096);
- for (const chunk of chunks) {
- await fetch(`${API_URL}/rooms/${roomId}/chatMessages`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ text: chunk }),
- });
- }
-}
diff --git a/src/channels/google-chat.ts b/src/channels/google-chat.ts
deleted file mode 100644
index f924c60..0000000
--- a/src/channels/google-chat.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-google-chat",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-registerFunction(
- {
- id: "channel::google-chat::webhook",
- description: "Handle Google Chat webhook",
- },
- async (req) => {
- const event = req.body || req;
- const headers = req.headers || {};
-
- const expectedToken = await getSecret("GOOGLE_CHAT_TOKEN");
- if (expectedToken) {
- const rawHeader =
- headers["authorization"] || headers["Authorization"] || "";
- const authHeader = Array.isArray(rawHeader)
- ? rawHeader[0] || ""
- : String(rawHeader);
- const bearer = authHeader.replace(/^Bearer\s+/i, "");
- if (!bearer || bearer !== expectedToken) {
- return { status_code: 401, body: { error: "Unauthorized" } };
- }
- }
-
- if (event.type !== "MESSAGE")
- return { status_code: 200, body: { ok: true } };
-
- const spaceId = event.space?.name;
- const text = event.message?.text || "";
- const userId = event.user?.name;
-
- const agentId = await resolveAgent(sdk, "google-chat", spaceId);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `google-chat:${spaceId}`,
- },
- });
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "google-chat", spaceId, userId },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { text: response.content } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::google-chat::webhook",
- config: { api_path: "webhook/google-chat", http_method: "POST" },
-});
diff --git a/src/channels/gotify.ts b/src/channels/gotify.ts
deleted file mode 100644
index 173de5a..0000000
--- a/src/channels/gotify.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-gotify",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-registerFunction(
- { id: "channel::gotify::webhook", description: "Handle Gotify push webhook" },
- async (req) => {
- const body = req.body || req;
- const { appid, message, title, extras } = body;
-
- if (!message) return { status_code: 200, body: { ok: true } };
-
- const channelKey = String(appid);
- const agentId = await resolveAgent(sdk, "gotify", channelKey);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: title ? `${title}: ${message}` : message,
- sessionId: `gotify:${channelKey}`,
- },
- });
-
- await sendMessage(response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "gotify", appid },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::gotify::webhook",
- config: { api_path: "webhook/gotify", http_method: "POST" },
-});
-
-async function sendMessage(text: string) {
- const baseUrl = await getSecret("GOTIFY_URL");
- if (!baseUrl) {
- throw new Error("GOTIFY_URL not configured");
- }
- const token = await getSecret("GOTIFY_TOKEN");
- if (!token) {
- throw new Error("GOTIFY_TOKEN not configured");
- }
- const normalizedUrl = baseUrl.replace(/\/+$/, "");
- const chunks = splitMessage(text, 4096);
- for (const chunk of chunks) {
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 10_000);
- try {
- const res = await fetch(`${normalizedUrl}/message`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "X-Gotify-Key": token,
- },
- body: JSON.stringify({
- title: "AgentOS",
- message: chunk,
- priority: 5,
- }),
- signal: controller.signal,
- });
- if (!res.ok) {
- const body = await res.text().catch(() => "");
- throw new Error(
- `Gotify send failed (${res.status}): ${body.slice(0, 300)}`,
- );
- }
- } finally {
- clearTimeout(timer);
- }
- }
-}
diff --git a/src/channels/guilded.ts b/src/channels/guilded.ts
deleted file mode 100644
index 09ca2e5..0000000
--- a/src/channels/guilded.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-guilded",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-const API_URL = "https://www.guilded.gg/api/v1";
-
-registerFunction(
- { id: "channel::guilded::webhook", description: "Handle Guilded webhook" },
- async (req) => {
- const body = req.body || req;
-
- if (body.type !== "ChatMessageCreated")
- return { status_code: 200, body: { ok: true } };
-
- const msg = body.message || body.d?.message;
- if (!msg?.content) return { status_code: 200, body: { ok: true } };
-
- const channelId = msg.channelId;
- const text = msg.content;
- const authorId = msg.createdBy;
-
- const agentId = await resolveAgent(sdk, "guilded", channelId);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `guilded:${channelId}`,
- },
- });
-
- await sendMessage(channelId, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "guilded", channelId, authorId },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::guilded::webhook",
- config: { api_path: "webhook/guilded", http_method: "POST" },
-});
-
-async function sendMessage(channelId: string, text: string) {
- const token = await getSecret("GUILDED_TOKEN");
- const chunks = splitMessage(text, 4000);
- for (const chunk of chunks) {
- await fetch(`${API_URL}/channels/${channelId}/messages`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ content: chunk }),
- });
- }
-}
diff --git a/src/channels/irc.ts b/src/channels/irc.ts
deleted file mode 100644
index b4cc448..0000000
--- a/src/channels/irc.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-irc",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-registerFunction(
- {
- id: "channel::irc::webhook",
- description: "Handle IRC message via HTTP bridge",
- },
- async (req) => {
- const body = req.body || req;
- const { channel, nick, message } = body;
-
- const ircNick = await getSecret("IRC_NICK");
- if (!message || nick === ircNick)
- return { status_code: 200, body: { ok: true } };
-
- const agentId = await resolveAgent(sdk, "irc", channel);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message,
- sessionId: `irc:${channel}`,
- },
- });
-
- await sendMessage(channel, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "irc", ircChannel: channel, nick },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::irc::webhook",
- config: { api_path: "webhook/irc", http_method: "POST" },
-});
-
-async function sendMessage(channel: string, text: string) {
- const bridgeUrl =
- (await getSecret("IRC_BRIDGE_URL")) || "http://localhost:5050";
- const chunks = splitMessage(text, 500);
- for (const chunk of chunks) {
- await fetch(`${bridgeUrl}/send`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ channel, message: chunk }),
- });
- }
-}
diff --git a/src/channels/keybase.ts b/src/channels/keybase.ts
deleted file mode 100644
index d7d691f..0000000
--- a/src/channels/keybase.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-keybase",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-registerFunction(
- {
- id: "channel::keybase::webhook",
- description: "Handle Keybase bot webhook",
- },
- async (req) => {
- const body = req.body || req;
- const { sender, channel, text, conversation_id } = body;
-
- const username = await getSecret("KEYBASE_USERNAME");
- if (!username) {
- return {
- status_code: 500,
- body: { error: "KEYBASE_USERNAME not configured" },
- };
- }
- if (!text || sender === username)
- return { status_code: 200, body: { ok: true } };
-
- const channelKey = conversation_id || channel;
- const agentId = await resolveAgent(sdk, "keybase", channelKey);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `keybase:${channelKey}`,
- },
- });
-
- await sendMessage(channelKey, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "keybase", sender, conversationId: channelKey },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::keybase::webhook",
- config: { api_path: "webhook/keybase", http_method: "POST" },
-});
-
-async function sendMessage(conversationId: string, text: string) {
- const bridgeUrl =
- (await getSecret("KEYBASE_BRIDGE_URL")) || "http://localhost:8822";
- const chunks = splitMessage(text, 4096);
- for (const chunk of chunks) {
- await fetch(`${bridgeUrl}/send`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- conversation_id: conversationId,
- message: chunk,
- }),
- });
- }
-}
diff --git a/src/channels/line.ts b/src/channels/line.ts
deleted file mode 100644
index d0ba68c..0000000
--- a/src/channels/line.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-line",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-const API_URL = "https://api.line.me/v2/bot/message";
-
-registerFunction(
- {
- id: "channel::line::webhook",
- description: "Handle LINE Messaging API webhook",
- },
- async (req) => {
- const body = req.body || req;
- const events = body.events || [];
-
- for (const event of events) {
- if (event.type !== "message" || event.message?.type !== "text") continue;
-
- const userId = event.source?.userId || event.source?.groupId;
- const text = event.message.text;
- const replyToken = event.replyToken;
-
- const agentId = await resolveAgent(sdk, "line", userId);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `line:${userId}`,
- },
- });
-
- await sendMessage(replyToken, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "line", userId },
- },
- action: TriggerAction.Void(),
- });
- }
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::line::webhook",
- config: { api_path: "webhook/line", http_method: "POST" },
-});
-
-async function sendMessage(replyToken: string, text: string) {
- const token = await getSecret("LINE_CHANNEL_TOKEN");
- if (!token) {
- throw new Error("LINE_CHANNEL_TOKEN not configured");
- }
- const chunks = splitMessage(text, 5000);
- const messages = chunks
- .slice(0, 5)
- .map((chunk) => ({ type: "text", text: chunk }));
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 10_000);
- try {
- const res = await fetch(`${API_URL}/reply`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ replyToken, messages }),
- signal: controller.signal,
- });
- if (!res.ok) {
- const body = await res.text().catch(() => "");
- throw new Error(
- `LINE send failed (${res.status}): ${body.slice(0, 300)}`,
- );
- }
- } finally {
- clearTimeout(timer);
- }
-}
diff --git a/src/channels/mattermost.ts b/src/channels/mattermost.ts
deleted file mode 100644
index ce3b0fa..0000000
--- a/src/channels/mattermost.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-mattermost",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-registerFunction(
- {
- id: "channel::mattermost::webhook",
- description: "Handle Mattermost outgoing webhook",
- },
- async (req) => {
- const body = req.body || req;
- const { channel_id, user_id, text, post_id } = body;
-
- if (!text) return { status_code: 200, body: { ok: true } };
-
- const agentId = await resolveAgent(sdk, "mattermost", channel_id);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `mattermost:${channel_id}`,
- },
- });
-
- await sendMessage(channel_id, response.content, post_id);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "mattermost", channelId: channel_id, userId: user_id },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::mattermost::webhook",
- config: { api_path: "webhook/mattermost", http_method: "POST" },
-});
-
-async function sendMessage(channelId: string, text: string, rootId?: string) {
- const token = await getSecret("MATTERMOST_TOKEN");
- if (!token) {
- throw new Error("MATTERMOST_TOKEN not configured");
- }
- const baseUrl = await getSecret("MATTERMOST_URL");
- if (!baseUrl) {
- throw new Error("MATTERMOST_URL not configured");
- }
- const chunks = splitMessage(text, 4000);
- for (const chunk of chunks) {
- await fetch(`${baseUrl}/api/v4/posts`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- channel_id: channelId,
- message: chunk,
- ...(rootId ? { root_id: rootId } : {}),
- }),
- });
- }
-}
diff --git a/src/channels/messenger.ts b/src/channels/messenger.ts
deleted file mode 100644
index 38ed9ee..0000000
--- a/src/channels/messenger.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { timingSafeEqual } from "crypto";
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-function safeCompare(a: string, b: string): boolean {
- if (typeof a !== "string" || typeof b !== "string") return false;
- const bufA = Buffer.from(a);
- const bufB = Buffer.from(b);
- if (bufA.length !== bufB.length) return false;
- return timingSafeEqual(bufA, bufB);
-}
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-messenger",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-const API_URL = "https://graph.facebook.com/v18.0/me/messages";
-
-registerFunction(
- {
- id: "channel::messenger::webhook",
- description: "Handle Facebook Messenger webhook",
- },
- async (req) => {
- const body = req.body || req;
-
- const verifyToken = await getSecret("MESSENGER_VERIFY_TOKEN");
- if (!verifyToken) {
- return {
- status_code: 500,
- body: { error: "Verify token not configured" },
- };
- }
- if (body["hub.mode"] === "subscribe") {
- if (safeCompare(body["hub.verify_token"], verifyToken)) {
- return { status_code: 200, body: body["hub.challenge"] };
- }
- return { status_code: 403, body: "Forbidden" };
- }
-
- const entry = body.entry?.[0];
- const messaging = entry?.messaging?.[0];
-
- if (!messaging?.message?.text)
- return { status_code: 200, body: { ok: true } };
-
- const senderId = messaging.sender.id;
- const text = messaging.message.text;
-
- const agentId = await resolveAgent(sdk, "messenger", senderId);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `messenger:${senderId}`,
- },
- });
-
- await sendMessage(senderId, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "messenger", senderId },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::messenger::webhook",
- config: { api_path: "webhook/messenger", http_method: "POST" },
-});
-
-async function sendMessage(recipientId: string, text: string) {
- const pageToken = await getSecret("MESSENGER_PAGE_TOKEN");
- if (!pageToken) {
- throw new Error("MESSENGER_PAGE_TOKEN not configured");
- }
- const chunks = splitMessage(text, 2000);
- for (const chunk of chunks) {
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 15_000);
- try {
- const res = await fetch(API_URL, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${pageToken}`,
- },
- body: JSON.stringify({
- recipient: { id: recipientId },
- message: { text: chunk },
- }),
- signal: controller.signal,
- });
- if (!res.ok) {
- const body = await res.text().catch(() => "");
- throw new Error(
- `Messenger send failed (${res.status}): ${body.slice(0, 300)}`,
- );
- }
- } finally {
- clearTimeout(timer);
- }
- }
-}
diff --git a/src/channels/mumble.ts b/src/channels/mumble.ts
deleted file mode 100644
index 3717da1..0000000
--- a/src/channels/mumble.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-mumble",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-registerFunction(
- {
- id: "channel::mumble::webhook",
- description: "Handle Mumble text channel via HTTP bridge",
- },
- async (req) => {
- const body = req.body || req;
- const { channel, user, message } = body;
-
- if (!message) return { status_code: 200, body: { ok: true } };
-
- const channelKey = channel || "root";
- const agentId = await resolveAgent(sdk, "mumble", channelKey);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message,
- sessionId: `mumble:${channelKey}`,
- },
- });
-
- await sendMessage(channelKey, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "mumble", mumbleChannel: channelKey, user },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::mumble::webhook",
- config: { api_path: "webhook/mumble", http_method: "POST" },
-});
-
-async function sendMessage(channel: string, text: string) {
- const bridgeUrl = await getSecret("MUMBLE_BRIDGE_URL");
- if (!bridgeUrl) {
- throw new Error("MUMBLE_BRIDGE_URL not configured");
- }
- const chunks = splitMessage(text, 4096);
- for (const chunk of chunks) {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 10_000);
- try {
- const res = await fetch(`${bridgeUrl}/send`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ channel, message: chunk }),
- signal: controller.signal,
- });
- if (!res.ok) {
- const body = await res.text().catch(() => "");
- throw new Error(
- `Mumble send failed (${res.status}): ${body.slice(0, 300)}`,
- );
- }
- } finally {
- clearTimeout(timeoutId);
- }
- }
-}
diff --git a/src/channels/nextcloud.ts b/src/channels/nextcloud.ts
deleted file mode 100644
index 37ea99b..0000000
--- a/src/channels/nextcloud.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-nextcloud",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-registerFunction(
- {
- id: "channel::nextcloud::webhook",
- description: "Handle Nextcloud Talk webhook",
- },
- async (req) => {
- const body = req.body || req;
- const { token: roomToken, actorId, message, messageId } = body;
-
- if (!message) return { status_code: 200, body: { ok: true } };
-
- const agentId = await resolveAgent(sdk, "nextcloud", roomToken);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message,
- sessionId: `nextcloud:${roomToken}`,
- },
- });
-
- await sendMessage(roomToken, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "nextcloud", roomToken, actorId },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::nextcloud::webhook",
- config: { api_path: "webhook/nextcloud", http_method: "POST" },
-});
-
-async function sendMessage(roomToken: string, text: string) {
- const token = (await getSecret("NEXTCLOUD_TOKEN")).trim();
- if (!token) {
- throw new Error("NEXTCLOUD_TOKEN not configured");
- }
- const baseUrl = (await getSecret("NEXTCLOUD_URL")).trim();
- if (!baseUrl) {
- throw new Error("NEXTCLOUD_URL not configured");
- }
- const normalizedUrl = baseUrl.replace(/\/+$/, "");
- const chunks = splitMessage(text, 4096);
- for (const chunk of chunks) {
- const res = await fetch(
- `${normalizedUrl}/ocs/v2.php/apps/spreed/api/v1/chat/${encodeURIComponent(roomToken)}`,
- {
- method: "POST",
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- "OCS-APIRequest": "true",
- },
- body: JSON.stringify({ message: chunk }),
- },
- );
- if (!res.ok) {
- const body = await res.text().catch(() => "");
- throw new Error(
- `Nextcloud send failed (${res.status}): ${body.slice(0, 300)}`,
- );
- }
- }
-}
diff --git a/src/channels/nostr.ts b/src/channels/nostr.ts
deleted file mode 100644
index 31522c7..0000000
--- a/src/channels/nostr.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-nostr",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-registerFunction(
- {
- id: "channel::nostr::webhook",
- description: "Handle Nostr relay event via HTTP bridge",
- },
- async (req) => {
- const body = req.body || req;
- const { pubkey, content, id, kind } = body;
-
- if (!content || kind !== 1) return { status_code: 200, body: { ok: true } };
-
- const agentId = await resolveAgent(sdk, "nostr", pubkey);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: content,
- sessionId: `nostr:${pubkey}`,
- },
- });
-
- await sendMessage(response.content, id);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "nostr", pubkey },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::nostr::webhook",
- config: { api_path: "webhook/nostr", http_method: "POST" },
-});
-
-async function sendMessage(content: string, replyToId?: string) {
- const privateKey = await getSecret("NOSTR_PRIVATE_KEY");
- if (!privateKey) {
- throw new Error("NOSTR_PRIVATE_KEY not configured");
- }
- const relay = (await getSecret("NOSTR_RELAY")) || "wss://relay.damus.io";
- const bridgeUrl =
- (await getSecret("NOSTR_BRIDGE_URL")) || "http://localhost:7777";
- const chunks = splitMessage(content, 4096);
- for (const chunk of chunks) {
- const res = await fetch(`${bridgeUrl}/publish`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- content: chunk,
- kind: 1,
- relay,
- private_key: privateKey,
- ...(replyToId ? { tags: [["e", replyToId]] } : {}),
- }),
- });
- if (!res.ok) {
- throw new Error(`Nostr bridge publish failed: ${res.status}`);
- }
- }
-}
diff --git a/src/channels/ntfy.ts b/src/channels/ntfy.ts
deleted file mode 100644
index 8b3f899..0000000
--- a/src/channels/ntfy.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-ntfy",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-registerFunction(
- { id: "channel::ntfy::webhook", description: "Handle ntfy.sh push webhook" },
- async (req) => {
- const body = req.body || req;
- const { topic, message, title, id: msgId } = body;
-
- if (!message) return { status_code: 200, body: { ok: true } };
-
- const agentId = await resolveAgent(sdk, "ntfy", topic);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: title ? `${title}: ${message}` : message,
- sessionId: `ntfy:${topic}`,
- },
- });
-
- await sendMessage(response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "ntfy", topic, msgId },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::ntfy::webhook",
- config: { api_path: "webhook/ntfy", http_method: "POST" },
-});
-
-async function sendMessage(text: string) {
- const token = await getSecret("NTFY_TOKEN");
- const ntfyTopic = await getSecret("NTFY_TOPIC");
- if (!ntfyTopic) {
- throw new Error("NTFY_TOPIC not configured");
- }
- const baseUrl = (await getSecret("NTFY_URL")) || "https://ntfy.sh";
- const chunks = splitMessage(text, 4096);
- for (const chunk of chunks) {
- await fetch(`${baseUrl}/${encodeURIComponent(ntfyTopic)}`, {
- method: "POST",
- headers: {
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
- Title: "AgentOS",
- },
- body: chunk,
- });
- }
-}
diff --git a/src/channels/pumble.ts b/src/channels/pumble.ts
deleted file mode 100644
index 7cf4151..0000000
--- a/src/channels/pumble.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-pumble",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-const API_URL = "https://api.pumble.com/v1";
-
-registerFunction(
- { id: "channel::pumble::webhook", description: "Handle Pumble webhook" },
- async (req) => {
- const body = req.body || req;
-
- if (body.type === "url_verification") {
- return { status_code: 200, body: { challenge: body.challenge } };
- }
-
- const event = body.event;
- if (!event?.text || event.bot_id)
- return { status_code: 200, body: { ok: true } };
-
- const channelId = event.channel;
- const text = event.text;
- const userId = event.user;
-
- const agentId = await resolveAgent(sdk, "pumble", channelId);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `pumble:${channelId}`,
- },
- });
-
- await sendMessage(channelId, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "pumble", channelId, userId },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::pumble::webhook",
- config: { api_path: "webhook/pumble", http_method: "POST" },
-});
-
-async function sendMessage(channelId: string, text: string) {
- const token = await getSecret("PUMBLE_TOKEN");
- if (!token) {
- throw new Error("PUMBLE_TOKEN not configured");
- }
- const chunks = splitMessage(text, 4000);
- for (const chunk of chunks) {
- const res = await fetch(`${API_URL}/messages`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ channel: channelId, text: chunk }),
- });
- if (!res.ok) {
- const body = await res.text().catch(() => "");
- throw new Error(
- `Pumble send failed (${res.status}): ${body.slice(0, 300)}`,
- );
- }
- }
-}
diff --git a/src/channels/revolt.ts b/src/channels/revolt.ts
deleted file mode 100644
index 08acc71..0000000
--- a/src/channels/revolt.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-revolt",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-const API_URL = "https://api.revolt.chat";
-
-registerFunction(
- { id: "channel::revolt::webhook", description: "Handle Revolt chat webhook" },
- async (req) => {
- const body = req.body || req;
-
- if (body.type !== "Message")
- return { status_code: 200, body: { ok: true } };
-
- const channelId = body.channel;
- const text = body.content || "";
- const authorId = body.author;
-
- if (!text) return { status_code: 200, body: { ok: true } };
-
- const agentId = await resolveAgent(sdk, "revolt", channelId);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `revolt:${channelId}`,
- },
- });
-
- await sendMessage(channelId, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "revolt", channelId, authorId },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::revolt::webhook",
- config: { api_path: "webhook/revolt", http_method: "POST" },
-});
-
-async function sendMessage(channelId: string, text: string) {
- const token = (await getSecret("REVOLT_TOKEN")).trim();
- if (!token) {
- throw new Error("REVOLT_TOKEN not configured");
- }
- const chunks = splitMessage(text, 2000);
- for (const chunk of chunks) {
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 10_000);
- try {
- const res = await fetch(
- `${API_URL}/channels/${encodeURIComponent(channelId)}/messages`,
- {
- method: "POST",
- headers: {
- "x-bot-token": token,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ content: chunk }),
- signal: controller.signal,
- },
- );
- if (!res.ok) {
- const body = await res.text().catch(() => "");
- throw new Error(
- `Revolt send failed (${res.status}): ${body.slice(0, 300)}`,
- );
- }
- } finally {
- clearTimeout(timer);
- }
- }
-}
diff --git a/src/channels/rocketchat.ts b/src/channels/rocketchat.ts
deleted file mode 100644
index 540dbf6..0000000
--- a/src/channels/rocketchat.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-rocketchat",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-registerFunction(
- {
- id: "channel::rocketchat::webhook",
- description: "Handle Rocket.Chat outgoing webhook",
- },
- async (req) => {
- const body = req.body || req;
- const { channel_id, user_id, user_name, text, message_id, tmid } = body;
-
- if (!text) return { status_code: 200, body: { ok: true } };
-
- const agentId = await resolveAgent(sdk, "rocketchat", channel_id);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `rocketchat:${channel_id}`,
- },
- });
-
- await sendMessage(channel_id, response.content, tmid);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: {
- channel: "rocketchat",
- channelId: channel_id,
- userName: user_name,
- },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::rocketchat::webhook",
- config: { api_path: "webhook/rocketchat", http_method: "POST" },
-});
-
-async function sendMessage(roomId: string, text: string, tmid?: string) {
- const token = await getSecret("ROCKETCHAT_TOKEN");
- if (!token) {
- throw new Error("ROCKETCHAT_TOKEN not configured");
- }
- const baseUrl = await getSecret("ROCKETCHAT_URL");
- if (!baseUrl) {
- throw new Error("ROCKETCHAT_URL not configured");
- }
- const userId = await getSecret("ROCKETCHAT_USER_ID");
- if (!userId) {
- throw new Error("ROCKETCHAT_USER_ID not configured");
- }
- const normalizedUrl = baseUrl.replace(/\/+$/, "");
- const chunks = splitMessage(text, 4000);
- for (const chunk of chunks) {
- const res = await fetch(`${normalizedUrl}/api/v1/chat.sendMessage`, {
- method: "POST",
- headers: {
- "X-Auth-Token": token,
- "X-User-Id": userId,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- message: {
- rid: roomId,
- msg: chunk,
- ...(tmid ? { tmid } : {}),
- },
- }),
- });
- if (!res.ok) {
- const body = await res.text().catch(() => "");
- throw new Error(
- `Rocket.Chat send failed (${res.status}): ${body.slice(0, 300)}`,
- );
- }
- }
-}
diff --git a/src/channels/threema.ts b/src/channels/threema.ts
deleted file mode 100644
index af3c292..0000000
--- a/src/channels/threema.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-threema",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-const API_URL = "https://msgapi.threema.ch";
-
-registerFunction(
- {
- id: "channel::threema::webhook",
- description: "Handle Threema Gateway webhook",
- },
- async (req) => {
- const body = req.body || req;
- const { from, text, messageId } = body;
-
- if (!text || !from) return { status_code: 200, body: { ok: true } };
-
- const agentId = await resolveAgent(sdk, "threema", from);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `threema:${from}`,
- },
- });
-
- await sendMessage(from, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "threema", from, messageId },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::threema::webhook",
- config: { api_path: "webhook/threema", http_method: "POST" },
-});
-
-async function sendMessage(to: string, text: string) {
- const threemaId = await getSecret("THREEMA_ID");
- if (!threemaId) {
- throw new Error("THREEMA_ID not configured");
- }
- const secret = await getSecret("THREEMA_SECRET");
- if (!secret) {
- throw new Error("THREEMA_SECRET not configured");
- }
- const chunks = splitMessage(text, 3500);
- for (const chunk of chunks) {
- const params = new URLSearchParams({
- from: threemaId,
- to,
- text: chunk,
- secret,
- });
- await fetch(`${API_URL}/send_simple`, {
- method: "POST",
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
- body: params.toString(),
- });
- }
-}
diff --git a/src/channels/twist.ts b/src/channels/twist.ts
deleted file mode 100644
index eb39497..0000000
--- a/src/channels/twist.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-twist",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-const API_URL = "https://api.twist.com/api/v3";
-
-registerFunction(
- {
- id: "channel::twist::webhook",
- description: "Handle Twist integration webhook",
- },
- async (req) => {
- const body = req.body || req;
- const { channel_id, content, creator, thread_id, comment_id } = body;
-
- if (!content) return { status_code: 200, body: { ok: true } };
-
- const channelKey = thread_id
- ? `thread:${thread_id}`
- : `channel:${channel_id}`;
- const agentId = await resolveAgent(sdk, "twist", channelKey);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: content,
- sessionId: `twist:${channelKey}`,
- },
- });
-
- await sendMessage(thread_id || channel_id, response.content, !!thread_id);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "twist", channelKey, creator },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::twist::webhook",
- config: { api_path: "webhook/twist", http_method: "POST" },
-});
-
-async function sendMessage(id: number, text: string, isThread: boolean) {
- const token = await getSecret("TWIST_TOKEN");
- if (!token) {
- throw new Error("TWIST_TOKEN not configured");
- }
- const endpoint = isThread ? "comments/add" : "thread_messages/add";
- const payload = isThread
- ? { thread_id: id, content: text }
- : { channel_id: id, content: text };
-
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 10_000);
- try {
- const res = await fetch(`${API_URL}/${endpoint}`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify(payload),
- signal: controller.signal,
- });
- if (!res.ok) {
- const body = await res.text().catch(() => "");
- throw new Error(
- `Twist send failed (${res.status}): ${body.slice(0, 300)}`,
- );
- }
- } finally {
- clearTimeout(timer);
- }
-}
diff --git a/src/channels/viber.ts b/src/channels/viber.ts
deleted file mode 100644
index 2a8c492..0000000
--- a/src/channels/viber.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-viber",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-const API_URL = "https://chatapi.viber.com/pa/send_message";
-
-registerFunction(
- {
- id: "channel::viber::webhook",
- description: "Handle Viber REST API webhook",
- },
- async (req) => {
- const body = req.body || req;
-
- if (body.event !== "message")
- return { status_code: 200, body: { ok: true } };
-
- const userId = body.sender?.id;
- const text = body.message?.text;
-
- if (!text || !userId) return { status_code: 200, body: { ok: true } };
-
- const agentId = await resolveAgent(sdk, "viber", userId);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `viber:${userId}`,
- },
- });
-
- await sendMessage(userId, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "viber", userId },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::viber::webhook",
- config: { api_path: "webhook/viber", http_method: "POST" },
-});
-
-async function sendMessage(receiverId: string, text: string) {
- const token = await getSecret("VIBER_TOKEN");
- if (!token) {
- throw new Error("VIBER_TOKEN not configured");
- }
- const chunks = splitMessage(text, 7000);
- for (const chunk of chunks) {
- const res = await fetch(API_URL, {
- method: "POST",
- headers: {
- "X-Viber-Auth-Token": token,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- receiver: receiverId,
- type: "text",
- text: chunk,
- }),
- });
- if (!res.ok) {
- const body = await res.text().catch(() => "");
- throw new Error(
- `Viber send failed (${res.status}): ${body.slice(0, 300)}`,
- );
- }
- }
-}
diff --git a/src/channels/webhook.ts b/src/channels/webhook.ts
deleted file mode 100644
index 69a023f..0000000
--- a/src/channels/webhook.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { registerWorker } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { splitMessage, resolveAgent, assertNoSsrf } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-webhook",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-registerFunction(
- {
- id: "channel::webhook::inbound",
- description: "Generic webhook handler for any platform",
- },
- async (req) => {
- const { body, headers, query_params, path_params } = req;
- const channelId =
- path_params?.channelId || query_params?.channel || "default";
-
- const agentId = await resolveAgent(sdk, "webhook", channelId);
-
- const message =
- body?.message ||
- body?.text ||
- body?.content ||
- "[Unrecognized webhook payload]";
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message,
- sessionId: `webhook:${channelId}`,
- },
- });
-
- const callbackUrl = body?.callback_url || body?.response_url;
- if (callbackUrl) {
- await assertNoSsrf(callbackUrl);
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 30_000);
- try {
- await fetch(callbackUrl, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ text: response.content }),
- signal: controller.signal,
- });
- } catch {
- } finally {
- clearTimeout(timer);
- }
- }
-
- return { status_code: 200, body: { response: response.content } };
- },
-);
-
-registerFunction(
- {
- id: "channel::webhook::configure",
- description: "Configure a webhook channel",
- },
- async ({
- channelId,
- agentId,
- callbackUrl,
- }: {
- channelId: string;
- agentId: string;
- callbackUrl?: string;
- }) => {
- await trigger({
- function_id: "state::set",
- payload: {
- scope: "channel_agents",
- key: `webhook:${channelId}`,
- value: { agentId, callbackUrl, configuredAt: Date.now() },
- },
- });
- return { configured: true, channelId };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::webhook::inbound",
- config: { api_path: "webhook/:channelId", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "channel::webhook::configure",
- config: { api_path: "api/channels/webhook", http_method: "POST" },
-});
diff --git a/src/channels/xmpp.ts b/src/channels/xmpp.ts
deleted file mode 100644
index 3050ca8..0000000
--- a/src/channels/xmpp.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-xmpp",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-registerFunction(
- {
- id: "channel::xmpp::webhook",
- description: "Handle XMPP message via HTTP bridge",
- },
- async (req) => {
- const body = req.body || req;
- const { from, to, body: text, type } = body;
- const jid = await getSecret("XMPP_JID");
- if (!jid) {
- return { status_code: 500, body: { error: "XMPP_JID not configured" } };
- }
-
- if (!text || from === jid) return { status_code: 200, body: { ok: true } };
-
- const channelKey = type === "groupchat" ? to : from;
- const agentId = await resolveAgent(sdk, "xmpp", channelKey);
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `xmpp:${channelKey}`,
- },
- });
-
- await sendMessage(channelKey, response.content, type);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "xmpp", from, chatType: type },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::xmpp::webhook",
- config: { api_path: "webhook/xmpp", http_method: "POST" },
-});
-
-async function sendMessage(to: string, text: string, type: string) {
- const bridgeUrl =
- (await getSecret("XMPP_BRIDGE_URL")) || "http://localhost:5280";
- const jid = await getSecret("XMPP_JID");
- if (!jid) {
- throw new Error("XMPP_JID secret not configured");
- }
- const chunks = splitMessage(text, 4096);
- for (const chunk of chunks) {
- try {
- const res = await fetch(`${bridgeUrl}/send`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- from: jid,
- to,
- body: chunk,
- type: type || "chat",
- }),
- });
- if (!res.ok) {
- console.error(`XMPP send failed: ${res.status}`);
- }
- } catch (err: any) {
- console.error(`XMPP send error: ${err.message}`);
- }
- }
-}
diff --git a/src/channels/zulip.ts b/src/channels/zulip.ts
deleted file mode 100644
index 5da65cc..0000000
--- a/src/channels/zulip.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { createSecretGetter } from "@agentos/shared/secrets";
-import { splitMessage, resolveAgent } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "channel-zulip",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const getSecret = createSecretGetter(trigger);
-
-registerFunction(
- { id: "channel::zulip::webhook", description: "Handle Zulip bot webhook" },
- async (req) => {
- const body = req.body || req;
- const { message } = body;
-
- if (!message?.content) return { status_code: 200, body: { ok: true } };
-
- const streamId = message.stream_id || message.sender_id;
- const text = message.content;
- const topic = message.subject || "";
-
- const agentId = await resolveAgent(sdk, "zulip", String(streamId));
-
- const response: any = await trigger({
- function_id: "agent::chat",
- payload: {
- agentId,
- message: text,
- sessionId: `zulip:${streamId}:${topic}`,
- },
- });
-
- await sendMessage(message.type, streamId, topic, response.content);
-
- trigger({
- function_id: "security::audit",
- payload: {
- type: "channel_message",
- agentId,
- detail: { channel: "zulip", streamId, senderId: message.sender_id },
- },
- action: TriggerAction.Void(),
- });
-
- return { status_code: 200, body: { ok: true } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "channel::zulip::webhook",
- config: { api_path: "webhook/zulip", http_method: "POST" },
-});
-
-async function sendMessage(
- type: string,
- to: number,
- topic: string,
- content: string,
-) {
- const email = await getSecret("ZULIP_EMAIL");
- if (!email) {
- throw new Error("ZULIP_EMAIL not configured");
- }
- const apiKey = await getSecret("ZULIP_API_KEY");
- if (!apiKey) {
- throw new Error("ZULIP_API_KEY not configured");
- }
- const site = await getSecret("ZULIP_SITE");
- if (!site) {
- throw new Error("ZULIP_SITE not configured");
- }
- const auth = Buffer.from(`${email}:${apiKey}`).toString("base64");
- const chunks = splitMessage(content, 10000);
- for (const chunk of chunks) {
- const params = new URLSearchParams({
- type: type === "private" ? "private" : "stream",
- to: String(to),
- topic,
- content: chunk,
- });
- await fetch(`${site}/api/v1/messages`, {
- method: "POST",
- headers: {
- Authorization: `Basic ${auth}`,
- "Content-Type": "application/x-www-form-urlencoded",
- },
- body: params.toString(),
- });
- }
-}
diff --git a/src/cost-tracker.ts b/src/cost-tracker.ts
deleted file mode 100644
index 6b12672..0000000
--- a/src/cost-tracker.ts
+++ /dev/null
@@ -1,246 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { PRICING } from "@agentos/shared/pricing";
-import type { CostRecord, CostSummary, BudgetStatus } from "./types.js";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "cost-tracker",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-const triggerVoid = (id: string, payload: unknown) =>
- trigger({ function_id: id, payload, action: TriggerAction.Void() });
-
-registerFunction(
- {
- id: "cost::track",
- description: "Calculate and store cost for an LLM call",
- metadata: { category: "cost" },
- },
- async (input: {
- agentId: string;
- sessionId: string;
- model: string;
- inputTokens: number;
- outputTokens: number;
- cacheReadTokens?: number;
- cacheWriteTokens?: number;
- }): Promise => {
- const pricing = PRICING[input.model];
- const inputPrice = pricing?.input ?? 0;
- const outputPrice = pricing?.output ?? 0;
- const cacheReadPrice = pricing?.cacheRead ?? inputPrice * 0.1;
- const cacheWritePrice = pricing?.cacheWrite ?? inputPrice * 1.25;
-
- const cacheRead = input.cacheReadTokens ?? 0;
- const cacheWrite = input.cacheWriteTokens ?? 0;
-
- const cost =
- (input.inputTokens * inputPrice +
- input.outputTokens * outputPrice +
- cacheRead * cacheReadPrice +
- cacheWrite * cacheWritePrice) /
- 1_000_000;
-
- const now = Date.now();
- const date = new Date(now).toISOString().slice(0, 10);
- const key = `${date}:${input.agentId}:${input.sessionId}:${now}`;
-
- const record: CostRecord = {
- agentId: input.agentId,
- sessionId: input.sessionId,
- model: input.model,
- provider: pricing ? "known" : "unknown",
- inputTokens: input.inputTokens,
- outputTokens: input.outputTokens,
- cacheReadTokens: cacheRead,
- cacheWriteTokens: cacheWrite,
- cost,
- timestamp: now,
- };
-
- await trigger({
- function_id: "state::set",
- payload: { scope: "cost_records", key, value: record },
- });
-
- triggerVoid("state::update", {
- scope: "cost_daily",
- key: `${date}:${input.agentId}`,
- operations: [
- { type: "increment", path: "cost", value: cost },
- { type: "increment", path: "inputTokens", value: input.inputTokens },
- { type: "increment", path: "outputTokens", value: input.outputTokens },
- { type: "increment", path: "calls", value: 1 },
- ],
- });
-
- triggerVoid("state::update", {
- scope: "cost_daily",
- key: date,
- operations: [
- { type: "increment", path: "totalCost", value: cost },
- { type: "increment", path: "totalCalls", value: 1 },
- ],
- });
-
- return record;
- },
-);
-
-registerFunction(
- {
- id: "cost::summary",
- description: "Aggregate cost data with grouping",
- metadata: { category: "cost" },
- },
- async (input: {
- agentId?: string;
- startDate?: string;
- endDate?: string;
- groupBy?: "day" | "agent" | "model";
- }): Promise => {
- const now = new Date();
- const start = input.startDate || now.toISOString().slice(0, 10);
- const end = input.endDate || now.toISOString().slice(0, 10);
- const groupBy = input.groupBy || "day";
-
- const records: CostRecord[] = [];
- const startDate = new Date(start);
- const endDate = new Date(end);
-
- for (
- let d = new Date(startDate);
- d <= endDate;
- d = new Date(d.getTime() + 86_400_000)
- ) {
- const dateStr = d.toISOString().slice(0, 10);
- const prefix = input.agentId ? `${dateStr}:${input.agentId}` : dateStr;
- const dayRecords: any = await trigger({
- function_id: "state::list",
- payload: { scope: "cost_records", prefix },
- }).catch(() => []);
-
- if (Array.isArray(dayRecords)) {
- for (const r of dayRecords) {
- if (r.value) records.push(r.value);
- else if (r.cost !== undefined) records.push(r);
- }
- }
- }
-
- const grouped = new Map();
- let total = 0;
-
- for (const r of records) {
- let key: string;
- if (groupBy === "day")
- key = new Date(r.timestamp).toISOString().slice(0, 10);
- else if (groupBy === "agent") key = r.agentId;
- else key = r.model;
-
- const entry = grouped.get(key) || { cost: 0, tokens: 0 };
- entry.cost += r.cost;
- entry.tokens += r.inputTokens + r.outputTokens;
- grouped.set(key, entry);
- total += r.cost;
- }
-
- const breakdown = [...grouped.entries()].map(([key, val]) => ({
- key,
- cost: Math.round(val.cost * 1_000_000) / 1_000_000,
- tokens: val.tokens,
- }));
-
- return {
- total: Math.round(total * 1_000_000) / 1_000_000,
- breakdown,
- period: { start, end },
- };
- },
-);
-
-registerFunction(
- {
- id: "cost::budget_check",
- description: "Check agent spend against budget limits",
- metadata: { category: "cost" },
- },
- async ({ agentId }: { agentId: string }): Promise => {
- const config: any = await trigger({
- function_id: "state::get",
- payload: { scope: "agents", key: agentId },
- }).catch(() => null);
-
- const dailyBudget = config?.resources?.dailyBudget ?? Infinity;
- const monthlyBudget = config?.resources?.monthlyBudget ?? Infinity;
-
- const today = new Date().toISOString().slice(0, 10);
- const dailyData: any = await trigger({
- function_id: "state::get",
- payload: { scope: "cost_daily", key: `${today}:${agentId}` },
- }).catch(() => null);
-
- const dailySpent = dailyData?.cost ?? 0;
-
- const now = new Date();
- const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
- let monthlySpent = 0;
-
- for (let d = new Date(monthStart); d <= now; d.setDate(d.getDate() + 1)) {
- const dateStr = d.toISOString().slice(0, 10);
- const dayData: any = await trigger({
- function_id: "state::get",
- payload: { scope: "cost_daily", key: `${dateStr}:${agentId}` },
- }).catch(() => null);
- monthlySpent += dayData?.cost ?? 0;
- }
-
- const dayOfMonth = now.getDate();
- const daysInMonth = new Date(
- now.getFullYear(),
- now.getMonth() + 1,
- 0,
- ).getDate();
- const projectedMonthly =
- dayOfMonth > 0 ? (monthlySpent / dayOfMonth) * daysInMonth : 0;
-
- const limit = Math.min(dailyBudget, monthlyBudget);
- const spent = dailyBudget <= monthlyBudget ? dailySpent : monthlySpent;
- const withinBudget =
- dailySpent <= dailyBudget && monthlySpent <= monthlyBudget;
-
- if (!withinBudget) {
- triggerVoid("security::audit", {
- type: "budget_exceeded",
- agentId,
- dailySpent,
- monthlySpent,
- dailyBudget,
- monthlyBudget,
- });
- }
-
- return {
- withinBudget,
- spent: Math.round(spent * 1_000_000) / 1_000_000,
- limit: Number.isFinite(limit) ? limit : -1,
- remaining: Number.isFinite(limit)
- ? Math.round(Math.max(0, limit - spent) * 1_000_000) / 1_000_000
- : -1,
- projectedMonthly: Math.round(projectedMonthly * 1_000_000) / 1_000_000,
- };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "cost::summary",
- config: { api_path: "api/costs/summary", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "cost::budget_check",
- config: { api_path: "api/costs/:agentId/budget", http_method: "GET" },
-});
diff --git a/src/dashboard.ts b/src/dashboard.ts
deleted file mode 100644
index e73b57b..0000000
--- a/src/dashboard.ts
+++ /dev/null
@@ -1,620 +0,0 @@
-import { registerWorker } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { requireAuth } from "@agentos/shared/utils";
-import { SECURITY_HEADERS } from "@agentos/shared/security-headers";
-import { safeCall } from "@agentos/shared/errors";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "dashboard",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-registerFunction(
- {
- id: "dashboard::page",
- description: "Serve the single-page Alpine.js dashboard",
- },
- async (_input: { path?: string }) => {
- return {
- html: buildDashboardHtml(),
- contentType: "text/html",
- headers: {
- ...SECURITY_HEADERS,
- "Content-Security-Policy":
- "default-src 'self'; script-src 'self' https://cdn.tailwindcss.com https://cdn.jsdelivr.net 'unsafe-inline'; object-src 'none'; frame-ancestors 'none'",
- },
- };
- },
-);
-
-registerFunction(
- { id: "dashboard::stats", description: "Aggregate dashboard statistics" },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const ctx = { functionId: "dashboard::stats" };
- const [
- health,
- agents,
- skills,
- usage,
- hands,
- workflows,
- sessions,
- approvals,
- ] = await Promise.all([
- safeCall(
- () =>
- trigger({
- function_id: "state::get",
- payload: { scope: "health", key: "status" },
- }),
- null,
- { ...ctx, operation: "get_health" },
- ),
- safeCall(
- () => trigger({ function_id: "agent::list", payload: {} }),
- { agents: [] },
- { ...ctx, operation: "list_agents" },
- ),
- safeCall(
- () => trigger({ function_id: "skill::list", payload: {} }),
- { skills: [] },
- { ...ctx, operation: "list_skills" },
- ),
- safeCall(
- () => trigger({ function_id: "llm::usage", payload: {} }),
- { stats: [] },
- { ...ctx, operation: "get_usage" },
- ),
- safeCall(
- () => trigger({ function_id: "hand::list", payload: {} }),
- { hands: [] },
- { ...ctx, operation: "list_hands" },
- ),
- safeCall(
- () => trigger({ function_id: "workflow::list", payload: {} }),
- { workflows: [] },
- { ...ctx, operation: "list_workflows" },
- ),
- safeCall(
- () =>
- trigger({
- function_id: "state::list",
- payload: { scope: "sessions" },
- }),
- { entries: [] },
- { ...ctx, operation: "list_sessions" },
- ),
- safeCall(
- () => trigger({ function_id: "approval::list", payload: {} }),
- { pending: [] },
- { ...ctx, operation: "list_approvals" },
- ),
- ]);
-
- const extract = (val: unknown, key: string) => (val as any)?.[key] || [];
- const agentList = extract(agents, "agents");
- const skillList = extract(skills, "skills");
- const handList = extract(hands, "hands");
- const workflowList = extract(workflows, "workflows");
- const sessionList = extract(sessions, "entries");
- const approvalList = extract(approvals, "pending");
- const usageStats = extract(usage, "stats");
-
- let totalRequests = 0;
- let totalInputTokens = 0;
- let totalOutputTokens = 0;
- let totalCost = 0;
- for (const s of usageStats) {
- totalRequests += s.requests || 0;
- totalInputTokens += s.input_tokens || 0;
- totalOutputTokens += s.output_tokens || 0;
- totalCost += s.cost || 0;
- }
-
- return {
- status: health || "healthy",
- agents: agentList.length,
- agentList,
- skills: skillList.length,
- skillList,
- hands: handList.length,
- handList,
- workflows: workflowList.length,
- workflowList,
- sessions: sessionList.length,
- sessionList: sessionList.slice(-50),
- approvals: approvalList.length,
- approvalList,
- requests: totalRequests,
- tokens: {
- input: totalInputTokens,
- output: totalOutputTokens,
- total: totalInputTokens + totalOutputTokens,
- },
- cost: totalCost,
- uptime: process.uptime(),
- };
- },
-);
-
-registerFunction(
- { id: "dashboard::events", description: "Get recent audit events" },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const input = req.body || req;
- const limit = input.limit ?? 100;
- const logs = await safeCall(
- () =>
- trigger({
- function_id: "state::list",
- payload: { scope: "audit" },
- }),
- { entries: [] },
- { operation: "list_audit_events", functionId: "dashboard::events" },
- );
- const entries = ((logs as any)?.entries || []).slice(-limit);
- return { events: entries };
- },
-);
-
-registerFunction(
- { id: "dashboard::logs", description: "Get system logs" },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const input = req.body || req;
- const limit = input.limit ?? 200;
- const level = input.level ?? "all";
- const logs = await safeCall(
- () =>
- trigger({
- function_id: "state::list",
- payload: { scope: "logs" },
- }),
- { entries: [] },
- { operation: "list_logs", functionId: "dashboard::logs" },
- );
- let entries = ((logs as any)?.entries || []).slice(-limit);
- if (level !== "all") {
- entries = entries.filter((e: any) => e.level === level);
- }
- return { logs: entries };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "dashboard::page",
- config: { api_path: "api/dashboard", http_method: "GET" },
-});
-
-registerTrigger({
- type: "http",
- function_id: "dashboard::stats",
- config: { api_path: "api/dashboard/stats", http_method: "GET" },
-});
-
-registerTrigger({
- type: "http",
- function_id: "dashboard::events",
- config: { api_path: "api/dashboard/events", http_method: "GET" },
-});
-
-registerTrigger({
- type: "http",
- function_id: "dashboard::logs",
- config: { api_path: "api/dashboard/logs", http_method: "GET" },
-});
-
-function buildDashboardHtml(): string {
- return `
-
-
-
-
-AgentOS Dashboard
-
-
-
-
-
-
-
-
-
-
AgentOS
-
Agent Operating System
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Overview
-
-
-
Recent Events
-
-
-
-
-
-
-
-
-
No events yet
-
-
-
-
-
-
-
Agents
-
-
-
-
-
- Name
- Model
- Status
- Tags
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Chat
-
-
-
-
-
Start a conversation with an agent
-
-
-
- Select agent
-
-
-
-
-
- Send
-
-
-
-
-
Channels
-
Message routing channels for agent communication.
-
Configure channels in config/channels/*.toml
-
-
-
-
-
-
-
-
-
Workflows
-
-
-
-
-
-
-
No workflows defined
-
-
-
-
-
-
Sessions
-
-
-
-
-
- ID
- Agent
- Started
- Status
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Approvals
-
-
-
-
-
-
-
No pending approvals
-
-
-
-
-
-
Logs
-
- All levels
- Errors
- Warnings
- Info
- Debug
-
-
-
-
-
-
-
-
-
-
-
No logs available
-
-
-
-
-
Settings
-
-
-
System
-
-
Status:
-
Uptime:
-
Total Requests:
-
Total Cost:
-
-
-
-
Token Usage
-
-
Input:
-
Output:
-
Total:
-
-
-
-
Integrations
-
Configure MCP integrations in integrations/*.toml files.
-
Refresh Data
-
-
-
-
-
-
-
-
-
-`;
-}
diff --git a/src/index.ts b/src/index.ts
deleted file mode 100644
index 26d40b8..0000000
--- a/src/index.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { shutdownManager } from "@agentos/shared/shutdown";
-
-shutdownManager.initShutdown();
-console.log("AgentOS booting (iii-sdk registerWorker API, OTel enabled)");
-
-import "./api.js";
-import "./artifact-dag.js";
-import "./lsp-tools.js";
-import "./approval.js";
-import "./approval-tiers.js";
-import "./context-cache.js";
-import "./context-manager.js";
-import "./context-monitor.js";
-import "./cost-tracker.js";
-import "./dashboard.js";
-import "./hand-runner.js";
-import "./knowledge-graph.js";
-import "./memory-reflection.js";
-import "./migration.js";
-import "./model-catalog.js";
-import "./recovery.js";
-import "./security-headers.js";
-import "./security-map.js";
-import "./security-zeroize.js";
-import "./skill-security.js";
-import "./skills.js";
-import "./task-decomposer.js";
-import "./tool-profiles.js";
-import "./tools.js";
-import "./tools-extended.js";
-
-console.log("AgentOS ready");
diff --git a/src/knowledge-graph.ts b/src/knowledge-graph.ts
deleted file mode 100644
index 5c7c8dc..0000000
--- a/src/knowledge-graph.ts
+++ /dev/null
@@ -1,429 +0,0 @@
-import { registerWorker } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "knowledge-graph",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-interface TemporalKGEntity {
- entity: string;
- type: string;
- properties: Record;
- relations: Array<{ target: string; type: string }>;
- validFrom: number;
- validUntil: number | null;
- createdAt: number;
- version: number;
- agentId: string;
-}
-
-interface TemporalAddInput {
- entity: string;
- type: string;
- properties: Record;
- relations: Array<{ target: string; type: string }>;
- validFrom?: number;
- validUntil?: number;
- agentId: string;
-}
-
-interface TemporalQueryInput {
- entity: string;
- timeRange?: { from: number; to: number };
- depth?: number;
- relationFilter?: string[];
-}
-
-interface TraverseInput {
- startEntity: string;
- direction: "outgoing" | "incoming" | "both";
- maxDepth: number;
- maxNodes?: number;
-}
-
-interface DiffResult {
- added: TemporalKGEntity[];
- removed: TemporalKGEntity[];
- modified: Array<{ before: TemporalKGEntity; after: TemporalKGEntity }>;
-}
-
-registerFunction(
- {
- id: "kg::add_temporal",
- description: "Add entity with temporal metadata to knowledge graph",
- metadata: { category: "knowledge-graph" },
- },
- async (input: TemporalAddInput) => {
- const {
- entity,
- type: entityType,
- properties,
- relations,
- validFrom,
- validUntil,
- agentId,
- } = input;
- if (!entity || !agentId) return { error: "entity and agentId required" };
-
- const scope = `kg_temporal:${agentId}`;
- const existing: any[] = await listVersions(scope, entity);
- const version = existing.length + 1;
-
- const entry: TemporalKGEntity = {
- entity,
- type: entityType,
- properties: properties || {},
- relations: relations || [],
- validFrom: validFrom || Date.now(),
- validUntil: validUntil || null,
- createdAt: Date.now(),
- version,
- agentId,
- };
-
- await trigger({
- function_id: "state::set",
- payload: { scope, key: `${entity}:${version}`, value: entry },
- });
-
- await trigger({
- function_id: "state::set",
- payload: {
- scope,
- key: `${entity}:latest`,
- value: { version, key: `${entity}:${version}` },
- },
- });
-
- for (const rel of entry.relations) {
- const targetLatest: any = await trigger({
- function_id: "state::get",
- payload: { scope, key: `${rel.target}:latest` },
- }).catch(() => null);
-
- if (targetLatest) {
- const targetEntry: any = await trigger({
- function_id: "state::get",
- payload: { scope, key: targetLatest.key },
- }).catch(() => null);
-
- if (
- targetEntry &&
- !targetEntry.relations?.some(
- (r: any) => r.target === entity && r.type === `inverse:${rel.type}`,
- )
- ) {
- const updatedRelations = [
- ...(targetEntry.relations || []),
- { target: entity, type: `inverse:${rel.type}` },
- ];
- await trigger({
- function_id: "state::update",
- payload: {
- scope,
- key: targetLatest.key,
- operations: [
- { type: "set", path: "relations", value: updatedRelations },
- ],
- },
- });
- }
- }
- }
-
- return { stored: true, entity, version };
- },
-);
-
-registerFunction(
- {
- id: "kg::query_temporal",
- description: "Query knowledge graph with time range filter",
- metadata: { category: "knowledge-graph" },
- },
- async (input: TemporalQueryInput) => {
- const { entity, timeRange, depth: rawDepth, relationFilter } = input;
- if (!entity) return { error: "entity required" };
-
- const depth = Math.max(1, Math.min(Number(rawDepth) || 3, 10));
- const now = Date.now();
- const visited = new Set();
- const results: TemporalKGEntity[] = [];
-
- async function bfs(startEntity: string, maxDepth: number) {
- const queue: Array<{ entityName: string; d: number }> = [
- { entityName: startEntity, d: 0 },
- ];
-
- while (queue.length > 0 && results.length < 500) {
- const item = queue.shift()!;
- if (item.d > maxDepth || visited.has(item.entityName)) continue;
- visited.add(item.entityName);
-
- const entries = await getAllEntries();
- const matching = entries.filter((e: TemporalKGEntity) => {
- if (e.entity !== item.entityName) return false;
- if (timeRange) {
- if (e.validFrom > timeRange.to) return false;
- if (e.validUntil && e.validUntil < timeRange.from) return false;
- } else {
- if (e.validUntil && e.validUntil < now) return false;
- }
- return true;
- });
-
- for (const m of matching) {
- results.push(m);
- if (item.d < maxDepth) {
- const filteredRels = relationFilter?.length
- ? m.relations.filter((r) => relationFilter.includes(r.type))
- : m.relations;
- for (const rel of filteredRels) {
- if (!visited.has(rel.target)) {
- queue.push({ entityName: rel.target, d: item.d + 1 });
- }
- }
- }
- }
- }
- }
-
- let cachedEntries: TemporalKGEntity[] | null = null;
- async function getAllEntries(): Promise {
- if (cachedEntries) return cachedEntries;
- cachedEntries = await listAllEntries(`kg_temporal:${input.entity}`);
- return cachedEntries;
- }
-
- await bfs(entity, depth);
- return results;
- },
-);
-
-registerFunction(
- {
- id: "kg::traverse",
- description: "General graph traversal with direction control",
- metadata: { category: "knowledge-graph" },
- },
- async (input: TraverseInput) => {
- const {
- startEntity,
- direction,
- maxDepth: rawMaxDepth,
- maxNodes: rawMaxNodes,
- } = input;
- if (!startEntity) return { error: "startEntity required" };
-
- const maxDepth = Math.max(1, Math.min(Number(rawMaxDepth) || 3, 10));
- const maxNodes = Math.max(1, Math.min(Number(rawMaxNodes) || 500, 500));
- const visited = new Set();
- const adjacency: Record = {};
- const paths: Record = {};
-
- const allEntries = await listAllEntries("kg_temporal:");
-
- const outgoing = new Map>();
- const incoming = new Map>();
- for (const entry of allEntries) {
- for (const rel of entry.relations || []) {
- if (!outgoing.has(entry.entity)) outgoing.set(entry.entity, new Set());
- outgoing.get(entry.entity)!.add(rel.target);
- if (!incoming.has(rel.target)) incoming.set(rel.target, new Set());
- incoming.get(rel.target)!.add(entry.entity);
- }
- }
-
- const queue: Array<{ entity: string; depth: number; path: string[] }> = [
- { entity: startEntity, depth: 0, path: [startEntity] },
- ];
-
- while (queue.length > 0 && visited.size < maxNodes) {
- const { entity, depth, path } = queue.shift()!;
- if (visited.has(entity) || depth > maxDepth) continue;
- visited.add(entity);
- paths[entity] = path;
-
- const neighbors = new Set();
- if (direction === "outgoing" || direction === "both") {
- for (const n of outgoing.get(entity) || []) neighbors.add(n);
- }
- if (direction === "incoming" || direction === "both") {
- for (const n of incoming.get(entity) || []) neighbors.add(n);
- }
-
- adjacency[entity] = [...neighbors];
- for (const n of neighbors) {
- if (!visited.has(n)) {
- queue.push({ entity: n, depth: depth + 1, path: [...path, n] });
- }
- }
- }
-
- return { adjacency, paths, nodeCount: visited.size };
- },
-);
-
-registerFunction(
- {
- id: "kg::timeline",
- description: "Get version history of an entity",
- metadata: { category: "knowledge-graph" },
- },
- async ({ entity, agentId }: { entity: string; agentId: string }) => {
- if (!entity || !agentId) return { error: "entity and agentId required" };
-
- const versions = await listVersions(`kg_temporal:${agentId}`, entity);
- return versions.sort((a, b) => a.createdAt - b.createdAt);
- },
-);
-
-registerFunction(
- {
- id: "kg::diff",
- description: "Compare KG entity state between two timestamps",
- metadata: { category: "knowledge-graph" },
- },
- async ({
- entity,
- agentId,
- timestamp1,
- timestamp2,
- }: {
- entity: string;
- agentId: string;
- timestamp1: number;
- timestamp2: number;
- }) => {
- if (!entity || !agentId) return { error: "entity and agentId required" };
-
- const versions = await listVersions(`kg_temporal:${agentId}`, entity);
-
- const atT1 = versions.filter((v) => v.createdAt <= timestamp1);
- const atT2 = versions.filter((v) => v.createdAt <= timestamp2);
-
- const t1Set = new Set(atT1.map((v) => `${v.entity}:${v.version}`));
- const t2Set = new Set(atT2.map((v) => `${v.entity}:${v.version}`));
-
- const added = atT2.filter((v) => !t1Set.has(`${v.entity}:${v.version}`));
- const removed = atT1.filter((v) => !t2Set.has(`${v.entity}:${v.version}`));
-
- const modified: DiffResult["modified"] = [];
- for (const v2 of atT2) {
- const v1 = atT1.find(
- (v) => v.entity === v2.entity && v.version === v2.version - 1,
- );
- if (
- v1 &&
- JSON.stringify(v1.properties) !== JSON.stringify(v2.properties)
- ) {
- modified.push({ before: v1, after: v2 });
- }
- }
-
- return { added, removed, modified } as DiffResult;
- },
-);
-
-registerFunction(
- {
- id: "kg::stats",
- description: "Get knowledge graph statistics",
- metadata: { category: "knowledge-graph" },
- },
- async ({ agentId }: { agentId: string }) => {
- if (!agentId) return { error: "agentId required" };
-
- const entries = await listAllEntries(`kg_temporal:${agentId}`);
-
- const entities = new Set(entries.map((e) => e.entity));
- let totalRelations = 0;
- const adjacency = new Map>();
-
- for (const entry of entries) {
- if (!adjacency.has(entry.entity)) adjacency.set(entry.entity, new Set());
- for (const rel of entry.relations || []) {
- adjacency.get(entry.entity)!.add(rel.target);
- totalRelations++;
- }
- }
-
- const visited = new Set();
- let connectedComponents = 0;
- for (const entity of entities) {
- if (visited.has(entity)) continue;
- connectedComponents++;
- const stack = [entity];
- while (stack.length > 0) {
- const node = stack.pop()!;
- if (visited.has(node)) continue;
- visited.add(node);
- for (const neighbor of adjacency.get(node) || []) {
- if (!visited.has(neighbor)) stack.push(neighbor);
- }
- }
- }
-
- const totalEntities = entities.size;
- const avgDegree = totalEntities > 0 ? totalRelations / totalEntities : 0;
-
- return { totalEntities, totalRelations, avgDegree, connectedComponents };
- },
-);
-
-async function listAllEntries(scope: string): Promise {
- const raw = (await trigger({
- function_id: "state::list",
- payload: { scope },
- }).catch(
- () => [],
- )) as any[];
- return (raw || [])
- .filter(
- (e: any) =>
- e.value?.entity && e.value?.version && !e.key?.endsWith(":latest"),
- )
- .map((e: any) => e.value as TemporalKGEntity);
-}
-
-async function listVersions(
- scope: string,
- entity: string,
-): Promise {
- const entries = await listAllEntries(scope);
- return entries.filter((e) => e.entity === entity);
-}
-
-registerTrigger({
- type: "http",
- function_id: "kg::add_temporal",
- config: { api_path: "api/kg/add", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "kg::query_temporal",
- config: { api_path: "api/kg/query", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "kg::traverse",
- config: { api_path: "api/kg/traverse", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "kg::timeline",
- config: { api_path: "api/kg/timeline", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "kg::diff",
- config: { api_path: "api/kg/diff", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "kg::stats",
- config: { api_path: "api/kg/stats", http_method: "POST" },
-});
diff --git a/src/memory-reflection.ts b/src/memory-reflection.ts
deleted file mode 100644
index cb7959e..0000000
--- a/src/memory-reflection.ts
+++ /dev/null
@@ -1,267 +0,0 @@
-import { registerWorker, TriggerAction, Logger } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { recordMetric } from "@agentos/shared/metrics";
-import { stripCodeFences, requireAuth , httpOk } from "@agentos/shared/utils";
-
-
-const log = new Logger();
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "memory-reflection",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-const triggerVoid = (id: string, payload: unknown) =>
- trigger({ function_id: id, payload, action: TriggerAction.Void() });
-
-const REFLECTION_INTERVAL = 5;
-const lastDiscovery = new Map();
-const DISCOVERY_COOLDOWN_MS = 5 * 60 * 1000;
-
-registerFunction(
- {
- id: "reflect::check_turn",
- description: "Increment turn counter, decide whether to trigger memory reflection",
- metadata: { category: "reflect" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const { agentId, sessionId, iterations = 0 } = req.body || req;
- const scope = `reflect:${agentId}`;
- const key = sessionId || "default";
-
- const updated: any = await trigger({
- function_id: "state::update",
- payload: {
- scope,
- key,
- operations: [{ type: "increment", path: "turnCount", value: 1 }],
- },
- });
-
- const turnCount = updated?.turnCount || 1;
- const shouldReflect = turnCount % REFLECTION_INTERVAL === 0;
- const shouldReviewSkills = iterations >= 5;
-
- if (shouldReflect) {
- triggerVoid("reflect::curate_memory", { agentId, sessionId });
- }
- if (shouldReviewSkills) {
- const lastTs = lastDiscovery.get(sessionId) || 0;
- if (Date.now() - lastTs >= DISCOVERY_COOLDOWN_MS) {
- lastDiscovery.set(sessionId, Date.now());
- triggerVoid("reflect::discover_skills", { agentId, sessionId, iterations });
- }
- }
-
- return httpOk(req, { shouldReflect, shouldReviewSkills, turnCount });
- },
-);
-
-registerFunction(
- {
- id: "reflect::curate_memory",
- description: "LLM reviews conversation, extracts durable facts for long-term memory",
- metadata: { category: "reflect" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const { agentId, sessionId } = req.body || req;
- const memories: any = await trigger({
- function_id: "memory::recall",
- payload: { agentId, query: "recent conversation context", limit: 30 },
- }).catch(() => []);
-
- if (!memories?.length) return httpOk(req, { saved: 0 });
-
- const recentContext = (memories || [])
- .slice(-20)
- .map((m: any) => `[${m.role}]: ${(m.content || "").slice(0, 500)}`)
- .join("\n\n");
-
- const existingSummary: any = await trigger({
- function_id: "state::get",
- payload: { scope: `reflect:summary:${agentId}`, key: sessionId },
- }).catch(() => null);
-
- const existingBlock = existingSummary?.facts
- ? `\nPREVIOUSLY EXTRACTED FACTS (focus on NEW facts not already captured):\n${existingSummary.facts.map((f: any) => `- ${f.content}`).join("\n")}\n`
- : "";
-
- const prompt = `Review this conversation and extract durable facts worth remembering.
-${existingBlock}
-CONVERSATION:
-${recentContext}
-
-Return a JSON object:
-{
- "facts": [{"content": "...", "importance": 0.0-1.0, "category": "preference|decision|learning|context"}],
- "profileUpdates": {"preferences": {}, "workStyle": "...", "communicationStyle": "..."} or null
-}
-
-Only include facts useful in future conversations. importance >= 0.5 to be stored.`;
-
- let llmResult: any;
- try {
- llmResult = await trigger({
- function_id: "llm::complete",
- payload: {
- model: {
- provider: "anthropic",
- model: "claude-haiku-4-5",
- maxTokens: 1024,
- },
- systemPrompt:
- "You are a memory curator. Extract durable facts from conversations. Output only JSON.",
- messages: [{ role: "user", content: prompt }],
- },
- });
- } catch {
- return httpOk(req, { saved: 0 });
- }
-
- let parsed: any;
- try {
- parsed = JSON.parse(stripCodeFences(llmResult?.content || "{}"));
- } catch {
- return httpOk(req, { saved: 0 });
- }
-
- const facts = Array.isArray(parsed.facts) ? parsed.facts : [];
- let saved = 0;
- for (const fact of facts) {
- if (!fact.content || (fact.importance || 0) < 0.5) continue;
- triggerVoid("memory::store", {
- agentId,
- sessionId,
- role: "system",
- content: `[Curated] ${fact.content}`,
- });
- saved++;
- }
-
- await trigger({
- function_id: "state::set",
- payload: {
- scope: `reflect:summary:${agentId}`,
- key: sessionId,
- value: { facts, updatedAt: Date.now() },
- },
- });
-
- if (parsed.profileUpdates) {
- triggerVoid("memory::user_profile::update", {
- agentId,
- updates: parsed.profileUpdates,
- });
- }
-
- recordMetric("reflect_reviews_total", 1, {
- type: "memory",
- saved: String(saved),
- });
- log.info("Memory reflection completed", { agentId, saved });
-
- return httpOk(req, { saved, totalFacts: facts.length });
- },
-);
-
-registerFunction(
- {
- id: "reflect::discover_skills",
- description: "Check if conversation yielded a reusable skill via evolve::generate",
- metadata: { category: "reflect" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const { agentId, sessionId, iterations = 0 } = req.body || req;
- if (iterations < 5) return httpOk(req, { created: false });
-
- const memories: any = await trigger({
- function_id: "memory::recall",
- payload: { agentId, query: "tools used complex workflow", limit: 30 },
- }).catch(() => []);
-
- if (!memories?.length) return httpOk(req, { created: false });
-
- const recentContext = (memories || [])
- .slice(-30)
- .map((m: any) => `[${m.role}]: ${(m.content || "").slice(0, 500)}`)
- .join("\n\n");
-
- let llmResult: any;
- try {
- llmResult = await trigger({
- function_id: "llm::complete",
- payload: {
- model: {
- provider: "anthropic",
- model: "claude-haiku-4-5",
- maxTokens: 1024,
- },
- systemPrompt:
- "You are a skill curator. Identify reusable patterns. Output only JSON.",
- messages: [
- {
- role: "user",
- content: `This session used ${iterations} tool iterations. Review the conversation:
-
-${recentContext}
-
-Was a non-trivial approach used that required trial and error?
-If a reusable function should be created, output:
-{"shouldCreate": true, "name": "...", "goal": "...", "spec": "..."}
-Otherwise output: {"shouldCreate": false}`,
- },
- ],
- },
- });
- } catch {
- return httpOk(req, { created: false });
- }
-
- try {
- const result = JSON.parse(
- stripCodeFences(llmResult?.content || "{}"),
- );
- if (result.shouldCreate && result.name && result.goal) {
- triggerVoid("evolve::generate", {
- goal: result.goal,
- spec: result.spec || "",
- name: result.name,
- agentId,
- metadata: {
- source: "auto_reflection",
- sessionId,
- iterations,
- },
- });
- recordMetric("reflect_reviews_total", 1, { type: "skill", created: "true" });
- log.info("Skill discovery triggered", {
- agentId,
- name: result.name,
- });
- return httpOk(req, { created: true, name: result.name });
- }
- } catch {}
-
- return httpOk(req, { created: false });
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "reflect::check_turn",
- config: { api_path: "api/reflect/check", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "reflect::curate_memory",
- config: { api_path: "api/reflect/curate", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "reflect::discover_skills",
- config: { api_path: "api/reflect/discover-skills", http_method: "POST" },
-});
diff --git a/src/migration.ts b/src/migration.ts
deleted file mode 100644
index 731e74c..0000000
--- a/src/migration.ts
+++ /dev/null
@@ -1,1144 +0,0 @@
-import { registerWorker } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "migration",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger } = sdk;
-
-interface MigrationItem {
- type: string;
- name: string;
- status: "migrated" | "skipped" | "error";
- source: string;
- target: string;
- reason?: string;
-}
-
-interface MigrationReport {
- framework: string;
- timestamp: string;
- dryRun: boolean;
- items: MigrationItem[];
- summary: {
- total: number;
- migrated: number;
- skipped: number;
- errors: number;
- };
-}
-
-interface OpenClawAgent {
- model?: string;
- system_prompt?: string;
- instructions?: string;
- tools?: string[];
- capabilities?: string[];
- temperature?: number;
-}
-
-interface OpenClawChannel {
- type?: string;
- webhook?: string;
- token?: string;
-}
-
-interface OpenClawModel {
- provider?: string;
- model?: string;
- api_key?: string;
- temperature?: number;
-}
-
-interface OpenClawTool {
- command?: string;
- args?: string[];
- description?: string;
-}
-
-interface OpenClawCron {
- schedule?: string;
- agent?: string;
- task?: string;
- enabled?: boolean;
-}
-
-interface OpenClawSkill {
- path?: string;
- enabled?: boolean;
- version?: string;
-}
-
-interface OpenClawSession {
- agent?: string;
- history?: unknown[];
- created?: string;
-}
-
-interface OpenClawConfig {
- agents?: Record;
- channels?: Record;
- models?: Record;
- tools?: Record;
- cron?: Record;
- skills?: Record;
- sessions?: Record;
- name?: string;
- version?: string;
-}
-
-registerFunction(
- {
- id: "migrate::openclaw",
- description:
- "Parse OpenClaw JSON5 config and migrate agents, channels, models, tools, cron, skills, sessions",
- },
- async (input: { dryRun?: boolean; configPath?: string }) => {
- const dryRun = input.dryRun ?? false;
- const items: MigrationItem[] = [];
- const fs = await import("fs/promises");
- const path = await import("path");
- const home = process.env.HOME || "/root";
-
- const configPaths = [
- input.configPath,
- path.join(home, ".openclaw/openclaw.json"),
- path.join(home, ".openclaw/config.json5"),
- path.join(home, ".clawdbot/config.json"),
- path.join(home, ".clawdbot/clawdbot.json"),
- path.join(home, ".moldbot/config.json"),
- path.join(home, ".moldbot/moldbot.json"),
- path.join(home, ".moltbot/config.json"),
- path.join(home, ".moltbot/moltbot.json"),
- ].filter(Boolean) as string[];
-
- let config: OpenClawConfig | null = null;
- let usedPath = "";
-
- for (const cp of configPaths) {
- try {
- const raw = await fs.readFile(cp, "utf-8");
- const cleaned = raw
- .replace(/\/\/.*$/gm, "")
- .replace(/\/\*[\s\S]*?\*\//g, "")
- .replace(/,\s*([}\]])/g, "$1")
- .replace(/(['"])?(\w+)(['"])?\s*:/g, '"$2":');
- config = JSON.parse(cleaned);
- usedPath = cp;
- break;
- } catch {
- continue;
- }
- }
-
- if (!config) {
- return {
- framework: "openclaw",
- timestamp: new Date().toISOString(),
- dryRun,
- items: [],
- summary: { total: 0, migrated: 0, skipped: 0, errors: 0 },
- configSearched: configPaths,
- } satisfies MigrationReport & { configSearched: string[] };
- }
-
- if (config.agents) {
- for (const [name, agent] of Object.entries(config.agents)) {
- try {
- const mappedModel = mapModel(agent.model ?? "claude-sonnet");
- const mappedTools = (agent.tools ?? []).map(mapTool);
-
- if (!dryRun) {
- const toml = [
- "[agent]",
- `name = "${name}"`,
- `description = "Migrated from OpenClaw (${usedPath})"`,
- 'module = "builtin:chat"',
- "",
- "[agent.model]",
- 'provider = "anthropic"',
- `model = "${mappedModel}"`,
- "max_tokens = 4096",
- "",
- "[agent.capabilities]",
- `tools = [${mappedTools.map((t) => `"${t}"`).join(", ")}]`,
- 'memory_scopes = ["self.*", "shared.*"]',
- 'network_hosts = ["*"]',
- "",
- "[agent.resources]",
- "max_tokens_per_hour = 500000",
- "",
- 'system_prompt = """',
- agent.system_prompt ??
- agent.instructions ??
- "You are a helpful assistant.",
- '"""',
- "",
- 'tags = ["migrated", "openclaw"]',
- ].join("\n");
-
- await fs.mkdir(`agents/${name}`, { recursive: true });
- await fs.writeFile(`agents/${name}/agent.toml`, toml);
- }
-
- items.push({
- type: "agent",
- name,
- status: "migrated",
- source: `openclaw:${usedPath}:agents.${name}`,
- target: `agents/${name}/agent.toml`,
- });
- } catch (err) {
- items.push({
- type: "agent",
- name,
- status: "error",
- source: `openclaw:agents.${name}`,
- target: "",
- reason: String(err),
- });
- }
- }
- }
-
- if (config.channels) {
- for (const [name, channel] of Object.entries(config.channels)) {
- if (!channel.type) {
- items.push({
- type: "channel",
- name,
- status: "skipped",
- source: `openclaw:channels.${name}`,
- target: "",
- reason: "No channel type specified",
- });
- continue;
- }
-
- if (!dryRun) {
- const toml = [
- "[channel]",
- `id = "${name}"`,
- `type = "${channel.type}"`,
- channel.webhook ? `webhook = "${channel.webhook}"` : "",
- channel.token ? `token = "${channel.token}"` : "",
- ]
- .filter(Boolean)
- .join("\n");
-
- await fs.mkdir("config/channels", { recursive: true });
- await fs.writeFile(`config/channels/${name}.toml`, toml);
- }
-
- items.push({
- type: "channel",
- name,
- status: "migrated",
- source: `openclaw:channels.${name}`,
- target: `config/channels/${name}.toml`,
- });
- }
- }
-
- if (config.models) {
- const supportedProviders = [
- "anthropic",
- "openai",
- "google",
- "openrouter",
- "aws-bedrock",
- "azure-openai",
- ];
-
- for (const [name, model] of Object.entries(config.models)) {
- if (model.provider && !supportedProviders.includes(model.provider)) {
- items.push({
- type: "model",
- name,
- status: "skipped",
- source: `openclaw:models.${name}`,
- target: "",
- reason: `Unsupported provider: ${model.provider}`,
- });
- continue;
- }
-
- if (!dryRun) {
- const toml = [
- "[model]",
- `id = "${name}"`,
- `provider = "${model.provider ?? "anthropic"}"`,
- `model = "${mapModel(model.model ?? "claude-sonnet")}"`,
- model.temperature !== undefined
- ? `temperature = ${model.temperature}`
- : "",
- ]
- .filter(Boolean)
- .join("\n");
-
- await fs.mkdir("config/models", { recursive: true });
- await fs.writeFile(`config/models/${name}.toml`, toml);
- }
-
- items.push({
- type: "model",
- name,
- status: "migrated",
- source: `openclaw:models.${name}`,
- target: `config/models/${name}.toml`,
- });
- }
- }
-
- if (config.tools) {
- for (const [name, tool] of Object.entries(config.tools)) {
- try {
- if (!dryRun) {
- const toml = [
- "[integration]",
- `id = "${name}"`,
- `name = "${name}"`,
- `description = "${tool.description ?? "Migrated from OpenClaw"}"`,
- 'category = "migrated"',
- 'transport = "stdio"',
- `command = "${tool.command ?? "npx"}"`,
- `args = [${(tool.args ?? []).map((a) => `"${a}"`).join(", ")}]`,
- "",
- "[integration.env]",
- "",
- "[integration.oauth]",
- "enabled = false",
- ].join("\n");
-
- await fs.mkdir("integrations", { recursive: true });
- await fs.writeFile(`integrations/${name}.toml`, toml);
- }
-
- items.push({
- type: "tool",
- name,
- status: "migrated",
- source: `openclaw:tools.${name}`,
- target: `integrations/${name}.toml`,
- });
- } catch (err) {
- items.push({
- type: "tool",
- name,
- status: "error",
- source: `openclaw:tools.${name}`,
- target: "",
- reason: String(err),
- });
- }
- }
- }
-
- if (config.cron) {
- for (const [name, cron] of Object.entries(config.cron)) {
- if (!cron.schedule || !cron.agent) {
- items.push({
- type: "cron",
- name,
- status: "skipped",
- source: `openclaw:cron.${name}`,
- target: "",
- reason: "Missing schedule or agent reference",
- });
- continue;
- }
-
- if (cron.enabled === false) {
- items.push({
- type: "cron",
- name,
- status: "skipped",
- source: `openclaw:cron.${name}`,
- target: "",
- reason: "Cron job disabled",
- });
- continue;
- }
-
- if (!dryRun) {
- const toml = [
- "[hand]",
- `id = "${name}"`,
- `name = "${name}"`,
- `description = "Migrated cron from OpenClaw: ${cron.task ?? ""}"`,
- "enabled = true",
- `schedule = "${cron.schedule}"`,
- "",
- "[hand.tools]",
- 'allowed = ["tool::*"]',
- "",
- "[hand.agent]",
- "max_iterations = 40",
- "temperature = 0.3",
- `system_prompt = """Execute the following task: ${cron.task ?? "Run scheduled job"}"""`,
- ].join("\n");
-
- await fs.mkdir(`hands/${name}`, { recursive: true });
- await fs.writeFile(`hands/${name}/HAND.toml`, toml);
- }
-
- items.push({
- type: "cron",
- name,
- status: "migrated",
- source: `openclaw:cron.${name}`,
- target: `hands/${name}/HAND.toml`,
- });
- }
- }
-
- if (config.skills) {
- for (const [name, skill] of Object.entries(config.skills)) {
- if (skill.enabled === false) {
- items.push({
- type: "skill",
- name,
- status: "skipped",
- source: `openclaw:skills.${name}`,
- target: "",
- reason: "Skill disabled",
- });
- continue;
- }
-
- if (!dryRun && skill.path) {
- try {
- const content = await fs.readFile(skill.path, "utf-8");
- await fs.mkdir(`skills/${name}`, { recursive: true });
- await fs.writeFile(`skills/${name}/SKILL.md`, content);
- } catch {
- items.push({
- type: "skill",
- name,
- status: "error",
- source: `openclaw:skills.${name}`,
- target: "",
- reason: `Could not read source: ${skill.path}`,
- });
- continue;
- }
- }
-
- items.push({
- type: "skill",
- name,
- status: "migrated",
- source: `openclaw:skills.${name}`,
- target: `skills/${name}/SKILL.md`,
- });
- }
- }
-
- if (config.sessions) {
- for (const [name, session] of Object.entries(config.sessions)) {
- if (!dryRun) {
- const sessionData = {
- id: name,
- agent: session.agent,
- history: session.history ?? [],
- created: session.created ?? new Date().toISOString(),
- migrated: new Date().toISOString(),
- source: "openclaw",
- };
-
- await fs.mkdir("data/sessions", { recursive: true });
- await fs.writeFile(
- `data/sessions/${name}.json`,
- JSON.stringify(sessionData, null, 2),
- );
- }
-
- items.push({
- type: "session",
- name,
- status: "migrated",
- source: `openclaw:sessions.${name}`,
- target: `data/sessions/${name}.json`,
- });
- }
- }
-
- const report: MigrationReport = {
- framework: "openclaw",
- timestamp: new Date().toISOString(),
- dryRun,
- items,
- summary: summarizeItems(items),
- };
-
- if (!dryRun) {
- await fs.mkdir("data/migrations", { recursive: true });
- await fs.writeFile(
- `data/migrations/openclaw-${Date.now()}.json`,
- JSON.stringify(report, null, 2),
- );
- }
-
- return report;
- },
-);
-
-registerFunction(
- {
- id: "migrate::langchain",
- description: "Parse LangChain Python configs and migrate to agentos format",
- },
- async (input: { dryRun?: boolean; configDir?: string }) => {
- const dryRun = input.dryRun ?? false;
- const items: MigrationItem[] = [];
- const configDir = input.configDir ?? process.cwd();
- const fs = await import("fs/promises");
- const pathMod = await import("path");
-
- const pythonFiles: string[] = [];
-
- const SKIP_DIRS = new Set(["node_modules", "__pycache__", ".venv", "venv"]);
-
- async function findPythonFiles(dir: string, depth = 0): Promise {
- if (depth > 5) return;
- try {
- const entries = await fs.readdir(dir, { withFileTypes: true });
- for (const entry of entries) {
- const fullPath = pathMod.join(dir, entry.name);
- if (
- entry.isDirectory() &&
- !entry.name.startsWith(".") &&
- !SKIP_DIRS.has(entry.name)
- ) {
- await findPythonFiles(fullPath, depth + 1);
- } else if (entry.name.endsWith(".py")) {
- pythonFiles.push(fullPath);
- }
- }
- } catch {
- return;
- }
- }
-
- await findPythonFiles(configDir);
-
- const langchainPatterns: Record = {
- llm: /(?:ChatOpenAI|ChatAnthropic|ChatGoogleGenerativeAI|AzureChatOpenAI)\s*\(/g,
- chain:
- /(?:LLMChain|SequentialChain|RouterChain|ConversationChain|RetrievalQA)\s*\(/g,
- tool: /(?:Tool|StructuredTool|BaseTool|tool\s*\()\s*\(/g,
- memory:
- /(?:ConversationBufferMemory|ConversationSummaryMemory|VectorStoreMemory|ChatMessageHistory)\s*\(/g,
- agent:
- /(?:create_react_agent|create_openai_tools_agent|AgentExecutor|initialize_agent)\s*\(/g,
- retriever:
- /(?:VectorStoreRetriever|SelfQueryRetriever|ContextualCompressionRetriever)\s*\(/g,
- embeddings:
- /(?:OpenAIEmbeddings|HuggingFaceEmbeddings|CohereEmbeddings)\s*\(/g,
- };
-
- for (const filePath of pythonFiles) {
- let content: string;
- try {
- content = await fs.readFile(filePath, "utf-8");
- } catch {
- continue;
- }
-
- if (!content.includes("langchain") && !content.includes("langgraph"))
- continue;
-
- const relativePath = pathMod.relative(configDir, filePath);
- const baseName = pathMod.basename(filePath, ".py");
-
- for (const [type, pattern] of Object.entries(langchainPatterns)) {
- const matches = content.match(pattern);
- if (!matches) continue;
-
- for (let i = 0; i < matches.length; i++) {
- const name = `${baseName}_${type}_${i}`;
-
- if (type === "agent" || type === "llm") {
- const modelMatch = content.match(
- /model(?:_name)?\s*=\s*["']([^"']+)["']/,
- );
- const tempMatch = content.match(/temperature\s*=\s*([\d.]+)/);
- const systemMatch = content.match(
- /system_message\s*=\s*["']([^"']+)["']/,
- );
-
- if (!dryRun) {
- const toml = [
- "[agent]",
- `name = "${name}"`,
- `description = "Migrated from LangChain (${relativePath})"`,
- 'module = "builtin:chat"',
- "",
- "[agent.model]",
- 'provider = "anthropic"',
- `model = "${mapModel(modelMatch?.[1] ?? "gpt-4")}"`,
- "max_tokens = 4096",
- "",
- "[agent.capabilities]",
- 'tools = ["tool::*"]',
- 'memory_scopes = ["self.*", "shared.*"]',
- 'network_hosts = ["*"]',
- "",
- "[agent.resources]",
- "max_tokens_per_hour = 500000",
- "",
- 'system_prompt = """',
- systemMatch?.[1] ??
- `Migrated ${type} from ${relativePath}. Review and customize this prompt.`,
- '"""',
- "",
- 'tags = ["migrated", "langchain"]',
- ].join("\n");
-
- await fs.mkdir(`agents/${name}`, { recursive: true });
- await fs.writeFile(`agents/${name}/agent.toml`, toml);
- }
-
- items.push({
- type,
- name,
- status: "migrated",
- source: `langchain:${relativePath}:${type}[${i}]`,
- target: `agents/${name}/agent.toml`,
- });
- } else if (type === "tool") {
- const toolNameMatch = content.match(/name\s*=\s*["']([^"']+)["']/);
- const toolName = toolNameMatch?.[1] ?? name;
-
- if (!dryRun) {
- const toml = [
- "[integration]",
- `id = "${toolName}"`,
- `name = "${toolName}"`,
- `description = "Migrated tool from LangChain (${relativePath})"`,
- 'category = "migrated"',
- 'transport = "stdio"',
- 'command = "python"',
- `args = ["-m", "${baseName}"]`,
- "",
- "[integration.env]",
- "",
- "[integration.oauth]",
- "enabled = false",
- ].join("\n");
-
- await fs.mkdir("integrations", { recursive: true });
- await fs.writeFile(`integrations/${toolName}.toml`, toml);
- }
-
- items.push({
- type: "tool",
- name: toolName,
- status: "migrated",
- source: `langchain:${relativePath}:tool[${i}]`,
- target: `integrations/${toolName}.toml`,
- });
- } else if (type === "chain") {
- items.push({
- type: "workflow",
- name,
- status: "migrated",
- source: `langchain:${relativePath}:chain[${i}]`,
- target: `workflows/${name}.toml`,
- });
- } else {
- items.push({
- type,
- name,
- status: "skipped",
- source: `langchain:${relativePath}:${type}[${i}]`,
- target: "",
- reason: `${type} requires manual migration review`,
- });
- }
- }
- }
- }
-
- const report: MigrationReport = {
- framework: "langchain",
- timestamp: new Date().toISOString(),
- dryRun,
- items,
- summary: summarizeItems(items),
- };
-
- if (!dryRun) {
- await fs.mkdir("data/migrations", { recursive: true });
- await fs.writeFile(
- `data/migrations/langchain-${Date.now()}.json`,
- JSON.stringify(report, null, 2),
- );
- }
-
- return report;
- },
-);
-
-registerFunction(
- {
- id: "migrate::scan",
- description: "Auto-detect installed agent frameworks in the system",
- },
- async (input: { baseDir?: string }) => {
- const fs = await import("fs/promises");
- const pathMod = await import("path");
- const home = process.env.HOME || "/root";
- const baseDir = input.baseDir ?? home;
-
- interface ScanResult {
- framework: string;
- detected: boolean;
- configPath: string;
- version?: string;
- migratable: boolean;
- }
-
- const results: ScanResult[] = [];
-
- const frameworks = [
- {
- name: "openclaw",
- paths: [
- ".openclaw/openclaw.json",
- ".openclaw/config.json5",
- ".clawdbot/config.json",
- ".moldbot/config.json",
- ".moltbot/config.json",
- ],
- migratable: true,
- },
- {
- name: "langchain",
- paths: [".langchain", "langchain.config.json"],
- migratable: true,
- },
- {
- name: "crewai",
- paths: [".crewai", "crewai.yaml", "crewai.yml"],
- migratable: true,
- },
- {
- name: "autogen",
- paths: ["autogen_config.json", "OAI_CONFIG_LIST", ".autogen"],
- migratable: true,
- },
- { name: "dspy", paths: [".dspy", "dspy_config.json"], migratable: false },
- {
- name: "llamaindex",
- paths: [".llamaindex", "llamaindex.config.json"],
- migratable: false,
- },
- {
- name: "semantic-kernel",
- paths: ["config/skills", ".semantic-kernel"],
- migratable: false,
- },
- {
- name: "haystack",
- paths: [".haystack", "haystack_pipeline.yaml", "haystack_pipeline.yml"],
- migratable: false,
- },
- {
- name: "langgraph",
- paths: [".langgraph", "langgraph.json"],
- migratable: true,
- },
- {
- name: "smolagents",
- paths: [".smolagents", "smolagents.yaml"],
- migratable: false,
- },
- {
- name: "phidata",
- paths: [".phidata", "phi_config.yaml"],
- migratable: false,
- },
- {
- name: "superagi",
- paths: [".superagi", "superagi_config.json"],
- migratable: false,
- },
- ];
-
- for (const fw of frameworks) {
- let detected = false;
- let foundPath = "";
-
- for (const p of fw.paths) {
- const fullPath = pathMod.join(baseDir, p);
- try {
- await fs.access(fullPath);
- detected = true;
- foundPath = fullPath;
- break;
- } catch {
- continue;
- }
- }
-
- let version: string | undefined;
- if (detected) {
- const versionFiles = [
- {
- file: "package.json",
- extract: (c: string) => {
- const p = JSON.parse(c);
- return p.dependencies?.[fw.name] ?? p.devDependencies?.[fw.name];
- },
- },
- {
- file: "requirements.txt",
- extract: (c: string) => {
- const m = c.match(new RegExp(`${fw.name}[=<>~!]*([\\d.]+)`));
- return m?.[1];
- },
- },
- {
- file: "pyproject.toml",
- extract: (c: string) => {
- const m = c.match(
- new RegExp(`${fw.name}[^"]*"[=<>~!]*([\\d.]+)"`),
- );
- return m?.[1];
- },
- },
- {
- file: "Pipfile",
- extract: (c: string) => {
- const m = c.match(
- new RegExp(`${fw.name}\\s*=\\s*"[=<>~]*([\\d.]+)"`),
- );
- return m?.[1];
- },
- },
- ];
-
- for (const vf of versionFiles) {
- try {
- const content = await fs.readFile(
- pathMod.join(baseDir, vf.file),
- "utf-8",
- );
- const v = vf.extract(content);
- if (v) {
- version = v;
- break;
- }
- } catch {
- continue;
- }
- }
- }
-
- results.push({
- framework: fw.name,
- detected,
- configPath: foundPath,
- version,
- migratable: fw.migratable,
- });
- }
-
- const npmGlobalCheck = ["langchain", "crewai", "autogen"];
- for (const pkg of npmGlobalCheck) {
- const existing = results.find((r) => r.framework === pkg);
- if (existing && !existing.detected) {
- try {
- const { stdout } = await execCommand("npm", [
- "list",
- "-g",
- pkg,
- "--json",
- ]);
- const parsed = JSON.parse(stdout);
- if (parsed.dependencies?.[pkg]) {
- existing.detected = true;
- existing.version = parsed.dependencies[pkg].version;
- existing.configPath = "global npm";
- }
- } catch {
- continue;
- }
- }
- }
-
- const pipCheck = [
- "langchain",
- "crewai",
- "autogen",
- "dspy-ai",
- "llama-index",
- "haystack-ai",
- ];
- for (const pkg of pipCheck) {
- const fwName = pkg
- .replace("-ai", "")
- .replace("llama-index", "llamaindex")
- .replace("dspy-ai", "dspy");
- const existing = results.find((r) => r.framework === fwName);
- if (existing && !existing.detected) {
- try {
- const { stdout } = await execCommand("pip", ["show", pkg]);
- if (stdout.includes("Version:")) {
- const versionMatch = stdout.match(/Version:\s*([\d.]+)/);
- existing.detected = true;
- existing.version = versionMatch?.[1];
- existing.configPath = "pip installed";
- }
- } catch {
- continue;
- }
- }
- }
-
- const detected = results.filter((r) => r.detected);
- const migratableDetected = detected.filter((r) => r.migratable);
-
- return {
- timestamp: new Date().toISOString(),
- baseDir,
- frameworks: results,
- detected,
- migratableDetected,
- summary: {
- scanned: results.length,
- found: detected.length,
- migratable: migratableDetected.length,
- },
- };
- },
-);
-
-registerFunction(
- {
- id: "migrate::report",
- description: "Generate a migration report from completed migration runs",
- },
- async (input: { reportId?: string; format?: "markdown" | "json" }) => {
- const fs = await import("fs/promises");
- const pathMod = await import("path");
- const format = input.format ?? "markdown";
- const migrationsDir = "data/migrations";
- let reports: MigrationReport[] = [];
-
- try {
- const files = await fs.readdir(migrationsDir);
- const jsonFiles = files.filter((f: string) => f.endsWith(".json")).sort();
-
- if (input.reportId) {
- const matching = jsonFiles.filter((f: string) =>
- f.includes(input.reportId!),
- );
- for (const file of matching) {
- const content = await fs.readFile(
- pathMod.join(migrationsDir, file),
- "utf-8",
- );
- reports.push(JSON.parse(content));
- }
- } else {
- for (const file of jsonFiles) {
- const content = await fs.readFile(
- pathMod.join(migrationsDir, file),
- "utf-8",
- );
- reports.push(JSON.parse(content));
- }
- }
- } catch {
- return { error: "No migration reports found", reports: [] };
- }
-
- const allItems = reports.flatMap((r) => r.items);
-
- const byType: Record> = {};
- for (const item of allItems) {
- if (!byType[item.type])
- byType[item.type] = { migrated: 0, skipped: 0, errors: 0 };
- byType[item.type][item.status] =
- (byType[item.type][item.status] ?? 0) + 1;
- }
-
- const byFramework: Record = {};
- for (const r of reports) {
- byFramework[r.framework] = r.summary;
- }
-
- const totals = summarizeItems(allItems);
-
- if (format === "json") {
- return { reports, aggregated: { byType, byFramework, totals } };
- }
-
- const lines = [
- "# AgentOS Migration Report",
- "",
- `Generated: ${new Date().toISOString()}`,
- `Total Migration Runs: ${reports.length}`,
- `Total Items Processed: ${totals.total}`,
- "",
- "## Summary by Framework",
- "",
- "| Framework | Total | Migrated | Skipped | Errors |",
- "|-----------|-------|----------|---------|--------|",
- ];
-
- for (const [fw, summary] of Object.entries(byFramework)) {
- lines.push(
- `| ${fw} | ${summary.total} | ${summary.migrated} | ${summary.skipped} | ${summary.errors} |`,
- );
- }
-
- lines.push("", "## Summary by Type", "");
- lines.push("| Type | Migrated | Skipped | Errors |");
- lines.push("|------|----------|---------|--------|");
- for (const [type, counts] of Object.entries(byType)) {
- lines.push(
- `| ${type} | ${counts.migrated ?? 0} | ${counts.skipped ?? 0} | ${counts.errors ?? 0} |`,
- );
- }
-
- const skippedItems = allItems.filter((i) => i.status === "skipped");
- if (skippedItems.length > 0) {
- lines.push("", "## Skipped Items", "");
- for (const item of skippedItems) {
- lines.push(
- `- **${item.type}/${item.name}**: ${item.reason ?? "Unknown reason"}`,
- );
- }
- }
-
- const errorItems = allItems.filter((i) => i.status === "error");
- if (errorItems.length > 0) {
- lines.push("", "## Errors", "");
- for (const item of errorItems) {
- lines.push(
- `- **${item.type}/${item.name}**: ${item.reason ?? "Unknown error"}`,
- );
- }
- }
-
- const migratedItems = allItems.filter((i) => i.status === "migrated");
- if (migratedItems.length > 0) {
- lines.push("", "## Successfully Migrated", "");
- for (const item of migratedItems) {
- lines.push(
- `- **${item.type}/${item.name}**: ${item.source} -> ${item.target}`,
- );
- }
- }
-
- lines.push("", "## Next Steps", "");
- lines.push(
- "1. Review migrated agent system prompts in `agents/*/agent.toml`",
- );
- lines.push("2. Configure integration API keys in `integrations/*.toml`");
- lines.push("3. Test migrated hands/cron jobs in `hands/*/HAND.toml`");
- lines.push("4. Verify session data integrity in `data/sessions/*.json`");
-
- const markdown = lines.join("\n");
-
- return { markdown, reports, aggregated: { byType, byFramework, totals } };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "migrate::openclaw",
- config: { api_path: "migrate/openclaw", http_method: "POST" },
-});
-
-registerTrigger({
- type: "http",
- function_id: "migrate::langchain",
- config: { api_path: "migrate/langchain", http_method: "POST" },
-});
-
-registerTrigger({
- type: "http",
- function_id: "migrate::scan",
- config: { api_path: "migrate/scan", http_method: "POST" },
-});
-
-registerTrigger({
- type: "http",
- function_id: "migrate::report",
- config: { api_path: "migrate/report", http_method: "POST" },
-});
-
-function summarizeItems(items: MigrationItem[]): MigrationReport["summary"] {
- return {
- total: items.length,
- migrated: items.filter((i) => i.status === "migrated").length,
- skipped: items.filter((i) => i.status === "skipped").length,
- errors: items.filter((i) => i.status === "error").length,
- };
-}
-
-function mapModel(source: string): string {
- const mapping: Record = {
- "gpt-4": "claude-sonnet-4-6",
- "gpt-4o": "claude-sonnet-4-6",
- "gpt-4-turbo": "claude-sonnet-4-6",
- "gpt-4o-mini": "claude-haiku-3.5",
- "gpt-3.5-turbo": "claude-haiku-3.5",
- "claude-3-opus": "claude-opus-4",
- "claude-3-sonnet": "claude-sonnet-4-6",
- "claude-3-haiku": "claude-haiku-3.5",
- "claude-3.5-sonnet": "claude-sonnet-4-6",
- "claude-sonnet": "claude-sonnet-4-6",
- "claude-opus": "claude-opus-4",
- "claude-haiku": "claude-haiku-3.5",
- "gemini-pro": "claude-sonnet-4-6",
- "gemini-1.5-pro": "claude-sonnet-4-6",
- "llama-3": "llama-3.3-70b",
- mixtral: "mixtral-8x7b",
- "command-r-plus": "claude-sonnet-4-6",
- };
- return mapping[source] || source;
-}
-
-function mapTool(source: string): string {
- const mapping: Record = {
- serpapi: "tool::web_search",
- google_search: "tool::web_search",
- "google-search": "tool::web_search",
- web_search: "tool::web_search",
- bing_search: "tool::web_search",
- brave_search: "tool::web_search",
- wikipedia: "tool::web_fetch",
- web_fetch: "tool::web_fetch",
- scrape: "tool::web_fetch",
- file_read: "tool::file_read",
- read_file: "tool::file_read",
- file_write: "tool::file_write",
- write_file: "tool::file_write",
- shell: "tool::shell_exec",
- terminal: "tool::shell_exec",
- python_repl: "tool::shell_exec",
- code_interpreter: "tool::shell_exec",
- browser: "tool::browser_navigate",
- calculator: "tool::calculate",
- memory: "memory::store",
- retriever: "memory::query",
- };
- return mapping[source] || `custom::${source}`;
-}
-
-async function execCommand(
- cmd: string,
- args: string[],
-): Promise<{ stdout: string }> {
- const { execFile } = await import("child_process");
- const { promisify } = await import("util");
- const exec = promisify(execFile);
- return exec(cmd, args, { maxBuffer: 10 * 1024 * 1024 });
-}
diff --git a/src/model-catalog.ts b/src/model-catalog.ts
deleted file mode 100644
index 42e324c..0000000
--- a/src/model-catalog.ts
+++ /dev/null
@@ -1,1039 +0,0 @@
-import { registerWorker } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "model-catalog",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger } = sdk;
-
-type Tier = "frontier" | "smart" | "balanced" | "fast" | "local";
-
-interface ModelEntry {
- id: string;
- provider: string;
- name: string;
- tier: Tier;
- contextWindow: number;
- maxOutput: number;
- inputPrice: number;
- outputPrice: number;
- supportsTools: boolean;
- supportsVision: boolean;
- local: boolean;
-}
-
-interface ProviderConfig {
- id: string;
- name: string;
- baseUrl: string;
- envKey: string;
- required: boolean;
- local: boolean;
- driver: "anthropic" | "openai_compat" | "gemini" | "bedrock";
-}
-
-const PROVIDERS: ProviderConfig[] = [
- {
- id: "anthropic",
- name: "Anthropic",
- baseUrl: "https://api.anthropic.com",
- envKey: "ANTHROPIC_API_KEY",
- required: true,
- local: false,
- driver: "anthropic",
- },
- {
- id: "openai",
- name: "OpenAI",
- baseUrl: "https://api.openai.com/v1",
- envKey: "OPENAI_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "gemini",
- name: "Google Gemini",
- baseUrl: "https://generativelanguage.googleapis.com",
- envKey: "GEMINI_API_KEY",
- required: true,
- local: false,
- driver: "gemini",
- },
- {
- id: "deepseek",
- name: "DeepSeek",
- baseUrl: "https://api.deepseek.com/v1",
- envKey: "DEEPSEEK_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "groq",
- name: "Groq",
- baseUrl: "https://api.groq.com/openai/v1",
- envKey: "GROQ_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "openrouter",
- name: "OpenRouter",
- baseUrl: "https://openrouter.ai/api/v1",
- envKey: "OPENROUTER_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "mistral",
- name: "Mistral AI",
- baseUrl: "https://api.mistral.ai/v1",
- envKey: "MISTRAL_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "together",
- name: "Together AI",
- baseUrl: "https://api.together.xyz/v1",
- envKey: "TOGETHER_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "fireworks",
- name: "Fireworks AI",
- baseUrl: "https://api.fireworks.ai/inference/v1",
- envKey: "FIREWORKS_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "ollama",
- name: "Ollama",
- baseUrl: "http://localhost:11434/v1",
- envKey: "OLLAMA_API_KEY",
- required: false,
- local: true,
- driver: "openai_compat",
- },
- {
- id: "vllm",
- name: "vLLM",
- baseUrl: "http://localhost:8000/v1",
- envKey: "VLLM_API_KEY",
- required: false,
- local: true,
- driver: "openai_compat",
- },
- {
- id: "lmstudio",
- name: "LM Studio",
- baseUrl: "http://localhost:1234/v1",
- envKey: "LMSTUDIO_API_KEY",
- required: false,
- local: true,
- driver: "openai_compat",
- },
- {
- id: "perplexity",
- name: "Perplexity AI",
- baseUrl: "https://api.perplexity.ai",
- envKey: "PERPLEXITY_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "cohere",
- name: "Cohere",
- baseUrl: "https://api.cohere.ai/v1",
- envKey: "COHERE_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "ai21",
- name: "AI21 Labs",
- baseUrl: "https://api.ai21.com/studio/v1",
- envKey: "AI21_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "cerebras",
- name: "Cerebras",
- baseUrl: "https://api.cerebras.ai/v1",
- envKey: "CEREBRAS_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "sambanova",
- name: "SambaNova",
- baseUrl: "https://api.sambanova.ai/v1",
- envKey: "SAMBANOVA_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "huggingface",
- name: "Hugging Face",
- baseUrl: "https://api-inference.huggingface.co",
- envKey: "HF_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "xai",
- name: "xAI",
- baseUrl: "https://api.x.ai/v1",
- envKey: "XAI_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "replicate",
- name: "Replicate",
- baseUrl: "https://api.replicate.com/v1",
- envKey: "REPLICATE_API_TOKEN",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "github_copilot",
- name: "GitHub Copilot",
- baseUrl: "https://api.githubcopilot.com",
- envKey: "GITHUB_TOKEN",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "qwen",
- name: "Qwen (Alibaba)",
- baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
- envKey: "DASHSCOPE_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "minimax",
- name: "MiniMax",
- baseUrl: "https://api.minimax.chat/v1",
- envKey: "MINIMAX_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "zhipu",
- name: "Zhipu AI (GLM)",
- baseUrl: "https://open.bigmodel.cn/api/paas/v4",
- envKey: "ZHIPU_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "moonshot",
- name: "Moonshot (Kimi)",
- baseUrl: "https://api.moonshot.cn/v1",
- envKey: "MOONSHOT_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "baidu",
- name: "Baidu Qianfan",
- baseUrl: "https://aip.baidubce.com/rpc/2.0",
- envKey: "QIANFAN_API_KEY",
- required: true,
- local: false,
- driver: "openai_compat",
- },
- {
- id: "bedrock",
- name: "AWS Bedrock",
- baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
- envKey: "AWS_ACCESS_KEY_ID",
- required: true,
- local: false,
- driver: "bedrock",
- },
-];
-
-const MODELS: ModelEntry[] = [
- {
- id: "claude-opus-4-6",
- provider: "anthropic",
- name: "Claude Opus 4.6",
- tier: "frontier",
- contextWindow: 200000,
- maxOutput: 32000,
- inputPrice: 15,
- outputPrice: 75,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "claude-sonnet-4-6",
- provider: "anthropic",
- name: "Claude Sonnet 4.6",
- tier: "smart",
- contextWindow: 200000,
- maxOutput: 16000,
- inputPrice: 3,
- outputPrice: 15,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "claude-haiku-4-5",
- provider: "anthropic",
- name: "Claude Haiku 4.5",
- tier: "fast",
- contextWindow: 200000,
- maxOutput: 8192,
- inputPrice: 0.8,
- outputPrice: 4,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "gpt-4o",
- provider: "openai",
- name: "GPT-4o",
- tier: "smart",
- contextWindow: 128000,
- maxOutput: 16384,
- inputPrice: 2.5,
- outputPrice: 10,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "gpt-4.1",
- provider: "openai",
- name: "GPT-4.1",
- tier: "smart",
- contextWindow: 1000000,
- maxOutput: 32768,
- inputPrice: 2,
- outputPrice: 8,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "o3",
- provider: "openai",
- name: "o3",
- tier: "frontier",
- contextWindow: 200000,
- maxOutput: 100000,
- inputPrice: 10,
- outputPrice: 40,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "o4-mini",
- provider: "openai",
- name: "o4-mini",
- tier: "balanced",
- contextWindow: 200000,
- maxOutput: 100000,
- inputPrice: 1.1,
- outputPrice: 4.4,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "gpt-4o-mini",
- provider: "openai",
- name: "GPT-4o mini",
- tier: "fast",
- contextWindow: 128000,
- maxOutput: 16384,
- inputPrice: 0.15,
- outputPrice: 0.6,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "gemini-2.5-flash",
- provider: "gemini",
- name: "Gemini 2.5 Flash",
- tier: "fast",
- contextWindow: 1000000,
- maxOutput: 65536,
- inputPrice: 0.15,
- outputPrice: 0.6,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "gemini-2.5-pro",
- provider: "gemini",
- name: "Gemini 2.5 Pro",
- tier: "frontier",
- contextWindow: 1000000,
- maxOutput: 65536,
- inputPrice: 1.25,
- outputPrice: 10,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "deepseek-chat",
- provider: "deepseek",
- name: "DeepSeek Chat",
- tier: "balanced",
- contextWindow: 128000,
- maxOutput: 8192,
- inputPrice: 0.14,
- outputPrice: 0.28,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "deepseek-reasoner",
- provider: "deepseek",
- name: "DeepSeek Reasoner",
- tier: "smart",
- contextWindow: 128000,
- maxOutput: 8192,
- inputPrice: 0.55,
- outputPrice: 2.19,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "llama-3.3-70b",
- provider: "groq",
- name: "Llama 3.3 70B",
- tier: "balanced",
- contextWindow: 131072,
- maxOutput: 32768,
- inputPrice: 0.59,
- outputPrice: 0.79,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "grok-2",
- provider: "xai",
- name: "Grok-2",
- tier: "smart",
- contextWindow: 131072,
- maxOutput: 32768,
- inputPrice: 2,
- outputPrice: 10,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "grok-3",
- provider: "xai",
- name: "Grok-3",
- tier: "frontier",
- contextWindow: 131072,
- maxOutput: 32768,
- inputPrice: 3,
- outputPrice: 15,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "grok-3-mini",
- provider: "xai",
- name: "Grok-3 Mini",
- tier: "fast",
- contextWindow: 131072,
- maxOutput: 32768,
- inputPrice: 0.3,
- outputPrice: 0.5,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "mistral-large",
- provider: "mistral",
- name: "Mistral Large",
- tier: "smart",
- contextWindow: 128000,
- maxOutput: 8192,
- inputPrice: 2,
- outputPrice: 6,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "sonar-pro",
- provider: "perplexity",
- name: "Sonar Pro",
- tier: "smart",
- contextWindow: 200000,
- maxOutput: 8192,
- inputPrice: 3,
- outputPrice: 15,
- supportsTools: false,
- supportsVision: false,
- local: false,
- },
- {
- id: "sonar",
- provider: "perplexity",
- name: "Sonar",
- tier: "balanced",
- contextWindow: 200000,
- maxOutput: 8192,
- inputPrice: 1,
- outputPrice: 1,
- supportsTools: false,
- supportsVision: false,
- local: false,
- },
- {
- id: "command-a",
- provider: "cohere",
- name: "Command A",
- tier: "smart",
- contextWindow: 256000,
- maxOutput: 8192,
- inputPrice: 2.5,
- outputPrice: 10,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "command-r-plus",
- provider: "cohere",
- name: "Command R+",
- tier: "smart",
- contextWindow: 128000,
- maxOutput: 4096,
- inputPrice: 3,
- outputPrice: 15,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "command-r",
- provider: "cohere",
- name: "Command R",
- tier: "balanced",
- contextWindow: 128000,
- maxOutput: 4096,
- inputPrice: 0.5,
- outputPrice: 1.5,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "jamba-1.5-large",
- provider: "ai21",
- name: "Jamba 1.5 Large",
- tier: "smart",
- contextWindow: 256000,
- maxOutput: 8192,
- inputPrice: 2,
- outputPrice: 8,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "jamba-1.5-mini",
- provider: "ai21",
- name: "Jamba 1.5 Mini",
- tier: "fast",
- contextWindow: 256000,
- maxOutput: 8192,
- inputPrice: 0.2,
- outputPrice: 0.4,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "cerebras-llama-3.3-70b",
- provider: "cerebras",
- name: "Llama 3.3 70B (Cerebras)",
- tier: "balanced",
- contextWindow: 8192,
- maxOutput: 8192,
- inputPrice: 0.6,
- outputPrice: 0.6,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "samba-llama-3.1-405b",
- provider: "sambanova",
- name: "Llama 3.1 405B (SambaNova)",
- tier: "frontier",
- contextWindow: 4096,
- maxOutput: 4096,
- inputPrice: 5,
- outputPrice: 10,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "samba-llama-3.3-70b",
- provider: "sambanova",
- name: "Llama 3.3 70B (SambaNova)",
- tier: "balanced",
- contextWindow: 4096,
- maxOutput: 4096,
- inputPrice: 0.6,
- outputPrice: 0.6,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "hf-llama-3.3-70b",
- provider: "huggingface",
- name: "Llama 3.3 70B Instruct (HF)",
- tier: "balanced",
- contextWindow: 128000,
- maxOutput: 8192,
- inputPrice: 0.36,
- outputPrice: 0.36,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "hf-mistral-7b",
- provider: "huggingface",
- name: "Mistral 7B Instruct v0.3 (HF)",
- tier: "fast",
- contextWindow: 32000,
- maxOutput: 4096,
- inputPrice: 0,
- outputPrice: 0,
- supportsTools: false,
- supportsVision: false,
- local: false,
- },
- {
- id: "replicate-llama-3.3-70b",
- provider: "replicate",
- name: "Llama 3.3 70B Instruct (Replicate)",
- tier: "balanced",
- contextWindow: 128000,
- maxOutput: 8192,
- inputPrice: 0.65,
- outputPrice: 2.75,
- supportsTools: false,
- supportsVision: false,
- local: false,
- },
- {
- id: "qwen-max",
- provider: "qwen",
- name: "Qwen Max",
- tier: "smart",
- contextWindow: 32000,
- maxOutput: 8192,
- inputPrice: 2.4,
- outputPrice: 9.6,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "qwen-plus",
- provider: "qwen",
- name: "Qwen Plus",
- tier: "balanced",
- contextWindow: 128000,
- maxOutput: 8192,
- inputPrice: 0.5,
- outputPrice: 1.5,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "qwen-turbo",
- provider: "qwen",
- name: "Qwen Turbo",
- tier: "fast",
- contextWindow: 1000000,
- maxOutput: 8192,
- inputPrice: 0.05,
- outputPrice: 0.15,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "abab7-chat",
- provider: "minimax",
- name: "ABAB 7 Chat",
- tier: "balanced",
- contextWindow: 245000,
- maxOutput: 8192,
- inputPrice: 1,
- outputPrice: 1,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "glm-4-plus",
- provider: "zhipu",
- name: "GLM-4 Plus",
- tier: "smart",
- contextWindow: 128000,
- maxOutput: 8192,
- inputPrice: 7,
- outputPrice: 7,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "glm-4",
- provider: "zhipu",
- name: "GLM-4",
- tier: "balanced",
- contextWindow: 128000,
- maxOutput: 8192,
- inputPrice: 1.4,
- outputPrice: 1.4,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "moonshot-v1-128k",
- provider: "moonshot",
- name: "Moonshot v1 128K",
- tier: "smart",
- contextWindow: 128000,
- maxOutput: 8192,
- inputPrice: 8.5,
- outputPrice: 8.5,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "moonshot-v1-32k",
- provider: "moonshot",
- name: "Moonshot v1 32K",
- tier: "balanced",
- contextWindow: 32000,
- maxOutput: 8192,
- inputPrice: 3.3,
- outputPrice: 3.3,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "ernie-4.0-turbo",
- provider: "baidu",
- name: "ERNIE 4.0 Turbo",
- tier: "smart",
- contextWindow: 128000,
- maxOutput: 8192,
- inputPrice: 4.2,
- outputPrice: 8.4,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "ernie-3.5-turbo",
- provider: "baidu",
- name: "ERNIE 3.5 Turbo",
- tier: "balanced",
- contextWindow: 128000,
- maxOutput: 8192,
- inputPrice: 0.56,
- outputPrice: 1.12,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "bedrock-claude-sonnet",
- provider: "bedrock",
- name: "Claude Sonnet 4 (Bedrock)",
- tier: "smart",
- contextWindow: 200000,
- maxOutput: 8192,
- inputPrice: 3,
- outputPrice: 15,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "bedrock-nova-pro",
- provider: "bedrock",
- name: "Amazon Nova Pro (Bedrock)",
- tier: "balanced",
- contextWindow: 300000,
- maxOutput: 8192,
- inputPrice: 0.8,
- outputPrice: 3.2,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "bedrock-llama-3.3-70b",
- provider: "bedrock",
- name: "Llama 3.3 70B (Bedrock)",
- tier: "balanced",
- contextWindow: 128000,
- maxOutput: 8192,
- inputPrice: 0.72,
- outputPrice: 0.72,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "copilot-gpt-4o",
- provider: "github_copilot",
- name: "GPT-4o (Copilot)",
- tier: "smart",
- contextWindow: 128000,
- maxOutput: 16384,
- inputPrice: 2.5,
- outputPrice: 10,
- supportsTools: true,
- supportsVision: true,
- local: false,
- },
- {
- id: "together-llama-3.3-70b",
- provider: "together",
- name: "Llama 3.3 70B (Together)",
- tier: "balanced",
- contextWindow: 131072,
- maxOutput: 8192,
- inputPrice: 0.88,
- outputPrice: 0.88,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "fireworks-llama-3.3-70b",
- provider: "fireworks",
- name: "Llama 3.3 70B (Fireworks)",
- tier: "balanced",
- contextWindow: 131072,
- maxOutput: 8192,
- inputPrice: 0.9,
- outputPrice: 0.9,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
- {
- id: "openrouter-auto",
- provider: "openrouter",
- name: "OpenRouter Auto",
- tier: "smart",
- contextWindow: 128000,
- maxOutput: 8192,
- inputPrice: 0,
- outputPrice: 0,
- supportsTools: true,
- supportsVision: false,
- local: false,
- },
-];
-
-const ALIASES: Record = {
- opus: "claude-opus-4-6",
- sonnet: "claude-sonnet-4-6",
- haiku: "claude-haiku-4-5",
- gpt4: "gpt-4o",
- gpt4o: "gpt-4o",
- gpt41: "gpt-4.1",
- o3: "o3",
- o4: "o4-mini",
- flash: "gemini-2.5-flash",
- pro: "gemini-2.5-pro",
- deepseek: "deepseek-chat",
- ds: "deepseek-chat",
- r1: "deepseek-reasoner",
- llama: "llama-3.3-70b",
- grok: "grok-2",
- grok3: "grok-3",
- "grok-3-mini": "grok-3-mini",
- mistral: "mistral-large",
- sonar: "sonar-pro",
- command: "command-a",
- "command-r": "command-r-plus",
- jamba: "jamba-1.5-large",
- cerebras: "cerebras-llama-3.3-70b",
- sambanova: "samba-llama-3.1-405b",
- "llama-3.3": "cerebras-llama-3.3-70b",
- hf: "hf-llama-3.3-70b",
- replicate: "replicate-llama-3.3-70b",
- qwen: "qwen-max",
- minimax: "abab7-chat",
- glm: "glm-4-plus",
- zhipu: "glm-4-plus",
- moonshot: "moonshot-v1-128k",
- kimi: "moonshot-v1-128k",
- ernie: "ernie-4.0-turbo",
- qianfan: "ernie-4.0-turbo",
- bedrock: "bedrock-claude-sonnet",
- nova: "bedrock-nova-pro",
- copilot: "copilot-gpt-4o",
- together: "together-llama-3.3-70b",
- fireworks: "fireworks-llama-3.3-70b",
- openrouter: "openrouter-auto",
- fast: "claude-haiku-4-5",
- cheap: "gpt-4o-mini",
- smart: "claude-sonnet-4-6",
- best: "claude-opus-4-6",
- frontier: "claude-opus-4-6",
-};
-
-registerFunction(
- {
- id: "catalog::models",
- description: "List all models with optional filters",
- },
- async ({
- tier,
- provider,
- supportsTools,
- }: { tier?: Tier; provider?: string; supportsTools?: boolean } = {}) => {
- let filtered = [...MODELS];
- if (tier) filtered = filtered.filter((m) => m.tier === tier);
- if (provider) filtered = filtered.filter((m) => m.provider === provider);
- if (supportsTools !== undefined)
- filtered = filtered.filter((m) => m.supportsTools === supportsTools);
- return filtered;
- },
-);
-
-registerFunction(
- {
- id: "catalog::resolve",
- description: "Resolve model ID or alias to full model entry",
- },
- async ({ model }: { model: string }) => {
- const resolvedId = ALIASES[model.toLowerCase()] || model;
- const entry = MODELS.find((m) => m.id === resolvedId);
- if (!entry) throw new Error(`Unknown model: ${model}`);
- return entry;
- },
-);
-
-registerFunction(
- {
- id: "catalog::providers",
- description: "List all providers with availability",
- },
- async () => {
- return PROVIDERS.map((p) => ({
- ...p,
- available: p.local || !!process.env[p.envKey],
- modelCount: MODELS.filter((m) => m.provider === p.id).length,
- }));
- },
-);
-
-registerFunction(
- { id: "catalog::aliases", description: "List all model aliases" },
- async () => ALIASES,
-);
-
-registerFunction(
- {
- id: "catalog::provider_test",
- description: "Test if a provider is reachable",
- },
- async ({ providerId }: { providerId: string }) => {
- const provider = PROVIDERS.find((p) => p.id === providerId);
- if (!provider) throw new Error(`Unknown provider: ${providerId}`);
-
- const apiKey = process.env[provider.envKey];
- if (!apiKey && provider.required) {
- return { reachable: false, reason: `${provider.envKey} not set` };
- }
-
- const resp = await fetch(provider.baseUrl, {
- method: "HEAD",
- signal: AbortSignal.timeout(5000),
- }).catch(() => null);
-
- return { reachable: !!resp, status: resp?.status };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "catalog::models",
- config: { api_path: "api/models", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "catalog::providers",
- config: { api_path: "api/providers", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "catalog::aliases",
- config: { api_path: "api/models/aliases", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "catalog::provider_test",
- config: { api_path: "api/providers/:providerId/test", http_method: "POST" },
-});
diff --git a/src/recovery.ts b/src/recovery.ts
deleted file mode 100644
index 8441acf..0000000
--- a/src/recovery.ts
+++ /dev/null
@@ -1,443 +0,0 @@
-import { registerWorker, TriggerAction, Logger } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { recordMetric } from "@agentos/shared/metrics";
-import { safeCall } from "@agentos/shared/errors";
-import { requireAuth, sanitizeId , httpOk } from "@agentos/shared/utils";
-
-
-const log = new Logger();
-const sdk = registerWorker(ENGINE_URL, { workerName: "recovery", otel: OTEL_CONFIG });
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-const triggerVoid = (id: string, payload: unknown) =>
- trigger({ function_id: id, payload, action: TriggerAction.Void() });
-
-const MAX_RECOVERY_ATTEMPTS = 3;
-
-type HealthClass = "healthy" | "degraded" | "dead" | "unrecoverable";
-
-interface ValidationResult {
- agentId: string;
- lifecycle: string | null;
- lastActivity: number | null;
- circuitBreakerOpen: boolean;
- memoryHealthy: boolean;
-}
-
-interface ClassificationResult {
- agentId: string;
- classification: HealthClass;
- checks: ValidationResult;
-}
-
-registerFunction(
- {
- id: "recovery::scan",
- description: "List all agents and validate each in parallel",
- metadata: { category: "recovery" },
- },
- async (req: any) => {
- if (req?.headers) requireAuth(req);
- const agents: any[] = await safeCall(
- () => trigger({ function_id: "state::list", payload: { scope: "agents" } }),
- [],
- { operation: "list_agents_for_scan" },
- );
-
- const results = await Promise.all(
- agents.map(async (agent) => {
- const agentId = agent.key || agent.value?.id || agent.id;
- if (!agentId) return null;
- return safeCall(
- () =>
- trigger({
- function_id: "recovery::validate",
- payload: { agentId },
- }) as Promise,
- null,
- { agentId, operation: "validate_agent" },
- );
- }),
- );
-
- const validResults = results.filter(Boolean);
- log.info("Recovery scan complete", { agentCount: validResults.length });
- recordMetric("recovery_scan", validResults.length, {}, "counter");
-
- return httpOk(req, { scannedAt: Date.now(), agents: validResults });
- },
-);
-
-registerFunction(
- {
- id: "recovery::validate",
- description: "Check lifecycle state, activity, circuit breaker, and memory health for an agent",
- metadata: { category: "recovery" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const { agentId: rawAgentId } = req.body || req;
-
- if (!rawAgentId) {
- throw Object.assign(new Error("agentId is required"), { statusCode: 400 });
- }
- const agentId = sanitizeId(rawAgentId);
-
- const lifecycleEntry: any = await safeCall(
- () =>
- trigger({
- function_id: "state::get",
- payload: { scope: `lifecycle:${agentId}`, key: "state" },
- }),
- null,
- { agentId, operation: "get_lifecycle" },
- );
-
- const sessions: any[] = await safeCall(
- () =>
- trigger({
- function_id: "state::list",
- payload: { scope: `sessions:${agentId}` },
- }),
- [],
- { agentId, operation: "list_sessions" },
- );
-
- let lastActivity: number | null = null;
- for (const s of sessions) {
- const ts = s.value?.lastActiveAt || s.value?.createdAt || 0;
- if (typeof ts === "number" && (lastActivity === null || ts > lastActivity)) {
- lastActivity = ts;
- }
- }
-
- const cbEntry: any = await safeCall(
- () =>
- trigger({
- function_id: "state::get",
- payload: { scope: "circuit_breakers", key: agentId },
- }),
- null,
- { agentId, operation: "get_circuit_breaker" },
- );
- const circuitBreakerOpen = cbEntry?.state === "open";
-
- const memEntry: any = await safeCall(
- () =>
- trigger({
- function_id: "state::get",
- payload: { scope: `memory:${agentId}`, key: "_health" },
- }),
- null,
- { agentId, operation: "get_memory_health" },
- );
- const memoryHealthy = memEntry?.healthy !== false;
-
- const result: ValidationResult = {
- agentId,
- lifecycle: lifecycleEntry?.state || null,
- lastActivity,
- circuitBreakerOpen,
- memoryHealthy,
- };
-
- return httpOk(req, result);
- },
-);
-
-registerFunction(
- {
- id: "recovery::classify",
- description: "Classify agent health as healthy, degraded, dead, or unrecoverable",
- metadata: { category: "recovery" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const { agentId: rawAgentId, checks: providedChecks } = req.body || req;
-
- if (!rawAgentId) {
- throw Object.assign(new Error("agentId is required"), { statusCode: 400 });
- }
- const agentId = sanitizeId(rawAgentId);
-
- const checks: ValidationResult = providedChecks ||
- (await trigger({
- function_id: "recovery::validate",
- payload: { agentId },
- }));
-
- const STALE_THRESHOLD_MS = 30 * 60 * 1000;
- const isStale =
- checks.lastActivity !== null &&
- Date.now() - checks.lastActivity > STALE_THRESHOLD_MS;
- const isTerminal =
- checks.lifecycle === "terminated" || checks.lifecycle === "done";
-
- let classification: HealthClass;
-
- if (isTerminal && checks.circuitBreakerOpen) {
- classification = "unrecoverable";
- } else if (
- checks.lifecycle === "failed" &&
- checks.circuitBreakerOpen &&
- !checks.memoryHealthy
- ) {
- classification = "unrecoverable";
- } else if (
- checks.lifecycle === "failed" ||
- (isStale && checks.circuitBreakerOpen)
- ) {
- classification = "dead";
- } else if (
- isStale ||
- checks.circuitBreakerOpen ||
- !checks.memoryHealthy ||
- checks.lifecycle === "blocked"
- ) {
- classification = "degraded";
- } else {
- classification = "healthy";
- }
-
- const result: ClassificationResult = { agentId, classification, checks };
- return httpOk(req, result);
- },
-);
-
-registerFunction(
- {
- id: "recovery::recover",
- description: "Attempt recovery based on classification: wake-up, restart, or escalate",
- metadata: { category: "recovery" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const { agentId: rawAgentId } = req.body || req;
-
- if (!rawAgentId) {
- throw Object.assign(new Error("agentId is required"), { statusCode: 400 });
- }
- const agentId = sanitizeId(rawAgentId);
-
- const attemptsEntry: any = await safeCall(
- () =>
- trigger({
- function_id: "state::get",
- payload: { scope: "recovery_attempts", key: agentId },
- }),
- null,
- { agentId, operation: "get_recovery_attempts" },
- );
- const attempts = attemptsEntry?.count || 0;
-
- if (attempts >= MAX_RECOVERY_ATTEMPTS) {
- triggerVoid("hook::fire", {
- type: "RecoveryExhausted",
- agentId,
- attempts,
- });
- log.warn("Recovery attempts exhausted", { agentId, attempts });
- return httpOk(req, { agentId, action: "exhausted", attempts });
- }
-
- const classified: ClassificationResult = await trigger({
- function_id: "recovery::classify",
- payload: { agentId },
- });
-
- let action: string;
-
- if (classified.classification === "healthy") {
- action = "none";
- } else if (classified.classification === "degraded") {
- const transitionResult: any = await safeCall(
- () =>
- trigger({
- function_id: "lifecycle::transition",
- payload: { agentId, newState: "working", reason: "Recovery wake-up" },
- }),
- null,
- { agentId, operation: "lifecycle_transition_wakeup" },
- );
- if (transitionResult?.transitioned) {
- triggerVoid("tool::agent_send", {
- targetAgentId: agentId,
- message: "Health check: your session appears stale. Resuming activity.",
- });
- action = "wake_up";
- } else {
- action = "none";
- }
- } else if (classified.classification === "dead") {
- const transitionResult: any = await safeCall(
- () =>
- trigger({
- function_id: "lifecycle::transition",
- payload: { agentId, newState: "recovering", reason: "Recovery restart" },
- }),
- null,
- { agentId, operation: "lifecycle_transition_restart" },
- );
- if (transitionResult?.transitioned) {
- if (classified.checks.circuitBreakerOpen) {
- await safeCall(
- () =>
- trigger({
- function_id: "state::set",
- payload: {
- scope: "circuit_breakers",
- key: agentId,
- value: { state: "closed", resetAt: Date.now() },
- },
- }),
- undefined,
- { agentId, operation: "reset_circuit_breaker" },
- );
- }
- const restartMsg = classified.checks.circuitBreakerOpen
- ? "Circuit breaker reset. Restarting."
- : "Recovery: session detected as inactive. Restarting.";
- triggerVoid("tool::agent_send", {
- targetAgentId: agentId,
- message: restartMsg,
- });
- action = "restart";
- } else {
- action = "none";
- }
- } else {
- triggerVoid("hook::fire", {
- type: "RecoveryEscalation",
- agentId,
- classification: classified.classification,
- checks: classified.checks,
- });
- action = "escalate";
- }
-
- if (action !== "none") {
- await safeCall(
- () =>
- trigger({
- function_id: "state::set",
- payload: {
- scope: "recovery_attempts",
- key: agentId,
- value: { count: attempts + 1, lastAttempt: Date.now() },
- },
- }),
- undefined,
- { agentId, operation: "increment_recovery_attempts" },
- );
- }
-
- log.info("Recovery action taken", { agentId, action, attempt: action !== "none" ? attempts + 1 : attempts });
- recordMetric("recovery_action", 1, { action }, "counter");
-
- return httpOk(req, { agentId, action, attempt: action !== "none" ? attempts + 1 : attempts, classification: classified.classification });
- },
-);
-
-registerFunction(
- {
- id: "recovery::report",
- description: "Scan all agents and auto-recover unhealthy ones in parallel",
- metadata: { category: "recovery" },
- },
- async (req: any) => {
- if (req?.headers) requireAuth(req);
- const scanResult: any = await trigger({
- function_id: "recovery::scan",
- payload: {},
- });
-
- const agents: ValidationResult[] = scanResult?.agents || [];
-
- const [classifications, recoveries] = await Promise.all([
- Promise.all(
- agents.map((a) =>
- safeCall(
- () =>
- trigger({
- function_id: "recovery::classify",
- payload: { agentId: a.agentId, checks: a },
- }) as Promise,
- { agentId: a.agentId, classification: "healthy" as HealthClass, checks: a },
- { agentId: a.agentId, operation: "classify" },
- ),
- ),
- ),
- Promise.all(
- agents
- .filter(
- (a) =>
- a.lifecycle === "failed" ||
- a.lifecycle === "blocked" ||
- (a.lastActivity !== null && Date.now() - a.lastActivity > 60 * 60 * 1000) ||
- a.circuitBreakerOpen ||
- !a.memoryHealthy,
- )
- .map((a) =>
- safeCall(
- () =>
- trigger({
- function_id: "recovery::recover",
- payload: { agentId: a.agentId },
- }),
- { agentId: a.agentId, action: "failed", error: "recovery failed" },
- { agentId: a.agentId, operation: "auto_recover" },
- ),
- ),
- ),
- ]);
-
- const summary = {
- healthy: classifications.filter((c) => c.classification === "healthy").length,
- degraded: classifications.filter((c) => c.classification === "degraded").length,
- dead: classifications.filter((c) => c.classification === "dead").length,
- unrecoverable: classifications.filter((c) => c.classification === "unrecoverable").length,
- };
-
- log.info("Recovery report complete", summary);
- recordMetric("recovery_report", 1, {}, "counter");
-
- return httpOk(req, {
- reportedAt: Date.now(),
- totalAgents: agents.length,
- summary,
- classifications,
- recoveryActions: recoveries,
- });
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "recovery::scan",
- config: { api_path: "api/recovery/scan", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "recovery::validate",
- config: { api_path: "api/recovery/validate", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "recovery::classify",
- config: { api_path: "api/recovery/classify", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "recovery::recover",
- config: { api_path: "api/recovery/recover", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "recovery::report",
- config: { api_path: "api/recovery/report", http_method: "POST" },
-});
-registerTrigger({
- type: "cron",
- function_id: "recovery::report",
- config: { expression: "*/10 * * * *" },
-});
diff --git a/src/skills.ts b/src/skills.ts
deleted file mode 100644
index 67628d5..0000000
--- a/src/skills.ts
+++ /dev/null
@@ -1,679 +0,0 @@
-import { registerWorker } from "iii-sdk";
-import { ENGINE_URL, OTEL_CONFIG, registerShutdown } from "@agentos/shared/config";
-import { requireAuth } from "@agentos/shared/utils";
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "skills",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-interface Skill {
- id: string;
- name: string;
- description: string;
- content: string;
- category: string;
- tags: string[];
- version: string;
- source: "bundled" | "installed" | "marketplace";
- installedAt?: number;
- toolScope?: string[];
- effort?: "normal" | "extended";
- model?: string;
- templateOnly?: boolean;
-}
-
-const BUNDLED_SKILLS: Omit[] = [
- {
- id: "aws",
- name: "AWS Expert",
- description: "AWS services, IAM, Lambda, S3, EC2",
- content: "",
- category: "cloud",
- tags: ["aws", "cloud"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "azure",
- name: "Azure Expert",
- description: "Azure services, AD, Functions, Blob",
- content: "",
- category: "cloud",
- tags: ["azure", "cloud"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "gcp",
- name: "GCP Expert",
- description: "Google Cloud Platform services",
- content: "",
- category: "cloud",
- tags: ["gcp", "cloud"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "docker",
- name: "Docker Expert",
- description: "Containers, Compose, multi-stage builds",
- content: "",
- category: "cloud",
- tags: ["docker", "containers"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "kubernetes",
- name: "Kubernetes Expert",
- description: "K8s orchestration, Helm, operators",
- content: "",
- category: "cloud",
- tags: ["kubernetes", "k8s"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "helm",
- name: "Helm Expert",
- description: "Helm charts, values, templating",
- content: "",
- category: "cloud",
- tags: ["helm", "kubernetes"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "terraform",
- name: "Terraform Expert",
- description: "IaC, providers, state management",
- content: "",
- category: "cloud",
- tags: ["terraform", "iac"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "ci-cd",
- name: "CI/CD Expert",
- description: "GitHub Actions, GitLab CI, pipelines",
- content: "",
- category: "devops",
- tags: ["ci-cd", "automation"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "ansible",
- name: "Ansible Expert",
- description: "Configuration management, playbooks",
- content: "",
- category: "devops",
- tags: ["ansible"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "prometheus",
- name: "Prometheus Expert",
- description: "Monitoring, alerting, PromQL",
- content: "",
- category: "devops",
- tags: ["prometheus", "monitoring"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "sentry",
- name: "Sentry Expert",
- description: "Error tracking, performance monitoring",
- content: "",
- category: "devops",
- tags: ["sentry", "monitoring"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "python-expert",
- name: "Python Expert",
- description: "Python idioms, typing, packaging",
- content: "",
- category: "languages",
- tags: ["python"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "typescript-expert",
- name: "TypeScript Expert",
- description: "TS patterns, generics, type system",
- content: "",
- category: "languages",
- tags: ["typescript"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "golang-expert",
- name: "Go Expert",
- description: "Go concurrency, interfaces, modules",
- content: "",
- category: "languages",
- tags: ["go", "golang"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "rust-expert",
- name: "Rust Expert",
- description: "Rust ownership, lifetimes, async",
- content: "",
- category: "languages",
- tags: ["rust"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "react-expert",
- name: "React Expert",
- description: "React patterns, hooks, state management",
- content: "",
- category: "languages",
- tags: ["react", "frontend"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "nextjs-expert",
- name: "Next.js Expert",
- description: "App Router, RSC, SSR/SSG",
- content: "",
- category: "languages",
- tags: ["nextjs", "react"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "postgres-expert",
- name: "PostgreSQL Expert",
- description: "SQL, indexing, optimization",
- content: "",
- category: "data",
- tags: ["postgres", "sql"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "redis-expert",
- name: "Redis Expert",
- description: "Caching, pub/sub, data structures",
- content: "",
- category: "data",
- tags: ["redis"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "mongodb",
- name: "MongoDB Expert",
- description: "Document modeling, aggregation",
- content: "",
- category: "data",
- tags: ["mongodb", "nosql"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "elasticsearch",
- name: "Elasticsearch Expert",
- description: "Full-text search, mappings",
- content: "",
- category: "data",
- tags: ["elasticsearch"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "api-tester",
- name: "API Tester",
- description: "REST/GraphQL testing, validation",
- content: "",
- category: "web",
- tags: ["api", "testing"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "graphql-expert",
- name: "GraphQL Expert",
- description: "Schema design, resolvers",
- content: "",
- category: "web",
- tags: ["graphql"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "openapi-expert",
- name: "OpenAPI Expert",
- description: "API specs, code generation",
- content: "",
- category: "web",
- tags: ["openapi", "swagger"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "code-reviewer",
- name: "Code Reviewer",
- description: "Code quality, security, patterns",
- content: "",
- category: "devtools",
- tags: ["review", "quality"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "git-expert",
- name: "Git Expert",
- description: "Git workflows, branching, rebasing",
- content: "",
- category: "devtools",
- tags: ["git"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "github",
- name: "GitHub Expert",
- description: "Actions, PRs, issues, releases",
- content: "",
- category: "devtools",
- tags: ["github"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "security-audit",
- name: "Security Auditor",
- description: "OWASP, vulnerability scanning",
- content: "",
- category: "security",
- tags: ["security", "audit"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "crypto-expert",
- name: "Cryptography Expert",
- description: "Encryption, hashing, PKI",
- content: "",
- category: "security",
- tags: ["crypto", "security"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "technical-writer",
- name: "Technical Writer",
- description: "Documentation, READMEs, guides",
- content: "",
- category: "content",
- tags: ["writing", "docs"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "email-writer",
- name: "Email Writer",
- description: "Professional email drafting",
- content: "",
- category: "content",
- tags: ["email", "writing"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "project-manager",
- name: "Project Manager",
- description: "Planning, tracking, sprints",
- content: "",
- category: "productivity",
- tags: ["pm", "planning"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "prompt-engineer",
- name: "Prompt Engineer",
- description: "Prompt optimization, few-shot",
- content: "",
- category: "productivity",
- tags: ["prompts", "llm"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "ml-engineer",
- name: "ML Engineer",
- description: "Model training, evaluation, deployment",
- content: "",
- category: "ai",
- tags: ["ml", "ai"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "vector-db",
- name: "Vector DB Expert",
- description: "Embeddings, similarity search",
- content: "",
- category: "ai",
- tags: ["vectors", "embeddings"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "linux-networking",
- name: "Linux Networking",
- description: "TCP/IP, iptables, DNS",
- content: "",
- category: "systems",
- tags: ["linux", "networking"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "nginx",
- name: "Nginx Expert",
- description: "Reverse proxy, load balancing, TLS",
- content: "",
- category: "systems",
- tags: ["nginx"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "shell-scripting",
- name: "Shell Scripting",
- description: "Bash, zsh, automation scripts",
- content: "",
- category: "systems",
- tags: ["shell", "bash"],
- version: "1.0.0",
- source: "bundled",
- },
- {
- id: "sysadmin",
- name: "System Administrator",
- description: "Server management, troubleshooting",
- content: "",
- category: "systems",
- tags: ["sysadmin"],
- version: "1.0.0",
- source: "bundled",
- },
-];
-
-registerFunction(
- {
- id: "skill::list",
- description: "List all available skills",
- metadata: { category: "skills" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const { category, tag } = req.body || req;
- const installed = (await trigger({
- function_id: "state::list",
- payload: { scope: "skills" },
- }).catch(() => [])) as any[];
- const installedSkills: Skill[] = installed
- .map((i: any) => i.value)
- .filter(Boolean);
-
- let all: Skill[] = [
- ...BUNDLED_SKILLS.map((s) => ({ ...s, installedAt: 0 })),
- ...installedSkills,
- ];
-
- if (category) all = all.filter((s) => s.category === category);
- if (tag) all = all.filter((s) => s.tags.includes(tag));
-
- return all;
- },
-);
-
-registerFunction(
- {
- id: "skill::install",
- description: "Install a skill from content or marketplace",
- metadata: { category: "skills" },
- },
- async (req: any) => {
- requireAuth(req);
- const {
- id,
- name,
- description,
- content,
- category,
- tags,
- signature,
- publicKey,
- toolScope,
- effort,
- model,
- templateOnly,
- } = req.body || req;
- const skillId =
- id || name?.toLowerCase().replace(/\s+/g, "-") || crypto.randomUUID();
-
- if (content) {
- const pipeline: any = await trigger({
- function_id: "skill::pipeline",
- payload: { content, signature, publicKey },
- }).catch(() => null);
-
- if (pipeline && !pipeline.approved) {
- return {
- installed: false,
- id: skillId,
- reason: "Security pipeline rejected skill",
- report: pipeline.report,
- };
- }
- }
-
- const skill: Skill = {
- id: skillId,
- name: name || skillId,
- description: description || "",
- content: content || "",
- category: category || "custom",
- tags: tags || [],
- version: "1.0.0",
- source: "installed",
- installedAt: Date.now(),
- toolScope: toolScope || undefined,
- effort: effort || undefined,
- model: model || undefined,
- templateOnly: templateOnly || undefined,
- };
-
- await trigger({
- function_id: "state::set",
- payload: { scope: "skills", key: skillId, value: skill },
- });
- return { installed: true, id: skillId };
- },
-);
-
-registerFunction(
- {
- id: "skill::uninstall",
- description: "Remove an installed skill",
- metadata: { category: "skills" },
- },
- async (req: any) => {
- requireAuth(req);
- const { id } = req.body || req;
- const skill = BUNDLED_SKILLS.find((s) => s.id === id);
- if (skill) throw new Error(`Cannot uninstall bundled skill: ${id}`);
-
- await trigger({
- function_id: "state::delete",
- payload: { scope: "skills", key: id },
- });
- return { uninstalled: true, id };
- },
-);
-
-registerFunction(
- {
- id: "skill::get",
- description: "Get skill content for injection into agent context",
- metadata: { category: "skills" },
- },
- async ({ id }: { id: string }) => {
- const bundled = BUNDLED_SKILLS.find((s) => s.id === id);
- if (bundled) return bundled;
-
- return trigger({
- function_id: "state::get",
- payload: { scope: "skills", key: id },
- });
- },
-);
-
-registerFunction(
- {
- id: "skill::search",
- description: "Search skills by query",
- metadata: { category: "skills" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const { query } = req.body || req;
- const q = query.toLowerCase();
- const allSkills = [...BUNDLED_SKILLS];
-
- const installed = (await trigger({
- function_id: "state::list",
- payload: { scope: "skills" },
- }).catch(() => [])) as any[];
- for (const i of installed) {
- if (i.value) allSkills.push(i.value);
- }
-
- return allSkills.filter(
- (s) =>
- s.name.toLowerCase().includes(q) ||
- s.description.toLowerCase().includes(q) ||
- s.tags.some((t) => t.includes(q)),
- );
- },
-);
-
-registerFunction(
- {
- id: "skill::parse",
- description: "Parse a SKILL.md file",
- metadata: { category: "skills" },
- },
- async ({ content }: { content: string }) => {
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
- if (!frontmatterMatch) {
- return { name: "unknown", description: "", content, tags: [] };
- }
-
- const [, frontmatter, body] = frontmatterMatch;
- const meta: Record = {};
-
- for (const line of frontmatter.split("\n")) {
- const [key, ...rest] = line.split(":");
- if (key && rest.length) {
- meta[key.trim()] = rest.join(":").trim();
- }
- }
-
- const parsedAllowedTools = meta.toolScope
- ? meta.toolScope
- .replace(/[\[\]]/g, "")
- .split(",")
- .map((t: string) => t.trim())
- .filter(Boolean)
- : undefined;
-
- const effort = meta.effort === "extended" ? "extended" : meta.effort === "normal" ? "normal" : undefined;
-
- return {
- name: meta.name || "unknown",
- description: meta.description || "",
- version: meta.version || "1.0.0",
- tags: meta.tags
- ? meta.tags
- .replace(/[\[\]]/g, "")
- .split(",")
- .map((t: string) => t.trim())
- : [],
- toolScope: parsedAllowedTools,
- effort,
- model: meta.model || undefined,
- templateOnly: meta.templateOnly === "true" ? true : undefined,
- content: body.trim(),
- };
- },
-);
-
-registerFunction(
- {
- id: "skill::marketplace_search",
- description: "Search the SkillKit marketplace for external skills",
- metadata: { category: "skills" },
- },
- async (req: any) => {
- if (req.headers) requireAuth(req);
- const { query, limit } = req.body || req;
- if (!query || query.length < 2) {
- throw new Error("Query must be at least 2 characters");
- }
- return trigger({
- function_id: "skillkit::search",
- payload: { query, limit: limit || 10 },
- }).catch((e: any) => ({
- results: [],
- error: `SkillKit unavailable: ${e.message}`,
- }));
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "skill::marketplace_search",
- config: { api_path: "api/skills/marketplace", http_method: "GET" },
-});
-
-registerTrigger({
- type: "http",
- function_id: "skill::list",
- config: { api_path: "api/skills", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "skill::install",
- config: { api_path: "api/skills", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "skill::uninstall",
- config: { api_path: "api/skills/:id", http_method: "DELETE" },
-});
-registerTrigger({
- type: "http",
- function_id: "skill::search",
- config: { api_path: "api/skills/search", http_method: "GET" },
-});
diff --git a/src/tool-profiles.ts b/src/tool-profiles.ts
deleted file mode 100644
index 032058e..0000000
--- a/src/tool-profiles.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-export const TOOL_PROFILES: Record = {
- chat: [
- "tool::web_search",
- "tool::web_fetch",
- "memory::recall",
- "memory::store",
- ],
- code: [
- "tool::file_*",
- "tool::shell_exec",
- "tool::code_*",
- "tool::apply_patch",
- ],
- research: ["tool::web_*", "tool::browser_*", "memory::*"],
- ops: [
- "tool::shell_exec",
- "tool::system_*",
- "tool::process_*",
- "tool::disk_*",
- "tool::network_*",
- ],
- data: [
- "tool::json_*",
- "tool::csv_*",
- "tool::yaml_*",
- "tool::regex_*",
- "tool::file_*",
- ],
- full: ["tool::*", "memory::*"],
-};
-
-export function matchToolProfile(
- toolId: string,
- patterns: string[],
-): boolean {
- return patterns.some((pattern) => {
- if (!pattern.includes("*")) return toolId === pattern;
- const regex = new RegExp(
- "^" + pattern.replace(/\*/g, ".*") + "$",
- );
- return regex.test(toolId);
- });
-}
-
-export function filterToolsByProfile(
- tools: any[],
- profileName: string,
-): any[] {
- const patterns = TOOL_PROFILES[profileName];
- if (!patterns) return tools;
- return tools.filter((t) => {
- const id = t.function_id || t.id || "";
- return matchToolProfile(id, patterns);
- });
-}
diff --git a/src/tools-extended.ts b/src/tools-extended.ts
deleted file mode 100644
index 7fa1a85..0000000
--- a/src/tools-extended.ts
+++ /dev/null
@@ -1,2063 +0,0 @@
-import { registerWorker, TriggerAction } from "iii-sdk";
-import {
- ENGINE_URL,
- OTEL_CONFIG,
- registerShutdown,
- WORKSPACE_ROOT,
- assertPathContained,
-} from "@agentos/shared/config";
-import { readFile } from "fs/promises";
-import { realpathSync } from "node:fs";
-import path, { resolve, relative } from "path";
-import { execFile } from "child_process";
-import { promisify } from "util";
-import { createHash, randomUUID } from "crypto";
-import os from "os";
-import { assertNoSsrf } from "@agentos/shared/utils";
-import { safeCall } from "@agentos/shared/errors";
-
-const execFileAsync = promisify(execFile);
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "tools-extended",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-
-const TAINT_ENV_ALLOWLIST = new Set([
- "PATH",
- "HOME",
- "USER",
- "LANG",
- "TERM",
- "NODE_ENV",
- "SHELL",
- "LC_ALL",
-]);
-
-function safeEnv(): Record {
- const env: Record = {};
- for (const key of TAINT_ENV_ALLOWLIST) {
- if (process.env[key]) env[key] = process.env[key]!;
- }
- return env;
-}
-
-registerFunction(
- {
- id: "tool::schedule_reminder",
- description: "Store a reminder with a target time",
- metadata: { category: "tools" },
- },
- async ({
- label,
- time,
- agentId,
- }: {
- label: string;
- time: string;
- agentId?: string;
- }) => {
- const id = randomUUID();
- const parsed = new Date(time);
- if (isNaN(parsed.getTime())) throw new Error("Invalid time format");
-
- await trigger({
- function_id: "state::set",
- payload: {
- scope: "reminders",
- key: id,
- value: {
- id,
- label,
- time: parsed.toISOString(),
- agentId: agentId || null,
- createdAt: Date.now(),
- },
- },
- });
- return { id, label, time: parsed.toISOString() };
- },
-);
-
-registerFunction(
- {
- id: "tool::cron_create",
- description: "Register a cron trigger for periodic execution",
- metadata: { category: "tools" },
- },
- async ({
- name,
- schedule,
- functionId,
- payload,
- }: {
- name: string;
- schedule: string;
- functionId: string;
- payload?: Record;
- }) => {
- if (!/^[\w\-:.]{1,128}$/.test(name)) {
- throw new Error("Invalid cron name");
- }
- if (!/^[\w\-:.]{1,256}$/.test(functionId)) {
- throw new Error("Invalid function ID");
- }
-
- registerTrigger({
- type: "cron",
- function_id: functionId,
- config: { expression: schedule },
- });
-
- await trigger({
- function_id: "state::set",
- payload: {
- scope: "cron_jobs",
- key: name,
- value: { name, schedule, functionId, payload, createdAt: Date.now() },
- },
- });
- return { created: true, name, schedule };
- },
-);
-
-registerFunction(
- {
- id: "tool::cron_list",
- description: "List all registered cron jobs",
- metadata: { category: "tools" },
- },
- async () => {
- const jobs: any = await safeCall(
- () => trigger({ function_id: "state::list", payload: { scope: "cron_jobs" } }),
- [],
- { operation: "list_cron_jobs", functionId: "tool::cron_list" },
- );
- return jobs.map((j: any) => j.value).filter(Boolean);
- },
-);
-
-registerFunction(
- {
- id: "tool::cron_delete",
- description: "Remove a cron job",
- metadata: { category: "tools" },
- },
- async ({ name }: { name: string }) => {
- await trigger({ function_id: "state::delete", payload: { scope: "cron_jobs", key: name } });
- return { deleted: true, name };
- },
-);
-
-registerFunction(
- {
- id: "tool::todo_create",
- description: "Create a todo item",
- metadata: { category: "tools" },
- },
- async ({
- title,
- description,
- priority,
- assignee,
- }: {
- title: string;
- description?: string;
- priority?: "low" | "medium" | "high";
- assignee?: string;
- }) => {
- const id = randomUUID();
- const todo = {
- id,
- title,
- description: description || "",
- priority: priority || "medium",
- assignee: assignee || null,
- status: "pending",
- createdAt: Date.now(),
- updatedAt: Date.now(),
- };
- await trigger({ function_id: "state::set", payload: { scope: "todos", key: id, value: todo } });
- return todo;
- },
-);
-
-registerFunction(
- {
- id: "tool::todo_list",
- description: "List todo items",
- metadata: { category: "tools" },
- },
- async ({ status, assignee }: { status?: string; assignee?: string }) => {
- const items: any = await safeCall(
- () => trigger({ function_id: "state::list", payload: { scope: "todos" } }),
- [],
- { operation: "list_todos", functionId: "tool::todo_list" },
- );
- let todos = items.map((i: any) => i.value).filter(Boolean);
-
- if (status) todos = todos.filter((t: any) => t.status === status);
- if (assignee) todos = todos.filter((t: any) => t.assignee === assignee);
-
- return todos;
- },
-);
-
-registerFunction(
- {
- id: "tool::todo_update",
- description: "Update a todo item status or fields",
- metadata: { category: "tools" },
- },
- async ({
- id,
- status,
- title,
- description,
- priority,
- assignee,
- }: {
- id: string;
- status?: string;
- title?: string;
- description?: string;
- priority?: string;
- assignee?: string;
- }) => {
- const existing: any = await trigger({
- function_id: "state::get",
- payload: { scope: "todos", key: id },
- });
- if (!existing) throw new Error(`Todo not found: ${id}`);
-
- const updated = {
- ...existing,
- ...(status !== undefined && { status }),
- ...(title !== undefined && { title }),
- ...(description !== undefined && { description }),
- ...(priority !== undefined && { priority }),
- ...(assignee !== undefined && { assignee }),
- updatedAt: Date.now(),
- };
- await trigger({ function_id: "state::set", payload: { scope: "todos", key: id, value: updated } });
- return updated;
- },
-);
-
-registerFunction(
- {
- id: "tool::todo_delete",
- description: "Delete a todo item",
- metadata: { category: "tools" },
- },
- async ({ id }: { id: string }) => {
- await trigger({ function_id: "state::delete", payload: { scope: "todos", key: id } });
- return { deleted: true, id };
- },
-);
-
-registerFunction(
- {
- id: "tool::image_analyze",
- description: "Describe an image using LLM vision",
- metadata: { category: "tools" },
- },
- async ({ url, prompt }: { url: string; prompt?: string }) => {
- await assertNoSsrf(url);
-
- const result: any = await safeCall(
- () =>
- trigger({
- function_id: "agent::chat",
- payload: {
- agentId: "default",
- message: prompt || "Describe this image in detail.",
- images: [url],
- },
- }),
- { content: "Vision analysis unavailable" },
- { operation: "image_analyze", functionId: "tool::image_analyze" },
- );
-
- return { url, description: result.content };
- },
-);
-
-registerFunction(
- {
- id: "tool::image_generate_prompt",
- description: "Generate an image generation prompt from a description",
- metadata: { category: "tools" },
- },
- async ({ description, style }: { description: string; style?: string }) => {
- const styleHint = style ? ` in ${style} style` : "";
- const result: any = await safeCall(
- () =>
- trigger({
- function_id: "agent::chat",
- payload: {
- agentId: "default",
- message: `Generate a detailed image generation prompt for: "${description}"${styleHint}. Return only the prompt text.`,
- },
- }),
- { content: description },
- {
- operation: "generate_prompt",
- functionId: "tool::image_generate_prompt",
- },
- );
-
- return { prompt: result.content, originalDescription: description };
- },
-);
-
-registerFunction(
- {
- id: "tool::audio_transcribe",
- description: "Transcribe audio (stub - requires external API)",
- metadata: { category: "tools" },
- },
- async ({ url }: { url: string }) => {
- await assertNoSsrf(url);
- return {
- transcript: null,
- status: "not_implemented",
- message:
- "Audio transcription requires an external API (Whisper, Deepgram). Configure TRANSCRIPTION_API_URL.",
- };
- },
-);
-
-registerFunction(
- {
- id: "tool::tts_speak",
- description: "Text-to-speech (stub - requires external API)",
- metadata: { category: "tools" },
- },
- async ({ text, voice }: { text: string; voice?: string }) => {
- return {
- audioUrl: null,
- status: "not_implemented",
- message:
- "TTS requires an external API (ElevenLabs, OpenAI TTS). Configure TTS_API_URL.",
- };
- },
-);
-
-registerFunction(
- {
- id: "tool::media_download",
- description: "SSRF-protected media file download",
- metadata: { category: "tools" },
- },
- async ({
- url,
- outputPath,
- maxSize,
- }: {
- url: string;
- outputPath: string;
- maxSize?: number;
- }) => {
- await assertNoSsrf(url);
- const resolved = resolve(WORKSPACE_ROOT, outputPath);
- assertPathContained(resolved);
-
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 60_000);
-
- try {
- const resp = await fetch(url, {
- signal: controller.signal,
- headers: { "User-Agent": "AgentOS/0.0.1" },
- });
-
- if (!resp.ok) {
- return { downloaded: false, error: "HTTP " + resp.status };
- }
-
- const contentLength = parseInt(resp.headers.get("content-length") || "0");
- const limit = maxSize || 50_000_000;
- if (contentLength > limit) {
- throw new Error(
- `File too large: ${contentLength} bytes (max ${limit})`,
- );
- }
-
- const buffer = Buffer.from(await resp.arrayBuffer());
- if (buffer.length > limit) {
- throw new Error(
- `File too large: ${buffer.length} bytes (max ${limit})`,
- );
- }
-
- const { writeFile } = await import("fs/promises");
- await writeFile(resolved, buffer);
-
- return {
- downloaded: true,
- path: resolved,
- size: buffer.length,
- contentType: resp.headers.get("content-type"),
- };
- } finally {
- clearTimeout(timer);
- }
- },
-);
-
-registerFunction(
- {
- id: "tool::kg_add",
- description: "Add an entity or relation to the knowledge graph",
- metadata: { category: "tools" },
- },
- async ({
- entity,
- type,
- relation,
- target,
- properties,
- }: {
- entity: string;
- type: string;
- relation?: string;
- target?: string;
- properties?: Record;
- }) => {
- const entityKey = entity.toLowerCase().replace(/\s+/g, "_");
-
- const existing: any = await safeCall(
- () => trigger({ function_id: "state::get", payload: { scope: "knowledge_graph", key: entityKey } }),
- null,
- { operation: "get_kg_entity", functionId: "tool::kg_add" },
- );
-
- const node = existing || {
- entity,
- type,
- relations: [],
- properties: {},
- createdAt: Date.now(),
- };
-
- if (properties) {
- node.properties = { ...node.properties, ...properties };
- }
-
- if (relation && target) {
- const targetKey = target.toLowerCase().replace(/\s+/g, "_");
- const existingRel = node.relations.find(
- (r: any) => r.relation === relation && r.target === targetKey,
- );
- if (!existingRel) {
- node.relations.push({
- relation,
- target: targetKey,
- addedAt: Date.now(),
- });
- }
- }
-
- node.updatedAt = Date.now();
- await trigger({
- function_id: "state::set",
- payload: { scope: "knowledge_graph", key: entityKey, value: node },
- });
-
- return { entity: entityKey, type, relationsCount: node.relations.length };
- },
-);
-
-registerFunction(
- {
- id: "tool::kg_query",
- description: "Traverse the knowledge graph from an entity",
- metadata: { category: "tools" },
- },
- async ({
- entity,
- depth,
- relation,
- }: {
- entity: string;
- depth?: number;
- relation?: string;
- }) => {
- const maxDepth = Math.min(depth || 2, 5);
- const entityKey = entity.toLowerCase().replace(/\s+/g, "_");
- const visited = new Set();
- const results: any[] = [];
-
- async function traverse(key: string, currentDepth: number) {
- if (currentDepth > maxDepth || visited.has(key)) return;
- if (visited.size >= 100) return;
- visited.add(key);
-
- const node: any = await safeCall(
- () => trigger({ function_id: "state::get", payload: { scope: "knowledge_graph", key } }),
- null,
- { operation: "traverse_kg", functionId: "tool::kg_query" },
- );
-
- if (!node) return;
- results.push({ ...node, _depth: currentDepth });
-
- let rels = node.relations || [];
- if (relation) rels = rels.filter((r: any) => r.relation === relation);
-
- for (const rel of rels) {
- await traverse(rel.target, currentDepth + 1);
- }
- }
-
- await traverse(entityKey, 0);
- return { root: entityKey, nodes: results, totalVisited: visited.size };
- },
-);
-
-registerFunction(
- {
- id: "tool::kg_visualize",
- description: "Generate a Mermaid diagram from the knowledge graph",
- metadata: { category: "tools" },
- },
- async ({ entity, depth }: { entity: string; depth?: number }) => {
- const maxDepth = Math.min(depth || 2, 4);
- const entityKey = entity.toLowerCase().replace(/\s+/g, "_");
- const visited = new Set();
- const edges: string[] = [];
-
- async function traverse(key: string, currentDepth: number) {
- if (currentDepth > maxDepth || visited.has(key)) return;
- if (visited.size >= 50) return;
- visited.add(key);
-
- const node: any = await safeCall(
- () => trigger({ function_id: "state::get", payload: { scope: "knowledge_graph", key } }),
- null,
- { operation: "traverse_kg_vis", functionId: "tool::kg_visualize" },
- );
- if (!node) return;
-
- for (const rel of node.relations || []) {
- const safeKey = key.replace(/[^a-zA-Z0-9_]/g, "_");
- const safeTarget = rel.target.replace(/[^a-zA-Z0-9_]/g, "_");
- const safeRel = (rel.relation || "").replace(/[^a-zA-Z0-9_ ]/g, "");
- edges.push(` ${safeKey} -->|${safeRel}| ${safeTarget}`);
- await traverse(rel.target, currentDepth + 1);
- }
- }
-
- await traverse(entityKey, 0);
-
- const mermaid = `graph TD\n${edges.join("\n")}`;
- return { mermaid, nodeCount: visited.size, edgeCount: edges.length };
- },
-);
-
-registerFunction(
- {
- id: "tool::memory_store",
- description: "Store a memory entry with metadata",
- metadata: { category: "tools" },
- },
- async ({
- key,
- content,
- tags,
- importance,
- }: {
- key: string;
- content: string;
- tags?: string[];
- importance?: number;
- }) => {
- const memoryKey = key || randomUUID();
- await trigger({
- function_id: "state::set",
- payload: {
- scope: "memories",
- key: memoryKey,
- value: {
- content,
- tags: tags || [],
- importance: importance || 5,
- createdAt: Date.now(),
- accessCount: 0,
- },
- },
- });
- return { stored: true, key: memoryKey };
- },
-);
-
-registerFunction(
- {
- id: "tool::memory_recall",
- description: "Recall a memory by key",
- metadata: { category: "tools" },
- },
- async ({ key }: { key: string }) => {
- const memory: any = await safeCall(
- () => trigger({ function_id: "state::get", payload: { scope: "memories", key } }),
- null,
- { operation: "recall_memory", functionId: "tool::memory_recall" },
- );
-
- if (!memory) return { found: false, key };
-
- memory.accessCount = (memory.accessCount || 0) + 1;
- memory.lastAccessed = Date.now();
- await trigger({ function_id: "state::set", payload: { scope: "memories", key, value: memory } });
-
- return { found: true, ...memory };
- },
-);
-
-registerFunction(
- {
- id: "tool::memory_search",
- description: "Search memories by query across content and tags",
- metadata: { category: "tools" },
- },
- async ({ query, limit }: { query: string; limit?: number }) => {
- const all: any = await safeCall(
- () => trigger({ function_id: "state::list", payload: { scope: "memories" } }),
- [],
- { operation: "search_memories", functionId: "tool::memory_search" },
- );
-
- const q = query.toLowerCase();
- const matches = all
- .map((i: any) => i.value)
- .filter(Boolean)
- .filter(
- (m: any) =>
- m.content?.toLowerCase().includes(q) ||
- m.tags?.some((t: string) => t.toLowerCase().includes(q)),
- )
- .slice(0, limit || 20);
-
- return { results: matches, total: matches.length };
- },
-);
-
-registerFunction(
- {
- id: "tool::agent_list",
- description: "List running agents",
- metadata: { category: "tools" },
- },
- async () => {
- const agents: any = await safeCall(
- () => trigger({ function_id: "state::list", payload: { scope: "agents" } }),
- [],
- { operation: "list_agents", functionId: "tool::agent_list" },
- );
- return agents
- .map((a: any) => ({
- id: a.key,
- name: a.value?.name,
- tags: a.value?.tags,
- createdAt: a.value?.createdAt,
- }))
- .filter((a: any) => a.name);
- },
-);
-
-registerFunction(
- {
- id: "tool::agent_delegate",
- description: "Delegate a task to another agent",
- metadata: { category: "tools" },
- },
- async ({
- agentId,
- task,
- context,
- }: {
- agentId: string;
- task: string;
- context?: string;
- }) => {
- const message = context ? `${task}\n\nContext: ${context}` : task;
- const result: any = await trigger({ function_id: "agent::chat", payload: { agentId, message } });
- return { agentId, response: result.content };
- },
-);
-
-registerFunction(
- {
- id: "tool::channel_send",
- description: "Send a message to a channel",
- metadata: { category: "tools" },
- },
- async ({
- channel,
- channelId,
- message,
- }: {
- channel: string;
- channelId: string;
- message: string;
- }) => {
- trigger({
- function_id: "enqueue",
- payload: { topic: `${channel}.outbound`, data: { channelId, message } },
- action: TriggerAction.Void(),
- });
- return { sent: true, channel, channelId };
- },
-);
-
-registerFunction(
- {
- id: "tool::env_get",
- description: "Get a filtered environment variable (safe list only)",
- metadata: { category: "tools" },
- },
- async ({ name }: { name: string }) => {
- if (!TAINT_ENV_ALLOWLIST.has(name)) {
- throw new Error(
- `Access denied for env var: ${name}. Allowed: ${[...TAINT_ENV_ALLOWLIST].join(", ")}`,
- );
- }
- return { name, value: process.env[name] || null };
- },
-);
-
-registerFunction(
- {
- id: "tool::system_info",
- description: "Get system information (OS, CPU, memory)",
- metadata: { category: "tools" },
- },
- async () => {
- let uptime = 0;
- try {
- uptime = os.uptime();
- } catch {
- uptime = 0;
- }
- return {
- platform: os.platform(),
- arch: os.arch(),
- hostname: os.hostname(),
- cpus: os.cpus().length,
- totalMemory: os.totalmem(),
- freeMemory: os.freemem(),
- uptime,
- nodeVersion: process.version,
- };
- },
-);
-
-registerFunction(
- {
- id: "tool::process_list",
- description: "List running processes",
- metadata: { category: "tools" },
- },
- async () => {
- const cmd = os.platform() === "win32" ? "tasklist" : "ps";
- const args =
- os.platform() === "win32"
- ? ["/fo", "csv", "/nh"]
- : ["aux", "--no-headers"];
-
- try {
- const { stdout } = await execFileAsync(cmd, args, {
- timeout: 10_000,
- maxBuffer: 512 * 1024,
- env: safeEnv(),
- });
- const lines = stdout.trim().split("\n").slice(0, 50);
- return { processes: lines, count: lines.length, truncated: true };
- } catch (err: any) {
- return { processes: [], error: err.message };
- }
- },
-);
-
-registerFunction(
- {
- id: "tool::disk_usage",
- description: "Get disk usage statistics",
- metadata: { category: "tools" },
- },
- async ({ path: diskPath }: { path?: string }) => {
- const target = diskPath || WORKSPACE_ROOT;
- const cmd = os.platform() === "win32" ? "wmic" : "df";
- const args =
- os.platform() === "win32"
- ? ["logicaldisk", "get", "size,freespace"]
- : ["-h", target];
-
- try {
- const { stdout } = await execFileAsync(cmd, args, {
- timeout: 10_000,
- maxBuffer: 256 * 1024,
- env: safeEnv(),
- });
- return { output: stdout.trim(), path: target };
- } catch (err: any) {
- return { error: err.message, path: target };
- }
- },
-);
-
-registerFunction(
- {
- id: "tool::network_check",
- description: "Check network connectivity",
- metadata: { category: "tools" },
- },
- async ({ host, timeout }: { host?: string; timeout?: number }) => {
- const target = host || "8.8.8.8";
-
- if (
- /^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|0\.)/.test(
- target,
- )
- ) {
- throw new Error("Cannot check private/reserved addresses");
- }
-
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), timeout || 5_000);
-
- try {
- const start = Date.now();
- await safeCall(
- () =>
- fetch(`https://${target}`, {
- method: "HEAD",
- signal: controller.signal,
- }),
- null,
- { operation: "network_check", functionId: "tool::network_check" },
- );
- const latency = Date.now() - start;
-
- return { reachable: true, host: target, latencyMs: latency };
- } catch {
- return { reachable: false, host: target, latencyMs: null };
- } finally {
- clearTimeout(timer);
- }
- },
-);
-
-registerFunction(
- {
- id: "tool::code_analyze",
- description: "Analyze code complexity (line counts, function counts)",
- metadata: { category: "tools" },
- },
- async ({ filePath }: { filePath: string }) => {
- const resolved = resolve(WORKSPACE_ROOT, filePath);
- assertPathContained(resolved);
-
- const content = await readFile(resolved, "utf-8");
- const lines = content.split("\n");
- const ext = path.extname(resolved);
-
- const functionPattern =
- ext === ".py"
- ? /^\s*(def|async\s+def)\s+\w+/
- : /^\s*(function|const\s+\w+\s*=\s*(async\s+)?\(|async\s+\w+\s*\(|export\s+(async\s+)?function)/;
-
- const classPattern =
- ext === ".py" ? /^\s*class\s+\w+/ : /^\s*(class|export\s+class)\s+\w+/;
-
- let functions = 0;
- let classes = 0;
- let comments = 0;
- let blankLines = 0;
-
- for (const line of lines) {
- const trimmed = line.trim();
- if (trimmed === "") blankLines++;
- else if (
- trimmed.startsWith("//") ||
- trimmed.startsWith("#") ||
- trimmed.startsWith("/*") ||
- trimmed.startsWith("*")
- )
- comments++;
- if (functionPattern.test(line)) functions++;
- if (classPattern.test(line)) classes++;
- }
-
- return {
- path: resolved,
- totalLines: lines.length,
- codeLines: lines.length - blankLines - comments,
- blankLines,
- comments,
- functions,
- classes,
- extension: ext,
- };
- },
-);
-
-registerFunction(
- {
- id: "tool::code_format",
- description: "Format code using prettier or language-specific formatter",
- metadata: { category: "tools" },
- },
- async ({ filePath, formatter }: { filePath: string; formatter?: string }) => {
- const resolved = resolve(WORKSPACE_ROOT, filePath);
- assertPathContained(resolved);
-
- const ext = path.extname(resolved);
- let cmd: string;
- let args: string[];
-
- if (formatter === "rustfmt" || ext === ".rs") {
- cmd = "rustfmt";
- args = [resolved];
- } else if (formatter === "black" || ext === ".py") {
- cmd = "python3";
- args = ["-m", "black", resolved];
- } else {
- cmd = "npx";
- args = ["prettier", "--write", resolved];
- }
-
- try {
- const { stdout, stderr } = await execFileAsync(cmd, args, {
- cwd: WORKSPACE_ROOT,
- timeout: 30_000,
- maxBuffer: 512 * 1024,
- env: safeEnv(),
- });
- return { formatted: true, path: resolved, output: stdout || stderr };
- } catch (err: any) {
- return {
- formatted: false,
- path: resolved,
- error: (err.stderr || err.message || "").slice(0, 10_000),
- };
- }
- },
-);
-
-registerFunction(
- {
- id: "tool::code_lint",
- description: "Lint code using eslint or language-specific linter",
- metadata: { category: "tools" },
- },
- async ({ filePath, linter }: { filePath: string; linter?: string }) => {
- const resolved = resolve(WORKSPACE_ROOT, filePath);
- assertPathContained(resolved);
-
- const ext = path.extname(resolved);
- let cmd: string;
- let args: string[];
-
- if (linter === "clippy" || ext === ".rs") {
- cmd = "cargo";
- args = ["clippy", "--message-format=json"];
- } else if (linter === "pylint" || ext === ".py") {
- cmd = "python3";
- args = ["-m", "pylint", "--output-format=json", resolved];
- } else {
- cmd = "npx";
- args = ["eslint", "--format=json", resolved];
- }
-
- try {
- const { stdout, stderr } = await execFileAsync(cmd, args, {
- cwd: WORKSPACE_ROOT,
- timeout: 60_000,
- maxBuffer: 1024 * 1024,
- env: safeEnv(),
- });
- return {
- path: resolved,
- output: (stdout || stderr).slice(0, 50_000),
- exitCode: 0,
- };
- } catch (err: any) {
- return {
- path: resolved,
- output: (err.stdout || err.stderr || err.message || "").slice(
- 0,
- 50_000,
- ),
- exitCode: err.code || 1,
- };
- }
- },
-);
-
-registerFunction(
- {
- id: "tool::code_test",
- description: "Run tests using a specified command",
- metadata: { category: "tools" },
- },
- async ({
- command,
- cwd,
- timeout,
- }: {
- command: string[];
- cwd?: string;
- timeout?: number;
- }) => {
- if (!command || command.length === 0) {
- throw new Error("command must be a non-empty array");
- }
-
- const ALLOWED_TEST_COMMANDS = new Set([
- "npm",
- "npx",
- "bun",
- "deno",
- "node",
- "python3",
- "python",
- "cargo",
- "go",
- "make",
- ]);
- const binary = path.basename(command[0]);
- if (!ALLOWED_TEST_COMMANDS.has(binary)) {
- throw new Error(
- `Test command not allowed: ${command[0]}. Allowed: ${[...ALLOWED_TEST_COMMANDS].join(", ")}`,
- );
- }
-
- const workDir = cwd ? resolve(WORKSPACE_ROOT, cwd) : WORKSPACE_ROOT;
- assertPathContained(workDir);
-
- try {
- const { stdout, stderr } = await execFileAsync(
- command[0],
- command.slice(1),
- {
- cwd: workDir,
- timeout: timeout || 120_000,
- maxBuffer: 2 * 1024 * 1024,
- env: safeEnv(),
- },
- );
- return {
- stdout: stdout.slice(0, 100_000),
- stderr: stderr.slice(0, 50_000),
- exitCode: 0,
- };
- } catch (err: any) {
- return {
- stdout: (err.stdout || "").slice(0, 100_000),
- stderr: (err.stderr || err.message || "").slice(0, 50_000),
- exitCode: err.code || 1,
- };
- }
- },
-);
-
-registerFunction(
- {
- id: "tool::code_explain",
- description: "Explain code using LLM",
- metadata: { category: "tools" },
- },
- async ({ filePath, question }: { filePath: string; question?: string }) => {
- const resolved = resolve(WORKSPACE_ROOT, filePath);
- assertPathContained(resolved);
-
- const content = await readFile(resolved, "utf-8");
- const truncated = content.slice(0, 50_000);
-
- const prompt = question
- ? `Explain this code, focusing on: ${question}\n\n\`\`\`\n${truncated}\n\`\`\``
- : `Explain what this code does:\n\n\`\`\`\n${truncated}\n\`\`\``;
-
- const result: any = await safeCall(
- () => trigger({ function_id: "agent::chat", payload: { agentId: "default", message: prompt } }),
- { content: "Code explanation unavailable" },
- { operation: "code_explain", functionId: "tool::code_explain" },
- );
-
- return { path: resolved, explanation: result.content };
- },
-);
-
-registerFunction(
- {
- id: "tool::json_transform",
- description: "Transform JSON data using jq-like expressions",
- metadata: { category: "tools" },
- },
- async ({ data, expression }: { data: string; expression: string }) => {
- const parsed = JSON.parse(data);
-
- if (expression === ".") return { result: parsed };
- if (expression.startsWith(".keys")) return { result: Object.keys(parsed) };
- if (expression.startsWith(".values"))
- return { result: Object.values(parsed) };
- if (expression.startsWith(".length")) {
- return {
- result: Array.isArray(parsed)
- ? parsed.length
- : Object.keys(parsed).length,
- };
- }
-
- const keys = expression.replace(/^\.|^\[/g, "").split(/[.\[]/);
- let current: any = parsed;
- for (const k of keys) {
- const cleanKey = k.replace(/[\]"']/g, "");
- if (current === null || current === undefined) break;
- current = current[cleanKey];
- }
-
- return { result: current };
- },
-);
-
-registerFunction(
- {
- id: "tool::csv_parse",
- description: "Parse CSV text to JSON",
- metadata: { category: "tools" },
- },
- async ({
- content,
- delimiter,
- hasHeader,
- }: {
- content: string;
- delimiter?: string;
- hasHeader?: boolean;
- }) => {
- const sep = delimiter || ",";
- const lines = content.trim().split("\n");
- if (lines.length === 0) return { rows: [], headers: [] };
-
- const useHeader = hasHeader !== false;
- const headers = useHeader
- ? lines[0].split(sep).map((h) => h.trim().replace(/^"|"$/g, ""))
- : [];
- const dataLines = useHeader ? lines.slice(1) : lines;
-
- const rows = dataLines.map((line) => {
- const values = line.split(sep).map((v) => v.trim().replace(/^"|"$/g, ""));
- if (useHeader) {
- const row: Record = {};
- headers.forEach((h, i) => {
- row[h] = values[i] || "";
- });
- return row;
- }
- return values;
- });
-
- return { rows, headers, rowCount: rows.length };
- },
-);
-
-registerFunction(
- {
- id: "tool::yaml_parse",
- description: "Parse YAML text to JSON (simple subset)",
- metadata: { category: "tools" },
- },
- async ({ content }: { content: string }) => {
- const result: Record = {};
- const lines = content.split("\n");
- const stack: { indent: number; obj: any; key?: string }[] = [
- { indent: -1, obj: result },
- ];
-
- for (const line of lines) {
- if (line.trim() === "" || line.trim().startsWith("#")) continue;
-
- const indent = line.search(/\S/);
- const trimmed = line.trim();
-
- while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
- stack.pop();
- }
-
- const parent = stack[stack.length - 1].obj;
-
- if (trimmed.startsWith("- ")) {
- const val = trimmed.slice(2).trim();
- if (!Array.isArray(parent)) {
- const key = stack[stack.length - 1].key;
- if (key) {
- const container =
- stack.length > 2 ? stack[stack.length - 2].obj : result;
- container[key] = [val];
- stack[stack.length - 1].obj = container[key];
- }
- } else {
- parent.push(val);
- }
- } else if (trimmed.includes(":")) {
- const colonIdx = trimmed.indexOf(":");
- const key = trimmed.slice(0, colonIdx).trim();
- const val = trimmed.slice(colonIdx + 1).trim();
-
- if (val === "" || val === "|" || val === ">") {
- parent[key] = {};
- stack.push({ indent, obj: parent[key], key });
- } else {
- let parsed: any = val;
- if (val === "true") parsed = true;
- else if (val === "false") parsed = false;
- else if (val === "null") parsed = null;
- else if (/^-?\d+(\.\d+)?$/.test(val)) parsed = Number(val);
- else parsed = val.replace(/^['"]|['"]$/g, "");
-
- parent[key] = parsed;
- }
- }
- }
-
- return { result };
- },
-);
-
-registerFunction(
- {
- id: "tool::regex_test",
- description: "Test a regex pattern against text",
- metadata: { category: "tools" },
- },
- async ({
- pattern,
- text,
- flags,
- }: {
- pattern: string;
- text: string;
- flags?: string;
- }) => {
- const safeFlags = (flags || "g").replace(/[^gimsuy]/g, "");
- const regex = new RegExp(pattern, safeFlags);
- const matches: any[] = [];
- let match: RegExpExecArray | null;
-
- let iterations = 0;
- while ((match = regex.exec(text)) !== null && iterations < 1000) {
- matches.push({
- match: match[0],
- index: match.index,
- groups: match.groups || null,
- captures: match.slice(1),
- });
- if (!regex.global) break;
- iterations++;
- }
-
- return { pattern, flags: safeFlags, matches, count: matches.length };
- },
-);
-
-registerFunction(
- {
- id: "tool::uuid_generate",
- description: "Generate a UUID v4",
- metadata: { category: "tools" },
- },
- async ({ count }: { count?: number }) => {
- const n = Math.min(count || 1, 100);
- const uuids = Array.from({ length: n }, () => randomUUID());
- return { uuids, count: n };
- },
-);
-
-registerFunction(
- {
- id: "tool::hash_compute",
- description: "Compute a hash (sha256, md5, sha1)",
- metadata: { category: "tools" },
- },
- async ({
- input,
- algorithm,
- encoding,
- }: {
- input: string;
- algorithm?: string;
- encoding?: "hex" | "base64";
- }) => {
- const ALLOWED_ALGORITHMS = new Set(["sha256", "sha1", "md5", "sha512"]);
- const algo = algorithm || "sha256";
- if (!ALLOWED_ALGORITHMS.has(algo)) {
- throw new Error(
- `Unsupported algorithm: ${algo}. Allowed: ${[...ALLOWED_ALGORITHMS].join(", ")}`,
- );
- }
- const enc = encoding || "hex";
- const hash = createHash(algo).update(input).digest(enc);
- return { hash, algorithm: algo, encoding: enc };
- },
-);
-
-registerFunction(
- {
- id: "tool::vector_store",
- description: "Store text with vector embedding for similarity search",
- metadata: { category: "tools" },
- },
- async ({
- id,
- text,
- metadata,
- namespace,
- }: {
- id?: string;
- text: string;
- metadata?: Record;
- namespace?: string;
- }) => {
- if (!text) throw new Error("text is required");
- const docId = id || randomUUID();
- const ns = namespace || "default";
- const words = text.toLowerCase().split(/\s+/).filter(Boolean);
- const wordSet = new Set(words);
- const termFreq: Record = {};
- for (const w of words) {
- termFreq[w] = (termFreq[w] || 0) + 1;
- }
-
- await trigger({
- function_id: "state::set",
- payload: {
- scope: `vector_store:${ns}`,
- key: docId,
- value: {
- id: docId,
- text,
- metadata: metadata || {},
- terms: Object.fromEntries([...wordSet].map((w) => [w, termFreq[w]])),
- wordCount: words.length,
- storedAt: Date.now(),
- },
- },
- });
- return { stored: true, id: docId, namespace: ns };
- },
-);
-
-registerFunction(
- {
- id: "tool::vector_search",
- description: "Search stored documents by similarity using BM25 scoring",
- metadata: { category: "tools" },
- },
- async ({
- query,
- namespace,
- limit,
- }: {
- query: string;
- namespace?: string;
- limit?: number;
- }) => {
- if (!query) throw new Error("query is required");
- const ns = namespace || "default";
- const maxResults = Math.min(limit || 10, 100);
- const queryTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
-
- const docs = (await trigger({
- function_id: "state::list",
- payload: { scope: `vector_store:${ns}` },
- }).catch(() => [])) as any[];
-
- const k1 = 1.5;
- const b = 0.75;
- const entries = docs.map((d: any) => d.value).filter(Boolean);
- const avgDl =
- entries.length > 0
- ? entries.reduce((s: number, e: any) => s + (e.wordCount || 0), 0) /
- entries.length
- : 1;
- const N = entries.length || 1;
-
- const scored = entries.map((doc: any) => {
- let score = 0;
- for (const qt of queryTerms) {
- const tf = doc.terms?.[qt] || 0;
- const df = entries.filter((e: any) => e.terms?.[qt]).length;
- const idf = Math.log((N - df + 0.5) / (df + 0.5) + 1);
- const dl = doc.wordCount || 1;
- score +=
- idf * ((tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (dl / avgDl))));
- }
- return { id: doc.id, text: doc.text, metadata: doc.metadata, score };
- });
-
- scored.sort((a: any, b: any) => b.score - a.score);
- return { results: scored.slice(0, maxResults), total: entries.length };
- },
-);
-
-registerFunction(
- {
- id: "tool::vector_delete",
- description: "Delete a document from vector store",
- metadata: { category: "tools" },
- },
- async ({ id, namespace }: { id: string; namespace?: string }) => {
- if (!id) throw new Error("id is required");
- const ns = namespace || "default";
- await trigger({ function_id: "state::delete", payload: { scope: `vector_store:${ns}`, key: id } });
- return { deleted: true, id };
- },
-);
-
-registerFunction(
- {
- id: "tool::git_status",
- description: "Get git repository status, branch, and recent log",
- metadata: { category: "tools" },
- },
- async ({ cwd }: { cwd?: string }) => {
- const dir = cwd ? resolve(WORKSPACE_ROOT, cwd) : WORKSPACE_ROOT;
- assertPathContained(dir);
-
- const [statusResult, branchResult, logResult] = await Promise.all([
- execFileAsync("git", ["status", "--porcelain"], {
- cwd: dir,
- env: safeEnv(),
- }).catch((e: any) => ({ stdout: "", stderr: e.message })),
- execFileAsync("git", ["branch", "--show-current"], {
- cwd: dir,
- env: safeEnv(),
- }).catch((e: any) => ({ stdout: "unknown", stderr: e.message })),
- execFileAsync("git", ["log", "--oneline", "-10"], {
- cwd: dir,
- env: safeEnv(),
- }).catch((e: any) => ({ stdout: "", stderr: e.message })),
- ]);
-
- return {
- branch: (statusResult as any).stdout?.trim()
- ? undefined
- : (branchResult as any).stdout?.trim(),
- currentBranch: (branchResult as any).stdout?.trim(),
- status:
- (statusResult as any).stdout?.trim().split("\n").filter(Boolean) || [],
- recentCommits:
- (logResult as any).stdout?.trim().split("\n").filter(Boolean) || [],
- };
- },
-);
-
-registerFunction(
- {
- id: "tool::git_diff",
- description: "Show git diff for staged or unstaged changes",
- metadata: { category: "tools" },
- },
- async ({
- staged,
- file,
- cwd,
- }: {
- staged?: boolean;
- file?: string;
- cwd?: string;
- }) => {
- const dir = cwd ? resolve(WORKSPACE_ROOT, cwd) : WORKSPACE_ROOT;
- assertPathContained(dir);
-
- const args = ["diff"];
- if (staged) args.push("--cached");
- if (file) {
- const filePath = resolve(dir, file);
- assertPathContained(filePath);
- args.push("--", file);
- }
-
- const result = await execFileAsync("git", args, {
- cwd: dir,
- env: safeEnv(),
- maxBuffer: 1024 * 1024,
- }).catch((e: any) => ({ stdout: "", stderr: e.message }));
-
- return {
- diff: (result as any).stdout?.slice(0, 50000) || "",
- truncated: ((result as any).stdout?.length || 0) > 50000,
- };
- },
-);
-
-registerFunction(
- {
- id: "tool::git_commit",
- description: "Stage files and create a git commit",
- metadata: { category: "tools" },
- },
- async ({
- message,
- files,
- cwd,
- }: {
- message: string;
- files?: string[];
- cwd?: string;
- }) => {
- if (!message) throw new Error("commit message is required");
- const dir = cwd ? resolve(WORKSPACE_ROOT, cwd) : WORKSPACE_ROOT;
- assertPathContained(dir);
-
- if (files && files.length > 0) {
- for (const f of files) {
- assertPathContained(resolve(dir, f));
- }
- await execFileAsync("git", ["add", ...files], {
- cwd: dir,
- env: safeEnv(),
- });
- } else {
- await execFileAsync("git", ["add", "-A"], { cwd: dir, env: safeEnv() });
- }
-
- const result = await execFileAsync("git", ["commit", "-m", message], {
- cwd: dir,
- env: safeEnv(),
- });
-
- return { committed: true, output: result.stdout.trim() };
- },
-);
-
-registerFunction(
- {
- id: "tool::git_log",
- description: "Get git log with optional filters",
- metadata: { category: "tools" },
- },
- async ({
- count,
- author,
- since,
- file,
- cwd,
- }: {
- count?: number;
- author?: string;
- since?: string;
- file?: string;
- cwd?: string;
- }) => {
- const dir = cwd ? resolve(WORKSPACE_ROOT, cwd) : WORKSPACE_ROOT;
- assertPathContained(dir);
-
- const n = Math.min(count || 20, 100);
- const args = ["log", `--oneline`, `-${n}`, "--format=%H|%an|%ae|%ai|%s"];
- if (author) args.push(`--author=${author}`);
- if (since) args.push(`--since=${since}`);
- if (file) {
- assertPathContained(resolve(dir, file));
- args.push("--", file);
- }
-
- const result = await execFileAsync("git", args, {
- cwd: dir,
- env: safeEnv(),
- }).catch((e: any) => ({ stdout: "", stderr: e.message }));
-
- const commits = ((result as any).stdout || "")
- .trim()
- .split("\n")
- .filter(Boolean)
- .map((line: string) => {
- const [hash, name, email, date, ...msgParts] = line.split("|");
- return { hash, author: name, email, date, message: msgParts.join("|") };
- });
-
- return { commits, count: commits.length };
- },
-);
-
-registerFunction(
- {
- id: "tool::sql_build",
- description: "Build parameterized SQL queries safely (no execution)",
- metadata: { category: "tools" },
- },
- async ({
- operation,
- table,
- columns,
- where,
- values,
- orderBy,
- limit,
- joins,
- }: {
- operation: "SELECT" | "INSERT" | "UPDATE" | "DELETE";
- table: string;
- columns?: string[];
- where?: Record;
- values?: Record;
- orderBy?: string;
- limit?: number;
- joins?: Array<{ type: string; table: string; on: string }>;
- }) => {
- if (!table || !operation)
- throw new Error("table and operation are required");
- const IDENT_RE = /^[a-zA-Z_][a-zA-Z0-9_.]*$/;
- if (!IDENT_RE.test(table)) throw new Error("Invalid table name");
-
- function assertIdentifier(name: string, label: string) {
- if (!IDENT_RE.test(name)) throw new Error(`Invalid ${label}: ${name}`);
- }
-
- const VALID_JOIN_TYPES = new Set([
- "INNER",
- "LEFT",
- "RIGHT",
- "CROSS",
- "FULL",
- ]);
-
- const params: unknown[] = [];
- let sql = "";
-
- if (operation === "SELECT") {
- if (columns?.length) {
- for (const col of columns) assertIdentifier(col, "column name");
- }
- const cols = columns?.length ? columns.join(", ") : "*";
- sql = `SELECT ${cols} FROM ${table}`;
- if (joins) {
- for (const j of joins) {
- const joinType = j.type.toUpperCase();
- if (!VALID_JOIN_TYPES.has(joinType))
- throw new Error(`Invalid join type: ${j.type}`);
- assertIdentifier(j.table, "join table name");
- if (
- !IDENT_RE.test(
- j.on
- .replace(/\s*=\s*/g, "=")
- .split("=")
- .map((s) => s.trim())
- .join(""),
- ) &&
- !/^[a-zA-Z0-9_.\s=]+$/.test(j.on)
- )
- throw new Error(`Invalid join condition: ${j.on}`);
- sql += ` ${joinType} JOIN ${j.table} ON ${j.on}`;
- }
- }
- } else if (operation === "INSERT") {
- if (!values || Object.keys(values).length === 0)
- throw new Error("values required for INSERT");
- const keys = Object.keys(values);
- for (const k of keys) assertIdentifier(k, "column name");
- const placeholders = keys.map((_, i) => `$${i + 1}`);
- sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders.join(", ")})`;
- params.push(...Object.values(values));
- } else if (operation === "UPDATE") {
- if (!values || Object.keys(values).length === 0)
- throw new Error("values required for UPDATE");
- const keys = Object.keys(values);
- for (const k of keys) assertIdentifier(k, "column name");
- const sets = keys.map((k, i) => `${k} = $${i + 1}`);
- sql = `UPDATE ${table} SET ${sets.join(", ")}`;
- params.push(...Object.values(values));
- } else if (operation === "DELETE") {
- sql = `DELETE FROM ${table}`;
- }
-
- if (where && Object.keys(where).length > 0) {
- const offset = params.length;
- const whereKeys = Object.keys(where);
- for (const k of whereKeys) assertIdentifier(k, "where column");
- const conditions = whereKeys.map((k, i) => `${k} = $${offset + i + 1}`);
- sql += ` WHERE ${conditions.join(" AND ")}`;
- params.push(...Object.values(where));
- }
-
- if (orderBy) {
- const orderParts = orderBy.split(",").map((s) => s.trim());
- for (const part of orderParts) {
- const [col, dir] = part.split(/\s+/);
- assertIdentifier(col, "orderBy column");
- if (dir && !["ASC", "DESC"].includes(dir.toUpperCase()))
- throw new Error(`Invalid order direction: ${dir}`);
- }
- sql += ` ORDER BY ${orderBy}`;
- }
- if (limit) sql += ` LIMIT ${Math.min(limit, 10000)}`;
-
- return { sql, params, parameterized: true };
- },
-);
-
-registerFunction(
- {
- id: "tool::snapshot_create",
- description: "Create a snapshot of agent state for backup/restore",
- metadata: { category: "tools" },
- },
- async ({
- agentId,
- scopes,
- label,
- }: {
- agentId: string;
- scopes?: string[];
- label?: string;
- }) => {
- if (!agentId) throw new Error("agentId is required");
- const snapshotId = `snap_${randomUUID().slice(0, 8)}`;
- const targetScopes = scopes || ["agents", "sessions", "memories", "config"];
- const snapshot: Record = {};
-
- for (const scope of targetScopes) {
- const data = (await trigger({ function_id: "state::list", payload: { scope } }).catch(
- () => [],
- )) as any[];
- const filtered = data.filter(
- (e: any) =>
- e.key === agentId ||
- e.key?.startsWith(`${agentId}:`) ||
- scope === "config",
- );
- snapshot[scope] = filtered.map((e: any) => ({
- key: e.key,
- value: e.value,
- }));
- }
-
- await trigger({
- function_id: "state::set",
- payload: {
- scope: "snapshots",
- key: snapshotId,
- value: {
- id: snapshotId,
- agentId,
- label: label || `Snapshot ${new Date().toISOString()}`,
- scopes: targetScopes,
- data: snapshot,
- createdAt: Date.now(),
- sizeEstimate: JSON.stringify(snapshot).length,
- },
- },
- });
-
- return { snapshotId, agentId, scopes: targetScopes, label };
- },
-);
-
-registerFunction(
- {
- id: "tool::snapshot_restore",
- description: "Restore agent state from a snapshot",
- metadata: { category: "tools" },
- },
- async ({ snapshotId, dryRun }: { snapshotId: string; dryRun?: boolean }) => {
- if (!snapshotId) throw new Error("snapshotId is required");
-
- const snapshot: any = await trigger({
- function_id: "state::get",
- payload: { scope: "snapshots", key: snapshotId },
- });
- if (!snapshot) throw new Error(`Snapshot ${snapshotId} not found`);
-
- const restoredKeys: string[] = [];
- for (const [scope, entries] of Object.entries(snapshot.data || {})) {
- for (const entry of entries as any[]) {
- restoredKeys.push(`${scope}:${entry.key}`);
- if (!dryRun) {
- await trigger({
- function_id: "state::set",
- payload: { scope, key: entry.key, value: entry.value },
- });
- }
- }
- }
-
- return {
- restored: !dryRun,
- dryRun: !!dryRun,
- snapshotId,
- agentId: snapshot.agentId,
- keysRestored: restoredKeys.length,
- keys: restoredKeys,
- };
- },
-);
-
-registerFunction(
- {
- id: "tool::snapshot_list",
- description: "List available snapshots",
- metadata: { category: "tools" },
- },
- async ({ agentId }: { agentId?: string }) => {
- const all = (await trigger({ function_id: "state::list", payload: { scope: "snapshots" } }).catch(
- () => [],
- )) as any[];
- let snapshots = all
- .map((e: any) => e.value)
- .filter(Boolean)
- .sort((a: any, b: any) => (b.createdAt || 0) - (a.createdAt || 0));
-
- if (agentId) {
- snapshots = snapshots.filter((s: any) => s.agentId === agentId);
- }
-
- return {
- snapshots: snapshots.map((s: any) => ({
- id: s.id,
- agentId: s.agentId,
- label: s.label,
- scopes: s.scopes,
- createdAt: s.createdAt,
- sizeEstimate: s.sizeEstimate,
- })),
- count: snapshots.length,
- };
- },
-);
-
-registerFunction(
- {
- id: "tool::api_call",
- description: "Make HTTP API calls with retry and structured response",
- metadata: { category: "tools" },
- },
- async ({
- url,
- method,
- headers,
- body,
- timeout,
- retries,
- }: {
- url: string;
- method?: string;
- headers?: Record;
- body?: unknown;
- timeout?: number;
- retries?: number;
- }) => {
- if (!url) throw new Error("url is required");
- await assertNoSsrf(url);
-
- const httpMethod = (method || "GET").toUpperCase();
- const timeoutMs = Math.min(timeout || 30000, 60000);
- const maxRetries = Math.min(retries || 0, 3);
-
- let lastError: string | undefined;
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
- try {
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), timeoutMs);
-
- try {
- const opts: RequestInit = {
- method: httpMethod,
- headers: {
- "User-Agent": "AgentOS/0.0.1",
- ...(headers || {}),
- },
- signal: controller.signal,
- };
-
- if (body && httpMethod !== "GET" && httpMethod !== "HEAD") {
- opts.body = typeof body === "string" ? body : JSON.stringify(body);
- if (!headers?.["Content-Type"]) {
- (opts.headers as Record)["Content-Type"] =
- "application/json";
- }
- }
-
- const response = await fetch(url, opts);
-
- const contentType = response.headers.get("content-type") || "";
- let responseBody: unknown;
- if (contentType.includes("application/json")) {
- responseBody = await response.json();
- } else {
- const text = await response.text();
- responseBody = text.slice(0, 100000);
- }
-
- return {
- status: response.status,
- statusText: response.statusText,
- headers: Object.fromEntries(response.headers.entries()),
- body: responseBody,
- attempt: attempt + 1,
- };
- } finally {
- clearTimeout(timer);
- }
- } catch (e: any) {
- lastError = e.message;
- if (attempt < maxRetries) {
- await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
- }
- }
- }
-
- return { error: lastError, attempts: maxRetries + 1 };
- },
-);
-
-registerFunction(
- {
- id: "tool::compose",
- description:
- "Chain multiple tool calls in sequence, piping outputs to inputs",
- metadata: { category: "tools" },
- },
- async ({
- steps,
- input,
- }: {
- steps: Array<{
- toolId: string;
- argsTemplate: Record;
- }>;
- input?: Record;
- }) => {
- if (!steps || steps.length === 0) throw new Error("steps are required");
- if (steps.length > 10) throw new Error("Maximum 10 steps allowed");
-
- let context: Record = { ...(input || {}) };
- const results: Array<{
- step: number;
- toolId: string;
- output: unknown;
- durationMs: number;
- }> = [];
-
- for (let i = 0; i < steps.length; i++) {
- const step = steps[i];
- const resolvedArgs: Record = {};
-
- for (const [key, val] of Object.entries(step.argsTemplate)) {
- if (typeof val === "string" && val.startsWith("$prev.")) {
- const path = val.slice(6);
- resolvedArgs[key] = (context as any)[path];
- } else if (typeof val === "string" && val.startsWith("$input.")) {
- const path = val.slice(7);
- resolvedArgs[key] = (input as any)?.[path];
- } else {
- resolvedArgs[key] = val;
- }
- }
-
- const start = Date.now();
- const output: any = await trigger({ function_id: step.toolId, payload: resolvedArgs }).catch(
- (e: any) => ({ error: e.message }),
- );
- const durationMs = Date.now() - start;
-
- results.push({ step: i, toolId: step.toolId, output, durationMs });
-
- if (output?.error) {
- return { completed: false, failedAt: i, results, error: output.error };
- }
-
- context =
- typeof output === "object" && output !== null
- ? output
- : { result: output };
- }
-
- return {
- completed: true,
- stepsExecuted: steps.length,
- results,
- finalOutput: context,
- };
- },
-);
-
-registerTrigger({
- type: "http",
- function_id: "tool::vector_search",
- config: { api_path: "api/vector/search", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "tool::git_status",
- config: { api_path: "api/git/status", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "tool::git_diff",
- config: { api_path: "api/git/diff", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "tool::git_log",
- config: { api_path: "api/git/log", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "tool::sql_build",
- config: { api_path: "api/sql/build", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "tool::snapshot_list",
- config: { api_path: "api/snapshots", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "tool::snapshot_create",
- config: { api_path: "api/snapshots", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "tool::api_call",
- config: { api_path: "api/tools/api-call", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "tool::compose",
- config: { api_path: "api/tools/compose", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "tool::todo_list",
- config: { api_path: "api/todos", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "tool::todo_create",
- config: { api_path: "api/todos", http_method: "POST" },
-});
-registerTrigger({
- type: "http",
- function_id: "tool::cron_list",
- config: { api_path: "api/cron", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "tool::agent_list",
- config: { api_path: "api/agents/list", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "tool::system_info",
- config: { api_path: "api/system/info", http_method: "GET" },
-});
-registerTrigger({
- type: "http",
- function_id: "tool::memory_search",
- config: { api_path: "api/memory/search", http_method: "GET" },
-});
diff --git a/src/tools.ts b/src/tools.ts
deleted file mode 100644
index a521918..0000000
--- a/src/tools.ts
+++ /dev/null
@@ -1,609 +0,0 @@
-import { registerWorker, TriggerAction, Logger } from "iii-sdk";
-import {
- ENGINE_URL,
- OTEL_CONFIG,
- registerShutdown,
- WORKSPACE_ROOT,
- assertPathContained,
-} from "@agentos/shared/config";
-import { readFile, writeFile, readdir, stat } from "fs/promises";
-import { realpathSync } from "node:fs";
-import path, { resolve, relative, join } from "path";
-import { execFile } from "child_process";
-import { promisify } from "util";
-import { assertNoSsrf } from "@agentos/shared/utils";
-import { safeCall } from "@agentos/shared/errors";
-import { recordMetric } from "@agentos/shared/metrics";
-
-const log = new Logger();
-const execFileAsync = promisify(execFile);
-
-const sdk = registerWorker(ENGINE_URL, {
- workerName: "tools",
- otel: OTEL_CONFIG,
-});
-registerShutdown(sdk);
-const { registerFunction, registerTrigger, trigger } = sdk;
-const triggerVoid = (id: string, payload: unknown) =>
- trigger({ function_id: id, payload, action: TriggerAction.Void() });
-
-
-async function withToolMetrics(
- toolId: string,
- fn: () => Promise,
-): Promise {
- const start = Date.now();
- try {
- const result = await fn();
- recordMetric("tool_execution_total", 1, { toolId, status: "success" });
- recordMetric(
- "function_call_duration_ms",
- Date.now() - start,
- { functionId: toolId, status: "success" },
- "histogram",
- );
- return result;
- } catch (err) {
- recordMetric("tool_execution_total", 1, { toolId, status: "failure" });
- recordMetric(
- "function_call_duration_ms",
- Date.now() - start,
- { functionId: toolId, status: "error" },
- "histogram",
- );
- throw err;
- }
-}
-
-const TAINT_ENV_ALLOWLIST = new Set([
- "PATH",
- "HOME",
- "USER",
- "LANG",
- "TERM",
- "NODE_ENV",
-]);
-
-registerFunction(
- {
- id: "tool::file_read",
- description: "Read file contents with path containment",
- metadata: { category: "tools" },
- },
- async ({ path, maxBytes }: { path: string; maxBytes?: number }) => {
- return withToolMetrics("tool::file_read", async () => {
- const resolved = resolve(WORKSPACE_ROOT, path);
- assertPathContained(resolved);
-
- const content = await readFile(resolved, "utf-8");
- const limited = maxBytes ? content.slice(0, maxBytes) : content;
- return { content: limited, path: resolved, size: content.length };
- });
- },
-);
-
-registerFunction(
- {
- id: "tool::file_write",
- description: "Write file with path containment",
- metadata: { category: "tools" },
- },
- async ({ path, content }: { path: string; content: string }) => {
- return withToolMetrics("tool::file_write", async () => {
- const resolved = resolve(WORKSPACE_ROOT, path);
- assertPathContained(resolved);
-
- await writeFile(resolved, content, "utf-8");
- return { written: true, path: resolved, size: content.length };
- });
- },
-);
-
-registerFunction(
- {
- id: "tool::file_list",
- description: "List directory contents",
- metadata: { category: "tools" },
- },
- async ({ path, recursive }: { path: string; recursive?: boolean }) => {
- const resolved = resolve(WORKSPACE_ROOT, path || ".");
- assertPathContained(resolved);
-
- const entries = await readdir(resolved, { withFileTypes: true });
- const results = await Promise.all(
- entries.map(async (e) => {
- const fullPath = join(resolved, e.name);
- const s = await safeCall(() => stat(fullPath), null, {
- operation: "file_stat",
- functionId: "tool::file_list",
- });
- return {
- name: e.name,
- type: e.isDirectory() ? "directory" : "file",
- size: s?.size || 0,
- modified: s?.mtime?.toISOString(),
- };
- }),
- );
-
- return { path: resolved, entries: results };
- },
-);
-
-registerFunction(
- {
- id: "tool::apply_patch",
- description: "Apply a unified diff patch",
- metadata: { category: "tools" },
- },
- async ({ path, patch }: { path: string; patch: string }) => {
- const resolved = resolve(WORKSPACE_ROOT, path);
- assertPathContained(resolved);
-
- const content = await readFile(resolved, "utf-8");
- const lines = content.split("\n");
- const patchLines = patch.split("\n");
-
- let output = [...lines];
- let offset = 0;
-
- for (const line of patchLines) {
- if (line.startsWith("@@")) {
- const match = line.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@/);
- if (match) offset = parseInt(match[1]) - 1;
- } else if (line.startsWith("-")) {
- const idx = output.indexOf(line.slice(1), offset);
- if (idx >= 0) {
- output.splice(idx, 1);
- }
- } else if (line.startsWith("+")) {
- output.splice(offset, 0, line.slice(1));
- offset++;
- } else if (!line.startsWith("\\")) {
- offset++;
- }
- }
-
- await writeFile(resolved, output.join("\n"), "utf-8");
- return { patched: true, path: resolved };
- },
-);
-
-const SHELL_COMMAND_ALLOWLIST = new Set([
- "git",
- "node",
- "npm",
- "npx",
- "bun",
- "deno",
- "python3",
- "python",
- "pip",
- "ls",
- "cat",
- "grep",
- "find",
- "echo",
- "mkdir",
- "touch",
- "cp",
- "mv",
- "head",
- "tail",
- "wc",
- "sort",
- "uniq",
- "diff",
- "curl",
- "wget",
- "tar",
- "zip",
- "unzip",
- "jq",
- "sed",
- "awk",
- "which",
- "env",
- "date",
- "cargo",
- "rustc",
- "go",
- "make",
- "cmake",
-]);
-
-registerFunction(
- {
- id: "tool::shell_exec",
- description: "Execute command with sandbox (no shell interpretation)",
- metadata: { category: "tools" },
- },
- async ({
- argv,
- cwd,
- timeout,
- }: {
- argv: string[];
- cwd?: string;
- timeout?: number;
- }) => {
- const start = Date.now();
- if (!argv || argv.length === 0) {
- throw new Error("argv must be a non-empty array");
- }
-
- const binary = path.basename(argv[0]);
- if (!SHELL_COMMAND_ALLOWLIST.has(binary)) {
- throw new Error(
- `Command not allowed: ${argv[0]}. Allowed: ${[...SHELL_COMMAND_ALLOWLIST].join(", ")}`,
- );
- }
-
- const workDir = cwd ? resolve(WORKSPACE_ROOT, cwd) : WORKSPACE_ROOT;
- assertPathContained(workDir);
-
- try {
- const { stdout, stderr } = await execFileAsync(argv[0], argv.slice(1), {
- cwd: workDir,
- timeout: timeout || 120_000,
- maxBuffer: 1024 * 1024,
- env: safeEnv(),
- });
-
- triggerVoid("security::audit", {
- type: "shell_exec",
- detail: { argv, cwd: workDir, exitCode: 0 },
- });
-
- recordMetric("tool_execution_total", 1, {
- toolId: "tool::shell_exec",
- status: "success",
- });
- recordMetric(
- "function_call_duration_ms",
- Date.now() - start,
- { functionId: "tool::shell_exec", status: "success" },
- "histogram",
- );
-
- return {
- stdout: stdout.slice(0, 100_000),
- stderr: stderr.slice(0, 50_000),
- exitCode: 0,
- };
- } catch (err: any) {
- recordMetric("tool_execution_total", 1, {
- toolId: "tool::shell_exec",
- status: "failure",
- });
- recordMetric(
- "function_call_duration_ms",
- Date.now() - start,
- { functionId: "tool::shell_exec", status: "error" },
- "histogram",
- );
- log.warn("Shell exec failed", {
- functionId: "tool::shell_exec",
- duration: Date.now() - start,
- });
-
- return {
- stdout: (err.stdout || "").slice(0, 100_000),
- stderr: (err.stderr || err.message || "").slice(0, 50_000),
- exitCode: err.code || 1,
- };
- }
- },
-);
-
-registerFunction(
- {
- id: "tool::web_fetch",
- description: "SSRF-protected HTTP fetch with HTML-to-text",
- metadata: { category: "tools" },
- },
- async ({ url, maxSize }: { url: string; maxSize?: number }) => {
- await assertNoSsrf(url);
-
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 30_000);
-
- try {
- return await withToolMetrics("tool::web_fetch", async () => {
- const resp = await fetch(url, {
- signal: controller.signal,
- headers: { "User-Agent": "AgentOS/0.0.1" },
- });
-
- const contentType = resp.headers.get("content-type") || "";
- const text = await resp.text();
- const limit = maxSize || 500_000;
- const limited = text.slice(0, limit);
- const content = contentType.includes("html")
- ? htmlToText(limited)
- : limited;
-
- return {
- url,
- status: resp.status,
- contentType,
- content: content.slice(0, 100_000),
- truncated: text.length > limit,
- };
- });
- } finally {
- clearTimeout(timer);
- }
- },
-);
-
-registerFunction(
- {
- id: "tool::web_search",
- description: "Multi-provider web search",
- metadata: { category: "tools" },
- },
- async ({
- query,
- provider,
- maxResults,
- }: {
- query: string;
- provider?: string;
- maxResults?: number;
- }) => {
- const limit = maxResults || 5;
-
- if (provider === "tavily" && process.env.TAVILY_API_KEY) {
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 30_000);
- try {
- const resp = await fetch("https://api.tavily.com/search", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- api_key: process.env.TAVILY_API_KEY,
- query,
- max_results: limit,
- }),
- signal: controller.signal,
- });
- const data = (await resp.json()) as any;
- return { results: data.results || [], provider: "tavily" };
- } finally {
- clearTimeout(timer);
- }
- }
-
- if (provider === "brave" && process.env.BRAVE_API_KEY) {
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 30_000);
- try {
- const resp = await fetch(
- `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${limit}`,
- {
- headers: { "X-Subscription-Token": process.env.BRAVE_API_KEY },
- signal: controller.signal,
- },
- );
- const data = (await resp.json()) as any;
- return {
- results: (data.web?.results || []).map((r: any) => ({
- title: r.title,
- url: r.url,
- content: r.description,
- })),
- provider: "brave",
- };
- } finally {
- clearTimeout(timer);
- }
- }
-
- const ddgController = new AbortController();
- const ddgTimer = setTimeout(() => ddgController.abort(), 30_000);
- try {
- const ddgResp = await fetch(
- `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1`,
- { signal: ddgController.signal },
- );
- const ddg = (await ddgResp.json()) as any;
- const results = (ddg.RelatedTopics || [])
- .slice(0, limit)
- .map((t: any) => ({
- title: t.Text?.slice(0, 100),
- url: t.FirstURL,
- content: t.Text,
- }));
- return { results, provider: "duckduckgo" };
- } finally {
- clearTimeout(ddgTimer);
- }
- },
-);
-
-const MAX_AGENT_DEPTH = 5;
-const DEFAULT_MAX_SUB_AGENTS = 20;
-
-registerFunction(
- {
- id: "tool::agent_spawn",
- description: "Spawn a sub-agent with depth limit and resource quota",
- metadata: { category: "tools" },
- },
- async ({
- template,
- parentId,
- message,
- }: {
- template: string;
- parentId?: string;
- message: string;
- }) => {
- let parentDepth = 0;
- let parentQuota = DEFAULT_MAX_SUB_AGENTS;
- if (parentId) {
- const depthEntry = await safeCall(
- () => trigger({ function_id: "state::get", payload: { scope: "agent_depth", key: parentId } }),
- null,
- { operation: "get_agent_depth", functionId: "tool::agent_spawn" },
- );
- parentDepth = (depthEntry as any)?.depth || 0;
-
- const quotaEntry: any = await safeCall(
- () => trigger({ function_id: "state::get", payload: { scope: "agent_quota", key: parentId } }),
- null,
- { operation: "get_agent_quota", functionId: "tool::agent_spawn" },
- );
- if (quotaEntry) {
- parentQuota = quotaEntry.remaining || 0;
- }
- }
- if (parentDepth >= MAX_AGENT_DEPTH) {
- throw new Error(`Max agent depth (${MAX_AGENT_DEPTH}) exceeded`);
- }
- if (parentQuota <= 0) {
- throw new Error(`Sub-agent quota exhausted for parent ${parentId}`);
- }
-
- const agentId = crypto.randomUUID();
-
- const result = await trigger({ function_id: "agent::create", payload: {
- id: agentId,
- name: `sub-${template}-${Date.now()}`,
- parentId,
- tags: ["sub-agent"],
- } });
-
- await trigger({ function_id: "state::set", payload: {
- scope: "agent_depth",
- key: agentId,
- value: {
- depth: parentDepth + 1,
- parent: parentId || null,
- createdAt: Date.now(),
- },
- } });
-
- const childQuota = Math.max(0, parentQuota - 1);
- await trigger({ function_id: "state::set", payload: {
- scope: "agent_quota",
- key: agentId,
- value: { remaining: childQuota, max: childQuota, parent: parentId },
- } });
-
- if (parentId) {
- await trigger({ function_id: "state::update", payload: {
- scope: "agent_quota",
- key: parentId,
- operations: [{ type: "increment", path: "remaining", value: -1 }],
- } });
- }
-
- const response: any = await trigger({ function_id: "agent::chat", payload: { agentId, message } });
-
- return { agentId, response: response.content, depth: parentDepth + 1 };
- },
-);
-
-const MESSAGE_MAX_BYTES = 100 * 1024;
-const MESSAGE_QUEUE_MAX = 1000;
-const AGENT_PAIR_RATE_WINDOW_MS = 60_000;
-const AGENT_PAIR_RATE_LIMIT = 10;
-const agentPairSendTimes = new Map();
-
-registerFunction(
- {
- id: "tool::agent_send",
- description: "Send message to another agent with spam prevention",
- metadata: { category: "tools" },
- },
- async ({
- agentId,
- targetAgentId,
- message,
- }: {
- agentId: string;
- targetAgentId: string;
- message: string;
- }) => {
- if (agentId) {
- const senderConfig = await safeCall(
- () => trigger({ function_id: "state::get", payload: { scope: "agents", key: agentId } }),
- null,
- { operation: "get_sender_config", functionId: "tool::agent_send" },
- );
- const allowed = (senderConfig as any)?.capabilities?.tools || [];
- if (!allowed.includes("*") && !allowed.includes("agent::send")) {
- throw new Error("Agent does not have agent::send capability");
- }
- }
-
- if (
- typeof message !== "string" ||
- Buffer.byteLength(message, "utf-8") > MESSAGE_MAX_BYTES
- ) {
- throw new Error(`Message exceeds size cap of ${MESSAGE_MAX_BYTES} bytes`);
- }
-
- const queueDepth: any = await safeCall(
- () =>
- trigger({ function_id: "state::get", payload: {
- scope: "agent_queue_depth",
- key: targetAgentId,
- } }),
- null,
- { operation: "get_queue_depth", functionId: "tool::agent_send" },
- );
- if (queueDepth && (queueDepth.count || 0) >= MESSAGE_QUEUE_MAX) {
- throw new Error(
- `Target agent ${targetAgentId} message queue full (max ${MESSAGE_QUEUE_MAX})`,
- );
- }
-
- const pairKey = `${agentId}:${targetAgentId}`;
- const now = Date.now();
- const times = agentPairSendTimes.get(pairKey) || [];
- const recent = times.filter((t) => now - t < AGENT_PAIR_RATE_WINDOW_MS);
- if (recent.length >= AGENT_PAIR_RATE_LIMIT) {
- throw new Error(
- `Rate limit exceeded: max ${AGENT_PAIR_RATE_LIMIT} messages per minute between agents`,
- );
- }
- recent.push(now);
- agentPairSendTimes.set(pairKey, recent);
-
- triggerVoid("state::update", {
- scope: "agent_queue_depth",
- key: targetAgentId,
- operations: [{ type: "increment", path: "count", value: 1 }],
- });
-
- triggerVoid("enqueue", {
- topic: "agent.inbox",
- data: { agentId: targetAgentId, message },
- });
- return { sent: true, targetAgentId };
- },
-);
-
-function safeEnv(): Record {
- const env: Record = {};
- for (const key of TAINT_ENV_ALLOWLIST) {
- if (process.env[key]) env[key] = process.env[key]!;
- }
- return env;
-}
-
-function htmlToText(html: string): string {
- return html
- .replace(/