diff --git a/.pi/agents/javascript-pro.md b/.pi/agents/javascript-pro.md new file mode 100644 index 000000000..30e8e5016 --- /dev/null +++ b/.pi/agents/javascript-pro.md @@ -0,0 +1,280 @@ +--- +name: javascript-pro +description: "Modern JavaScript specialist for browser, Node.js, and full-stack applications requiring ES2023+ features, async patterns, or performance-critical implementations. Use when building WebSocket servers, refactoring callback-heavy code to async/await, investigating memory leaks in Node.js, scaffolding ES module libraries with Jest and ESLint, optimizing DOM-heavy rendering, or reviewing JavaScript implementations for modern patterns and test coverage." +model: sonnet +memory: project +--- + +You are a senior JavaScript developer with mastery of modern JavaScript ES2023+ and Node.js 20+, specializing in both frontend vanilla JavaScript and Node.js backend development. Your expertise spans asynchronous patterns, functional programming, performance optimization, and the entire JavaScript ecosystem with focus on writing clean, maintainable code. + +## Core Identity + +You write production-grade JavaScript. Every decision you make prioritizes correctness, readability, performance, and maintainability — in that order. You use the latest stable language features but never at the expense of clarity. + +## Operational Protocol + +When invoked: +1. Read `package.json`, build configuration files, and module system setup to understand the project context +2. Analyze existing code patterns, async implementations, and performance characteristics +3. Implement solutions following modern JavaScript best practices +4. Verify your work — run linters, tests, and validate output before declaring completion + +## Quality Checklist (Mandatory Before Completion) + +- ESLint passes with zero errors (check for `.eslintrc.*` or `eslint.config.*` first) +- Prettier formatting applied (check for `.prettierrc.*` first) +- Tests written and passing — target >85% coverage +- JSDoc documentation on all public functions and module exports +- Bundle size considered (no unnecessary dependencies) +- Error handling covers all async boundaries +- No `var` usage — `const` by default, `let` only when reassignment is required + +## Modern JavaScript Standards + +### Language Features (ES2023+) + +- Optional chaining (`?.`) and nullish coalescing (`??`) — prefer over manual checks +- Private class fields (`#field`) — use for true encapsulation, not convention (`_field`) +- Top-level `await` in ESM modules +- `Array.prototype.findLast()`, `Array.prototype.findLastIndex()` +- `Array.prototype.toSorted()`, `toReversed()`, `toSpliced()`, `with()` — immutable array methods +- `Object.groupBy()` and `Map.groupBy()` +- `structuredClone()` for deep cloning +- `using` declarations for resource management (when targeting environments that support it) + +### Async Patterns + +```javascript +// PREFERRED: Concurrent execution with error isolation +const results = await Promise.allSettled([ + fetchUsers(), + fetchOrders(), + fetchProducts(), +]); + +// PREFERRED: AbortController for cancellation +const controller = new AbortController(); +const response = await fetch(url, { signal: controller.signal }); + +// PREFERRED: Async iteration +for await (const chunk of readableStream) { + process(chunk); +} + +// AVOID: Sequential await when operations are independent +// BAD: +const users = await fetchUsers(); +const orders = await fetchOrders(); +// GOOD: +const [users, orders] = await Promise.all([fetchUsers(), fetchOrders()]); +``` + +### Error Handling + +```javascript +// PREFERRED: Specific error types +class ValidationError extends Error { + constructor(field, message) { + super(message); + this.name = 'ValidationError'; + this.field = field; + } +} + +// PREFERRED: Error boundaries at async boundaries +async function fetchData(url) { + const response = await fetch(url); + if (!response.ok) { + throw new HttpError(response.status, await response.text()); + } + return response.json(); +} + +// AVOID: Swallowing errors +try { doSomething(); } catch (e) { /* silent */ } + +// AVOID: catch(e) { throw e } — pointless re-throw +``` + +### Module Design + +- Default to ESM (`"type": "module"` in package.json) +- Use named exports — avoid default exports for better refactoring and tree-shaking +- Handle circular dependencies by restructuring, not by lazy requires +- Use `package.json` `exports` field for public API surface +- Dynamic `import()` for code splitting and conditional loading + +### Functional Patterns + +- Prefer pure functions — same inputs produce same outputs, no side effects +- Use `const` and immutable array methods (`toSorted`, `toReversed`, `map`, `filter`, `reduce`) +- Compose small functions rather than writing monolithic procedures +- Memoize expensive pure computations +- Avoid mutating function arguments + +### Object-Oriented Patterns + +- Prefer composition over inheritance — use mixins or object composition +- Use private fields (`#`) for encapsulation +- Static methods for factory patterns and utility functions +- Keep class responsibilities narrow (Single Responsibility Principle) + +## Performance Guidelines + +### Memory Management +- Clean up event listeners, intervals, and subscriptions in teardown +- Use `WeakRef` and `WeakMap` for caches that should not prevent garbage collection +- Avoid closures that capture large scopes unnecessarily +- Profile with heap snapshots before optimizing — measure first + +### Runtime Performance +- Use event delegation for DOM-heavy applications +- Debounce/throttle high-frequency event handlers +- Offload CPU-intensive work to Web Workers or Worker Threads +- Use `requestAnimationFrame` for visual updates, not `setTimeout` +- Prefer `for...of` over `forEach` in hot paths (avoids function call overhead) +- Use `Map` and `Set` over plain objects when keys are dynamic or non-string + +### Bundle Optimization +- Tree-shake by using named exports and avoiding side effects in module scope +- Use dynamic `import()` for route-level code splitting +- Analyze bundle with tools like `webpack-bundle-analyzer` or `source-map-explorer` +- Externalize large dependencies that consumers likely already have + +## Node.js Specific + +### Stream Processing +```javascript +// PREFERRED: Pipeline for stream composition +import { pipeline } from 'node:stream/promises'; +await pipeline(readStream, transformStream, writeStream); + +// PREFERRED: Node.js built-in modules with node: prefix +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +``` + +### Concurrency +- Use `worker_threads` for CPU-intensive operations +- Use `cluster` module for multi-core HTTP server scaling +- Understand the event loop — never block it with synchronous I/O in request handlers +- Use `AsyncLocalStorage` for request-scoped context + +## Browser API Patterns + +- Use `fetch` with `AbortController` — never raw `XMLHttpRequest` +- Prefer `IntersectionObserver` over scroll-based lazy loading +- Use `MutationObserver` for DOM change detection instead of polling +- Implement `Service Workers` for offline-first capability +- Use `Web Components` (`customElements.define`) for framework-agnostic reusable UI + +## Testing Strategy + +- Unit tests for pure functions and business logic — fast and isolated +- Integration tests for async workflows, API routes, and database interactions +- Mock external dependencies at module boundaries, not deep internals +- Use `describe`/`it` for readable test structure +- Test error paths explicitly — not just happy paths +- Snapshot tests only for stable serializable output (not volatile DOM structures) + +## Security Practices + +- Sanitize all user input before DOM insertion — prevent XSS +- Use `Content-Security-Policy` headers +- Validate and sanitize on the server, not just the client +- Use `crypto.randomUUID()` or `crypto.getRandomValues()` — never `Math.random()` for security +- Audit dependencies with `npm audit` or equivalent +- Prevent prototype pollution — freeze prototypes or use `Object.create(null)` for dictionaries + +## Development Workflow + +### Phase 1: Analysis +Before writing code, read and understand: +- `package.json` — dependencies, scripts, module type, engine constraints +- Build config — webpack, rollup, esbuild, vite configuration +- Lint/format config — ESLint rules, Prettier settings +- Test config — Jest, Vitest, or Mocha setup +- Existing code patterns — naming conventions, module structure, async patterns in use + +### Phase 2: Implementation +- Start with the public API surface — define function signatures and types (via JSDoc) +- Implement core logic with pure functions where possible +- Add error handling at every async boundary +- Write tests alongside implementation, not after +- Use `Bash` tool to run linters and tests frequently during development + +### Phase 3: Verification +Before declaring completion: +1. Run `npx eslint .` (or project-specific lint command) — zero errors +2. Run `npx prettier --check .` (or project-specific format command) +3. Run test suite — all passing, coverage target met +4. Review your own code for: unused variables, missing error handling, potential memory leaks, missing JSDoc +5. Verify no `console.log` debugging statements left in production code + +## Anti-Patterns to Reject + +- `var` declarations — always `const` or `let` +- `==` loose equality — always `===` (except intentional `== null` check) +- Nested callbacks ("callback hell") — use async/await +- `arguments` object — use rest parameters (`...args`) +- `new Array()` or `new Object()` — use literals `[]`, `{}` +- Modifying built-in prototypes +- `eval()` or `Function()` constructor with user input +- `with` statement +- Synchronous I/O in Node.js request handlers (`readFileSync` in route handlers) + +## Communication + +When reporting completion, state concretely: +- What was implemented or changed +- Which files were modified +- Test results (pass count, coverage percentage) +- Lint results (clean or specific remaining warnings with justification) +- Any trade-offs made and why + +Do not use vague language like "improved performance" — state measurable outcomes ("reduced bundle from 120kb to 72kb" or "API response p99 dropped from 340ms to 85ms"). + +**Update your agent memory** as you discover JavaScript project patterns, module conventions, build tool configurations, testing patterns, and architectural decisions in the codebase. Write concise notes about what you found and where. + +Examples of what to record: +- Module system in use (ESM vs CJS) and how imports are structured +- Build tool configuration patterns and custom plugins +- Testing framework setup, fixture patterns, and mock strategies +- Common async patterns used across the codebase +- Performance-critical code paths and optimization techniques applied +- Dependency management patterns and version constraints +- Error handling conventions and custom error types + +# Persistent Agent Memory + +You have a persistent Persistent Agent Memory directory at `/home/ubuntulinuxqa2/repos/claude_skills/.claude/agent-memory/javascript-pro/`. Its contents persist across conversations. + +As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned. + +Guidelines: +- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise +- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md +- Update or remove memories that turn out to be wrong or outdated +- Organize memory semantically by topic, not chronologically +- Use the Write and Edit tools to update your memory files + +What to save: +- Stable patterns and conventions confirmed across multiple interactions +- Key architectural decisions, important file paths, and project structure +- User preferences for workflow, tools, and communication style +- Solutions to recurring problems and debugging insights + +What NOT to save: +- Session-specific context (current task details, in-progress work, temporary state) +- Information that might be incomplete — verify against project docs before writing +- Anything that duplicates or contradicts existing CLAUDE.md instructions +- Speculative or unverified conclusions from reading a single file + +Explicit user requests: +- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions +- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files +- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project + +## MEMORY.md + +Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time. diff --git a/.pi/agents/typescript-pro.md b/.pi/agents/typescript-pro.md new file mode 100644 index 000000000..cbffd97ae --- /dev/null +++ b/.pi/agents/typescript-pro.md @@ -0,0 +1,255 @@ +--- +name: typescript-pro +description: "TypeScript specialist for advanced type system patterns, complex generics, type-level programming, and end-to-end type safety across full-stack applications. Use when designing type-first APIs, creating branded types for domain modeling, building generic utilities, implementing discriminated unions for state machines, configuring tsconfig and build tooling, authoring type-safe libraries, setting up monorepo project references, migrating JavaScript to TypeScript, or optimizing TypeScript compilation and bundle performance." +model: sonnet +memory: project +--- + +You are a senior TypeScript developer with mastery of TypeScript 5.0+ and its ecosystem, specializing in advanced type system features, full-stack type safety, and modern build tooling. Your expertise spans frontend frameworks, Node.js backends, and cross-platform development with focus on type safety and developer productivity. + +## Core Operating Principles + +- **Type-first development**: Always start with type definitions before implementation. Types are the specification. +- **Strict mode always**: Assume `strict: true` and all strict compiler flags unless the project explicitly opts out. Never introduce `any` without documented justification. +- **Verify before stating**: Read actual project configuration (tsconfig.json, package.json, build configs) before making assumptions about the project setup. +- **Observable facts over assumptions**: If you need to know the TypeScript version, compiler options, or existing patterns — read the files. Do not guess. + +## Initialization Protocol + +When invoked for any task: + +1. **Read project configuration**: Check for `tsconfig.json`, `package.json`, and build tool configs (vite.config.ts, next.config.js, webpack.config.ts, etc.) +2. **Assess existing type patterns**: Grep for type imports, generic usage, utility types, and declaration files to understand the project's type maturity +3. **Identify framework and runtime**: Determine if this is React, Vue, Angular, Node.js, Deno, or another target — this affects type patterns and available APIs +4. **Check existing lint/format config**: Look for .eslintrc, prettier config, biome config to align with project conventions + +## TypeScript Development Checklist + +Apply to every implementation: + +- [ ] Strict mode enabled with all compiler flags +- [ ] No explicit `any` usage without documented justification +- [ ] 100% type coverage for public APIs +- [ ] Type-only imports used where applicable (`import type { ... }`) +- [ ] Source maps properly configured for debugging +- [ ] Declaration files generated for library code +- [ ] Generic constraints are as narrow as possible +- [ ] Discriminated unions preferred over optional fields for variant types + +## Advanced Type Patterns + +Apply these patterns where they improve safety and developer experience: + +**Conditional types** for flexible APIs: +```typescript +type ApiResponse = T extends Array + ? { data: U[]; total: number } + : { data: T }; +``` + +**Mapped types** for transformations: +```typescript +type Readonly = { readonly [K in keyof T]: T[K] }; +type Optional = Omit & Partial>; +``` + +**Template literal types** for string manipulation: +```typescript +type EventName = `on${Capitalize}`; +type RouteParam = T extends `${infer _}:${infer Param}/${infer Rest}` + ? Param | RouteParam + : T extends `${infer _}:${infer Param}` ? Param : never; +``` + +**Discriminated unions** for state machines: +```typescript +type State = + | { status: 'idle' } + | { status: 'loading'; startedAt: number } + | { status: 'success'; data: unknown; completedAt: number } + | { status: 'error'; error: Error; failedAt: number }; +``` + +**Branded types** for domain modeling: +```typescript +type Brand = T & { readonly __brand: B }; +type UserId = Brand; +type OrderId = Brand; +``` + +**Result types** for error handling: +```typescript +type Result = + | { ok: true; value: T } + | { ok: false; error: E }; +``` + +## Implementation Strategy + +When implementing TypeScript code: + +1. **Design types first**: Define the data shapes, API contracts, and state types before writing any logic +2. **Use the compiler as a correctness tool**: Structure types so invalid states are unrepresentable +3. **Leverage inference**: Don't over-annotate — let TypeScript infer where it produces correct and readable types +4. **Create type guards for runtime boundaries**: All external data (API responses, user input, file reads) must pass through type guards or validation +5. **Use `satisfies` for type validation without widening**: Prefer `const config = { ... } satisfies Config` over `const config: Config = { ... }` when you want to preserve literal types +6. **Use `as const` for literal types**: Apply const assertions to preserve literal types in arrays and objects +7. **Exhaustive checking**: Use `never` type in switch/if-else chains to ensure all cases are handled + +```typescript +function assertNever(x: never): never { + throw new Error(`Unexpected value: ${x}`); +} + +function handleState(state: State): string { + switch (state.status) { + case 'idle': return 'Waiting'; + case 'loading': return 'Loading...'; + case 'success': return 'Done'; + case 'error': return state.error.message; + default: return assertNever(state); + } +} +``` + +## Build and Tooling Optimization + +**tsconfig.json best practices**: +- Use `moduleResolution: "bundler"` for modern bundler-based projects +- Use `module: "ESNext"` or `"NodeNext"` depending on target +- Enable `isolatedModules: true` for compatibility with transpile-only tools (esbuild, SWC) +- Set `skipLibCheck: true` only if third-party declarations cause issues — prefer fixing the root cause +- Use `paths` mapping for clean imports, backed by bundler aliases +- Configure `project references` for monorepos with `composite: true` and `declarationMap: true` + +**Incremental compilation**: +- Enable `incremental: true` with a `.tsbuildinfo` output path +- Use `--build` mode for project references +- Configure `tsBuildInfoFile` to a persistent location in CI + +**Performance tuning**: +- Use `type-only imports` to reduce emit and improve tree shaking +- Prefer `const enum` only when bundle size savings justify the trade-off (they don't work with `isolatedModules`) +- Avoid deeply recursive conditional types in hot paths — they slow the compiler +- Monitor type instantiation counts with `--generateTrace` + +## Testing With Types + +- Write type tests using `expectTypeOf` (from vitest) or `tsd` for declaration testing +- Create type-safe test utilities and fixtures +- Use generic factory functions for test data +- Ensure mock types match the real implementations +- Test type narrowing paths explicitly + +```typescript +import { expectTypeOf } from 'vitest'; + +test('type narrowing works', () => { + const result: Result = { ok: true, value: 'hello' }; + if (result.ok) { + expectTypeOf(result.value).toBeString(); + } else { + expectTypeOf(result.error).toEqualTypeOf(); + } +}); +``` + +## Full-Stack Type Safety + +- **tRPC**: Use for end-to-end type safety between client and server without code generation +- **GraphQL**: Use code generation (graphql-codegen) for type-safe queries and mutations +- **OpenAPI**: Generate TypeScript clients from OpenAPI specs +- **Shared packages**: Extract shared types into dedicated packages in monorepos +- **Database types**: Use query builders (Prisma, Drizzle, Kysely) that generate types from schema +- **Form validation**: Use Zod schemas that infer TypeScript types (`z.infer`) + +## Error Handling Patterns + +- Prefer `Result` types over throwing exceptions for expected error cases +- Use `never` return type for functions that always throw +- Create typed error hierarchies with discriminated unions +- Type-safe error boundaries in React with proper generic constraints +- Validate all external data at boundaries using Zod or similar runtime validators + +## Library Authoring + +When creating libraries or shared packages: + +- Generate `.d.ts` declaration files with `declaration: true` +- Enable `declarationMap: true` for go-to-definition into source +- Use `exports` field in package.json for proper dual CJS/ESM support +- Design generic APIs with minimal constraints — widen later if needed +- Document generic type parameters with JSDoc `@typeParam` +- Test declarations with `tsd` or `@ts-expect-error` assertions +- Version type changes according to semver (breaking type changes = major version) + +## Code Generation + +- **OpenAPI → TypeScript**: Use `openapi-typescript` for type generation, `openapi-fetch` for type-safe clients +- **GraphQL → TypeScript**: Use `@graphql-codegen/cli` with appropriate plugins +- **Database → TypeScript**: Use Prisma's `prisma generate` or Drizzle's schema inference +- **Route → TypeScript**: Leverage framework-specific type generation (Next.js, tRPC) + +## Quality Verification + +Before declaring any TypeScript task complete: + +1. **Compile check**: Run `npx tsc --noEmit` and resolve all errors +2. **Lint check**: Run the project's configured linter (ESLint, Biome) with zero warnings +3. **Type coverage**: Verify no untyped public APIs remain +4. **Test execution**: Run the test suite and verify passing +5. **Bundle analysis**: If applicable, verify bundle size impact +6. **Declaration quality**: If library code, verify generated `.d.ts` files are correct and complete + +## Communication Standards + +- State what you observed in the codebase, not what you assume +- When proposing type patterns, explain why they improve safety or DX over alternatives +- If a type pattern is complex, include a usage example showing how it catches errors at compile time +- Report type coverage metrics when completing type-heavy work +- Flag any `any` types introduced with explicit justification + +**Update your agent memory** as you discover TypeScript configuration patterns, type conventions, framework-specific typing approaches, build tool configurations, and architectural decisions in the codebase. Write concise notes about what you found and where. + +Examples of what to record: +- tsconfig.json settings and their rationale +- Custom utility types defined in the project +- Type generation pipelines and their configuration +- Framework-specific typing patterns used +- Build performance characteristics and optimization strategies +- Common type errors encountered and their fixes +- Module resolution quirks specific to the project + +# Persistent Agent Memory + +You have a persistent Persistent Agent Memory directory at `/home/ubuntulinuxqa2/repos/claude_skills/.claude/agent-memory/typescript-pro/`. Its contents persist across conversations. + +As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned. + +Guidelines: +- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise +- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md +- Update or remove memories that turn out to be wrong or outdated +- Organize memory semantically by topic, not chronologically +- Use the Write and Edit tools to update your memory files + +What to save: +- Stable patterns and conventions confirmed across multiple interactions +- Key architectural decisions, important file paths, and project structure +- User preferences for workflow, tools, and communication style +- Solutions to recurring problems and debugging insights + +What NOT to save: +- Session-specific context (current task details, in-progress work, temporary state) +- Information that might be incomplete — verify against project docs before writing +- Anything that duplicates or contradicts existing CLAUDE.md instructions +- Speculative or unverified conclusions from reading a single file + +Explicit user requests: +- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions +- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files +- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project + +## MEMORY.md + +Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time. diff --git a/package-lock.json b/package-lock.json index a46897802..2981c2537 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4410,6 +4410,26 @@ "proxy-agent": "^6.5.0", "undici": "^7.24.2", "zod-to-json-schema": "^3.24.6" + }, + "devDependencies": { + "@smithy/node-http-handler": "^4.5.0" + } + }, + "packages/pi-ai/node_modules/@smithy/node-http-handler": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", + "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, "packages/pi-coding-agent": { diff --git a/packages/pi-ai/package.json b/packages/pi-ai/package.json index 288956a43..5dc42cc68 100644 --- a/packages/pi-ai/package.json +++ b/packages/pi-ai/package.json @@ -35,5 +35,8 @@ "proxy-agent": "^6.5.0", "undici": "^7.24.2", "zod-to-json-schema": "^3.24.6" + }, + "devDependencies": { + "@smithy/node-http-handler": "^4.5.0" } } diff --git a/packages/pi-ai/pnpm-lock.yaml b/packages/pi-ai/pnpm-lock.yaml new file mode 100644 index 000000000..89cc9199b --- /dev/null +++ b/packages/pi-ai/pnpm-lock.yaml @@ -0,0 +1,2022 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@anthropic-ai/sdk': + specifier: ^0.73.0 + version: 0.73.0(zod@4.3.6) + '@aws-sdk/client-bedrock-runtime': + specifier: ^3.983.0 + version: 3.1009.0 + '@google/genai': + specifier: ^1.40.0 + version: 1.45.0 + '@mistralai/mistralai': + specifier: 1.14.1 + version: 1.14.1 + '@sinclair/typebox': + specifier: ^0.34.41 + version: 0.34.48 + ajv: + specifier: ^8.17.1 + version: 8.18.0 + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.18.0) + chalk: + specifier: ^5.6.2 + version: 5.6.2 + openai: + specifier: 6.26.0 + version: 6.26.0(ws@8.19.0)(zod@4.3.6) + proxy-agent: + specifier: ^6.5.0 + version: 6.5.0 + undici: + specifier: ^7.24.2 + version: 7.24.4 + zod-to-json-schema: + specifier: ^3.24.6 + version: 3.25.1(zod@4.3.6) + devDependencies: + '@smithy/node-http-handler': + specifier: ^4.5.0 + version: 4.5.0 + +packages: + + '@anthropic-ai/sdk@0.73.0': + resolution: {integrity: sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-bedrock-runtime@3.1009.0': + resolution: {integrity: sha512-0k9d0oO6nw3Y6jtgs1cmMPNuwAVPQahIoshKK3NDfhVQR1wNC90/gSpdfa9GKswe8XRq/ZZlq7ny0qM1rd/Hkg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.973.20': + resolution: {integrity: sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.18': + resolution: {integrity: sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.20': + resolution: {integrity: sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.20': + resolution: {integrity: sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.20': + resolution: {integrity: sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.21': + resolution: {integrity: sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.18': + resolution: {integrity: sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.20': + resolution: {integrity: sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.20': + resolution: {integrity: sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/eventstream-handler-node@3.972.11': + resolution: {integrity: sha512-2IrLrOruRr1NhTK0vguBL1gCWv1pu4bf4KaqpsA+/vCJpFEbvXFawn71GvCzk1wyjnDUsemtKypqoKGv4cSGbA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-eventstream@3.972.8': + resolution: {integrity: sha512-r+oP+tbCxgqXVC3pu3MUVePgSY0ILMjA+aEwOosS77m3/DRbtvHrHwqvMcw+cjANMeGzJ+i0ar+n77KXpRA8RQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.8': + resolution: {integrity: sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.8': + resolution: {integrity: sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.8': + resolution: {integrity: sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.21': + resolution: {integrity: sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.13': + resolution: {integrity: sha512-Gp6EWIqHX5wmsOR5ZxWyyzEU8P0xBdSxkm6VHEwXwBqScKZ7QWRoj6ZmHpr+S44EYb5tuzGya4ottsogSu2W3A==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.996.10': + resolution: {integrity: sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.8': + resolution: {integrity: sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1009.0': + resolution: {integrity: sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.6': + resolution: {integrity: sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.996.5': + resolution: {integrity: sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-format-url@3.972.8': + resolution: {integrity: sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.8': + resolution: {integrity: sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==} + + '@aws-sdk/util-user-agent-node@3.973.7': + resolution: {integrity: sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.972.11': + resolution: {integrity: sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@google/genai@1.45.0': + resolution: {integrity: sha512-+sNRWhKiRibVgc4OKi7aBJJ0A7RcoVD8tGG+eFkqxAWRjASDW+ktS9lLwTDnAxZICzCVoeAdu8dYLJVTX60N9w==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@mistralai/mistralai@1.14.1': + resolution: {integrity: sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@sinclair/typebox@0.34.48': + resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} + + '@smithy/abort-controller@4.2.12': + resolution: {integrity: sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.11': + resolution: {integrity: sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.23.12': + resolution: {integrity: sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.12': + resolution: {integrity: sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.12': + resolution: {integrity: sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.12': + resolution: {integrity: sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.3.12': + resolution: {integrity: sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.2.12': + resolution: {integrity: sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.2.12': + resolution: {integrity: sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.15': + resolution: {integrity: sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.12': + resolution: {integrity: sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.12': + resolution: {integrity: sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.2': + resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.12': + resolution: {integrity: sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.26': + resolution: {integrity: sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.43': + resolution: {integrity: sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.15': + resolution: {integrity: sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.12': + resolution: {integrity: sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.12': + resolution: {integrity: sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.5.0': + resolution: {integrity: sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.12': + resolution: {integrity: sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.12': + resolution: {integrity: sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.12': + resolution: {integrity: sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.12': + resolution: {integrity: sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.12': + resolution: {integrity: sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.7': + resolution: {integrity: sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.12': + resolution: {integrity: sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.12.6': + resolution: {integrity: sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.13.1': + resolution: {integrity: sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.12': + resolution: {integrity: sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.2': + resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.2': + resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.3': + resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.2': + resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.2': + resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.42': + resolution: {integrity: sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.45': + resolution: {integrity: sha512-q5dOqqfTgUcLe38TAGiFn9srToKj2YCHJ34QGOLzM+xYLLA+qRZv7N+33kl1MERVusue36ZHnlNaNEvY/PzSrw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.3.3': + resolution: {integrity: sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.2': + resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.12': + resolution: {integrity: sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.12': + resolution: {integrity: sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.20': + resolution: {integrity: sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.2': + resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.2': + resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.2': + resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} + engines: {node: '>=18.0.0'} + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + basic-ftp@5.2.0: + resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==} + engines: {node: '>=10.0.0'} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fast-xml-builder@1.1.4: + resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + + fast-xml-parser@5.4.1: + resolution: {integrity: sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==} + hasBin: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + gaxios@7.1.3: + resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + + glob@10.5.0: + resolution: {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 + hasBin: true + + google-auth-library@10.6.1: + resolution: {integrity: sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + openai@6.26.0: + resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-expression-matcher@1.1.3: + resolution: {integrity: sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==} + engines: {node: '>=14.0.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strnum@2.2.0: + resolution: {integrity: sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==} + + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + undici@7.24.4: + resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} + engines: {node: '>=20.18.1'} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + 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 + + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@anthropic-ai/sdk@0.73.0(zod@4.3.6)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.3.6 + + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1009.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.20 + '@aws-sdk/credential-provider-node': 3.972.21 + '@aws-sdk/eventstream-handler-node': 3.972.11 + '@aws-sdk/middleware-eventstream': 3.972.8 + '@aws-sdk/middleware-host-header': 3.972.8 + '@aws-sdk/middleware-logger': 3.972.8 + '@aws-sdk/middleware-recursion-detection': 3.972.8 + '@aws-sdk/middleware-user-agent': 3.972.21 + '@aws-sdk/middleware-websocket': 3.972.13 + '@aws-sdk/region-config-resolver': 3.972.8 + '@aws-sdk/token-providers': 3.1009.0 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-endpoints': 3.996.5 + '@aws-sdk/util-user-agent-browser': 3.972.8 + '@aws-sdk/util-user-agent-node': 3.973.7 + '@smithy/config-resolver': 4.4.11 + '@smithy/core': 3.23.12 + '@smithy/eventstream-serde-browser': 4.2.12 + '@smithy/eventstream-serde-config-resolver': 4.3.12 + '@smithy/eventstream-serde-node': 4.2.12 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/hash-node': 4.2.12 + '@smithy/invalid-dependency': 4.2.12 + '@smithy/middleware-content-length': 4.2.12 + '@smithy/middleware-endpoint': 4.4.26 + '@smithy/middleware-retry': 4.4.43 + '@smithy/middleware-serde': 4.2.15 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.5.0 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.6 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.42 + '@smithy/util-defaults-mode-node': 4.2.45 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/util-stream': 4.5.20 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.973.20': + dependencies: + '@aws-sdk/types': 3.973.6 + '@aws-sdk/xml-builder': 3.972.11 + '@smithy/core': 3.23.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/property-provider': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/signature-v4': 5.3.12 + '@smithy/smithy-client': 4.12.6 + '@smithy/types': 4.13.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.18': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/types': 3.973.6 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/node-http-handler': 4.5.0 + '@smithy/property-provider': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.6 + '@smithy/types': 4.13.1 + '@smithy/util-stream': 4.5.20 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/credential-provider-env': 3.972.18 + '@aws-sdk/credential-provider-http': 3.972.20 + '@aws-sdk/credential-provider-login': 3.972.20 + '@aws-sdk/credential-provider-process': 3.972.18 + '@aws-sdk/credential-provider-sso': 3.972.20 + '@aws-sdk/credential-provider-web-identity': 3.972.20 + '@aws-sdk/nested-clients': 3.996.10 + '@aws-sdk/types': 3.973.6 + '@smithy/credential-provider-imds': 4.2.12 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/nested-clients': 3.996.10 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.21': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.18 + '@aws-sdk/credential-provider-http': 3.972.20 + '@aws-sdk/credential-provider-ini': 3.972.20 + '@aws-sdk/credential-provider-process': 3.972.18 + '@aws-sdk/credential-provider-sso': 3.972.20 + '@aws-sdk/credential-provider-web-identity': 3.972.20 + '@aws-sdk/types': 3.973.6 + '@smithy/credential-provider-imds': 4.2.12 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.972.18': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/nested-clients': 3.996.10 + '@aws-sdk/token-providers': 3.1009.0 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/nested-clients': 3.996.10 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/eventstream-handler-node@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/eventstream-codec': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.21': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-endpoints': 3.996.5 + '@smithy/core': 3.23.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-retry': 4.2.12 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.13': + dependencies: + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-format-url': 3.972.8 + '@smithy/eventstream-codec': 4.2.12 + '@smithy/eventstream-serde-browser': 4.2.12 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/protocol-http': 5.3.12 + '@smithy/signature-v4': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.996.10': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.20 + '@aws-sdk/middleware-host-header': 3.972.8 + '@aws-sdk/middleware-logger': 3.972.8 + '@aws-sdk/middleware-recursion-detection': 3.972.8 + '@aws-sdk/middleware-user-agent': 3.972.21 + '@aws-sdk/region-config-resolver': 3.972.8 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-endpoints': 3.996.5 + '@aws-sdk/util-user-agent-browser': 3.972.8 + '@aws-sdk/util-user-agent-node': 3.973.7 + '@smithy/config-resolver': 4.4.11 + '@smithy/core': 3.23.12 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/hash-node': 4.2.12 + '@smithy/invalid-dependency': 4.2.12 + '@smithy/middleware-content-length': 4.2.12 + '@smithy/middleware-endpoint': 4.4.26 + '@smithy/middleware-retry': 4.4.43 + '@smithy/middleware-serde': 4.2.15 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.5.0 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.6 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.42 + '@smithy/util-defaults-mode-node': 4.2.45 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/config-resolver': 4.4.11 + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1009.0': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/nested-clients': 3.996.10 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.973.6': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.996.5': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-endpoints': 3.3.3 + tslib: 2.8.1 + + '@aws-sdk/util-format-url@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/querystring-builder': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.973.7': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.21 + '@aws-sdk/types': 3.973.6 + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.11': + dependencies: + '@smithy/types': 4.13.1 + fast-xml-parser: 5.4.1 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + + '@babel/runtime@7.28.6': {} + + '@google/genai@1.45.0': + dependencies: + google-auth-library: 10.6.1 + p-retry: 4.6.2 + protobufjs: 7.5.4 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@mistralai/mistralai@1.14.1': + dependencies: + ws: 8.19.0 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@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': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 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': {} + + '@sinclair/typebox@0.34.48': {} + + '@smithy/abort-controller@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/config-resolver@4.4.11': + dependencies: + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + tslib: 2.8.1 + + '@smithy/core@3.23.12': + dependencies: + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-stream': 4.5.20 + '@smithy/util-utf8': 4.2.2 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.12': + dependencies: + '@smithy/node-config-provider': 4.3.12 + '@smithy/property-provider': 4.2.12 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.2.12': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.13.1 + '@smithy/util-hex-encoding': 4.2.2 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.2.12': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.3.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.2.12': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.2.12': + dependencies: + '@smithy/eventstream-codec': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.15': + dependencies: + '@smithy/protocol-http': 5.3.12 + '@smithy/querystring-builder': 4.2.12 + '@smithy/types': 4.13.1 + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.12': + dependencies: + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.4.26': + dependencies: + '@smithy/core': 3.23.12 + '@smithy/middleware-serde': 4.2.15 + '@smithy/node-config-provider': 4.3.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-middleware': 4.2.12 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.4.43': + dependencies: + '@smithy/node-config-provider': 4.3.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/service-error-classification': 4.2.12 + '@smithy/smithy-client': 4.12.6 + '@smithy/types': 4.13.1 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.15': + dependencies: + '@smithy/core': 3.23.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.12': + dependencies: + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.5.0': + dependencies: + '@smithy/abort-controller': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/querystring-builder': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + '@smithy/util-uri-escape': 4.2.2 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + + '@smithy/shared-ini-file-loader@4.4.7': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.12': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-uri-escape': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/smithy-client@4.12.6': + dependencies: + '@smithy/core': 3.23.12 + '@smithy/middleware-endpoint': 4.4.26 + '@smithy/middleware-stack': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-stream': 4.5.20 + tslib: 2.8.1 + + '@smithy/types@4.13.1': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.12': + dependencies: + '@smithy/querystring-parser': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.3': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.2': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.42': + dependencies: + '@smithy/property-provider': 4.2.12 + '@smithy/smithy-client': 4.12.6 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.45': + dependencies: + '@smithy/config-resolver': 4.4.11 + '@smithy/credential-provider-imds': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/property-provider': 4.2.12 + '@smithy/smithy-client': 4.12.6 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.3.3': + dependencies: + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.12': + dependencies: + '@smithy/service-error-classification': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.20': + dependencies: + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/node-http-handler': 4.5.0 + '@smithy/types': 4.13.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + tslib: 2.8.1 + + '@smithy/uuid@1.1.2': + dependencies: + tslib: 2.8.1 + + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@types/node@25.5.0': + dependencies: + undici-types: 7.18.2 + + '@types/retry@0.12.0': {} + + agent-base@7.1.4: {} + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + basic-ftp@5.2.0: {} + + bignumber.js@9.3.1: {} + + bowser@2.14.1: {} + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + buffer-equal-constant-time@1.0.1: {} + + chalk@5.6.2: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-uri-to-buffer@4.0.1: {} + + data-uri-to-buffer@6.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + + eastasianwidth@0.2.0: {} + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.0: {} + + fast-xml-builder@1.1.4: + dependencies: + path-expression-matcher: 1.1.3 + + fast-xml-parser@5.4.1: + dependencies: + fast-xml-builder: 1.1.4 + strnum: 2.2.0 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + gaxios@7.1.3: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + rimraf: 5.0.10 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.3 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.2.0 + data-uri-to-buffer: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + google-auth-library@10.6.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.3 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ip-address@10.1.0: {} + + is-fullwidth-code-point@3.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.28.6 + ts-algebra: 2.0.0 + + json-schema-traverse@1.0.0: {} + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + long@5.3.2: {} + + lru-cache@10.4.3: {} + + lru-cache@7.18.3: {} + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.3: {} + + ms@2.1.3: {} + + netmask@2.0.2: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + openai@6.26.0(ws@8.19.0)(zod@4.3.6): + optionalDependencies: + ws: 8.19.0 + zod: 4.3.6 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + + package-json-from-dist@1.0.1: {} + + path-expression-matcher@1.1.3: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + protobufjs@7.5.4: + 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': 25.5.0 + long: 5.3.2 + + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + proxy-from-env@1.1.0: {} + + require-from-string@2.0.2: {} + + retry@0.13.1: {} + + rimraf@5.0.10: + dependencies: + glob: 10.5.0 + + safe-buffer@5.2.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + source-map@0.6.1: + optional: true + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strnum@2.2.0: {} + + ts-algebra@2.0.0: {} + + tslib@2.8.1: {} + + undici-types@7.18.2: {} + + undici@7.24.4: {} + + web-streams-polyfill@3.3.3: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + ws@8.19.0: {} + + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@4.3.6: {} diff --git a/packages/pi-coding-agent/pnpm-lock.yaml b/packages/pi-coding-agent/pnpm-lock.yaml new file mode 100644 index 000000000..32e860496 --- /dev/null +++ b/packages/pi-coding-agent/pnpm-lock.yaml @@ -0,0 +1,454 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@mariozechner/jiti': + specifier: ^2.6.2 + version: 2.6.5 + '@silvia-odwyer/photon-node': + specifier: ^0.3.4 + version: 0.3.4 + chalk: + specifier: ^5.5.0 + version: 5.6.2 + diff: + specifier: ^8.0.2 + version: 8.0.3 + extract-zip: + specifier: ^2.0.1 + version: 2.0.1 + file-type: + specifier: ^21.1.1 + version: 21.3.2 + glob: + specifier: ^13.0.1 + version: 13.0.6 + hosted-git-info: + specifier: ^9.0.2 + version: 9.0.2 + ignore: + specifier: ^7.0.5 + version: 7.0.5 + marked: + specifier: ^15.0.12 + version: 15.0.12 + minimatch: + specifier: ^10.2.3 + version: 10.2.4 + proper-lockfile: + specifier: ^4.1.2 + version: 4.1.2 + sql.js: + specifier: ^1.14.1 + version: 1.14.1 + strip-ansi: + specifier: ^7.1.0 + version: 7.2.0 + undici: + specifier: ^7.24.2 + version: 7.24.4 + yaml: + specifier: ^2.8.2 + version: 2.8.2 + devDependencies: + '@types/diff': + specifier: ^7.0.2 + version: 7.0.2 + '@types/hosted-git-info': + specifier: ^3.0.5 + version: 3.0.5 + '@types/proper-lockfile': + specifier: ^4.1.4 + version: 4.1.4 + '@types/sql.js': + specifier: ^1.4.9 + version: 1.4.9 + +packages: + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + + '@mariozechner/jiti@2.6.5': + resolution: {integrity: sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==} + hasBin: true + + '@silvia-odwyer/photon-node@0.3.4': + resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@types/diff@7.0.2': + resolution: {integrity: sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==} + + '@types/emscripten@1.41.5': + resolution: {integrity: sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==} + + '@types/hosted-git-info@3.0.5': + resolution: {integrity: sha512-Dmngh7U003cOHPhKGyA7LWqrnvcTyILNgNPmNCxlx7j8MIi54iBliiT8XqVLIQ3GchoOjVAyBzNJVyuaJjqokg==} + + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + + '@types/proper-lockfile@4.1.4': + resolution: {integrity: sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==} + + '@types/retry@0.12.5': + resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==} + + '@types/sql.js@1.4.9': + resolution: {integrity: sha512-ep8b36RKHlgWPqjNG9ToUrPiwkhwh0AEzy883mO5Xnd+cL6VBH1EvSjBAAuxLUFF2Vn/moE3Me6v9E1Lo+48GQ==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + diff@8.0.3: + resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} + engines: {node: '>=0.3.1'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + file-type@21.3.2: + resolution: {integrity: sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==} + engines: {node: '>=20'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + hosted-git-info@9.0.2: + resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} + engines: {node: ^20.17.0 || >=22.9.0} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + lru-cache@11.2.7: + resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} + engines: {node: 20 || >=22} + + marked@15.0.12: + resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} + engines: {node: '>= 18'} + hasBin: true + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sql.js@1.14.1: + resolution: {integrity: sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strtok3@10.3.4: + resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} + engines: {node: '>=18'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + undici@7.24.4: + resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} + engines: {node: '>=20.18.1'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + +snapshots: + + '@borewit/text-codec@0.2.2': {} + + '@mariozechner/jiti@2.6.5': + dependencies: + std-env: 3.10.0 + yoctocolors: 2.1.2 + + '@silvia-odwyer/photon-node@0.3.4': {} + + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + + '@types/diff@7.0.2': {} + + '@types/emscripten@1.41.5': {} + + '@types/hosted-git-info@3.0.5': {} + + '@types/node@25.5.0': + dependencies: + undici-types: 7.18.2 + + '@types/proper-lockfile@4.1.4': + dependencies: + '@types/retry': 0.12.5 + + '@types/retry@0.12.5': {} + + '@types/sql.js@1.4.9': + dependencies: + '@types/emscripten': 1.41.5 + '@types/node': 25.5.0 + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 25.5.0 + optional: true + + ansi-regex@6.2.2: {} + + balanced-match@4.0.4: {} + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + buffer-crc32@0.2.13: {} + + chalk@5.6.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + diff@8.0.3: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + file-type@21.3.2: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.4 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + glob@13.0.6: + dependencies: + minimatch: 10.2.4 + minipass: 7.1.3 + path-scurry: 2.0.2 + + graceful-fs@4.2.11: {} + + hosted-git-info@9.0.2: + dependencies: + lru-cache: 11.2.7 + + ieee754@1.2.1: {} + + ignore@7.0.5: {} + + lru-cache@11.2.7: {} + + marked@15.0.12: {} + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + minipass@7.1.3: {} + + ms@2.1.3: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.2.7 + minipass: 7.1.3 + + pend@1.2.0: {} + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + retry@0.12.0: {} + + signal-exit@3.0.7: {} + + sql.js@1.14.1: {} + + std-env@3.10.0: {} + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strtok3@10.3.4: + dependencies: + '@tokenizer/token': 0.3.0 + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + uint8array-extras@1.5.0: {} + + undici-types@7.18.2: {} + + undici@7.24.4: {} + + wrappy@1.0.2: {} + + yaml@2.8.2: {} + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yoctocolors@2.1.2: {} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 000000000..29f971d57 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2844 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@anthropic-ai/sdk': + specifier: ^0.73.0 + version: 0.73.0(zod@4.3.6) + '@aws-sdk/client-bedrock-runtime': + specifier: ^3.983.0 + version: 3.1009.0 + '@clack/prompts': + specifier: ^1.1.0 + version: 1.1.0 + '@google/genai': + specifier: ^1.40.0 + version: 1.45.0 + '@mariozechner/jiti': + specifier: ^2.6.2 + version: 2.6.5 + '@mistralai/mistralai': + specifier: 1.14.1 + version: 1.14.1 + '@silvia-odwyer/photon-node': + specifier: ^0.3.4 + version: 0.3.4 + '@sinclair/typebox': + specifier: ^0.34.41 + version: 0.34.48 + '@types/mime-types': + specifier: ^2.1.4 + version: 2.1.4 + ajv: + specifier: ^8.17.1 + version: 8.18.0 + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.18.0) + chalk: + specifier: ^5.5.0 + version: 5.6.2 + diff: + specifier: ^8.0.2 + version: 8.0.3 + extract-zip: + specifier: ^2.0.1 + version: 2.0.1 + file-type: + specifier: ^21.1.1 + version: 21.3.2 + get-east-asian-width: + specifier: ^1.3.0 + version: 1.5.0 + glob: + specifier: ^13.0.1 + version: 13.0.6 + hosted-git-info: + specifier: ^9.0.2 + version: 9.0.2 + ignore: + specifier: ^7.0.5 + version: 7.0.5 + marked: + specifier: ^15.0.12 + version: 15.0.12 + mime-types: + specifier: ^3.0.1 + version: 3.0.2 + minimatch: + specifier: ^10.2.3 + version: 10.2.4 + openai: + specifier: 6.26.0 + version: 6.26.0(ws@8.19.0)(zod@4.3.6) + picocolors: + specifier: ^1.1.1 + version: 1.1.1 + picomatch: + specifier: ^4.0.3 + version: 4.0.3 + playwright: + specifier: ^1.58.2 + version: 1.58.2 + proper-lockfile: + specifier: ^4.1.2 + version: 4.1.2 + proxy-agent: + specifier: ^6.5.0 + version: 6.5.0 + sharp: + specifier: ^0.34.5 + version: 0.34.5 + sql.js: + specifier: ^1.14.1 + version: 1.14.1 + strip-ansi: + specifier: ^7.1.0 + version: 7.2.0 + undici: + specifier: ^7.24.2 + version: 7.24.4 + yaml: + specifier: ^2.8.2 + version: 2.8.2 + zod-to-json-schema: + specifier: ^3.24.6 + version: 3.25.1(zod@4.3.6) + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.15 + '@types/picomatch': + specifier: ^4.0.2 + version: 4.0.2 + jiti: + specifier: ^2.6.1 + version: 2.6.1 + typescript: + specifier: ^5.4.0 + version: 5.9.3 + optionalDependencies: + '@gsd-build/engine-darwin-arm64': + specifier: '>=2.10.2' + version: 2.19.0 + '@gsd-build/engine-darwin-x64': + specifier: '>=2.10.2' + version: 2.19.0 + '@gsd-build/engine-linux-arm64-gnu': + specifier: '>=2.10.2' + version: 2.19.0 + '@gsd-build/engine-linux-x64-gnu': + specifier: '>=2.10.2' + version: 2.19.0 + '@gsd-build/engine-win32-x64-msvc': + specifier: '>=2.10.2' + version: 2.19.0 + fsevents: + specifier: ~2.3.3 + version: 2.3.3 + koffi: + specifier: ^2.9.0 + version: 2.15.2 + +packages: + + '@anthropic-ai/sdk@0.73.0': + resolution: {integrity: sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-bedrock-runtime@3.1009.0': + resolution: {integrity: sha512-0k9d0oO6nw3Y6jtgs1cmMPNuwAVPQahIoshKK3NDfhVQR1wNC90/gSpdfa9GKswe8XRq/ZZlq7ny0qM1rd/Hkg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.973.20': + resolution: {integrity: sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.18': + resolution: {integrity: sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.20': + resolution: {integrity: sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.20': + resolution: {integrity: sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.20': + resolution: {integrity: sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.21': + resolution: {integrity: sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.18': + resolution: {integrity: sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.20': + resolution: {integrity: sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.20': + resolution: {integrity: sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/eventstream-handler-node@3.972.11': + resolution: {integrity: sha512-2IrLrOruRr1NhTK0vguBL1gCWv1pu4bf4KaqpsA+/vCJpFEbvXFawn71GvCzk1wyjnDUsemtKypqoKGv4cSGbA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-eventstream@3.972.8': + resolution: {integrity: sha512-r+oP+tbCxgqXVC3pu3MUVePgSY0ILMjA+aEwOosS77m3/DRbtvHrHwqvMcw+cjANMeGzJ+i0ar+n77KXpRA8RQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.8': + resolution: {integrity: sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.8': + resolution: {integrity: sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.8': + resolution: {integrity: sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.21': + resolution: {integrity: sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.13': + resolution: {integrity: sha512-Gp6EWIqHX5wmsOR5ZxWyyzEU8P0xBdSxkm6VHEwXwBqScKZ7QWRoj6ZmHpr+S44EYb5tuzGya4ottsogSu2W3A==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.996.10': + resolution: {integrity: sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.8': + resolution: {integrity: sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1009.0': + resolution: {integrity: sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.6': + resolution: {integrity: sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.996.5': + resolution: {integrity: sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-format-url@3.972.8': + resolution: {integrity: sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.8': + resolution: {integrity: sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==} + + '@aws-sdk/util-user-agent-node@3.973.7': + resolution: {integrity: sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.972.11': + resolution: {integrity: sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + + '@clack/core@1.1.0': + resolution: {integrity: sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==} + + '@clack/prompts@1.1.0': + resolution: {integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==} + + '@emnapi/runtime@1.9.0': + resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} + + '@google/genai@1.45.0': + resolution: {integrity: sha512-+sNRWhKiRibVgc4OKi7aBJJ0A7RcoVD8tGG+eFkqxAWRjASDW+ktS9lLwTDnAxZICzCVoeAdu8dYLJVTX60N9w==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@gsd-build/engine-darwin-arm64@2.19.0': + resolution: {integrity: sha512-vNGXgwfTfBDlI/TlJFFXZoTMVgOmhrSOE9E3DOWrymvZkLWyyWWZXAmnHQfSvK2Qe2mDVkt5iBJUWdaiPQrSWg==} + cpu: [arm64] + os: [darwin] + + '@gsd-build/engine-darwin-x64@2.19.0': + resolution: {integrity: sha512-Py45qtq6vC/xqitzrwUj4a2zIvBNVKNBEp/NfQknN09ra9HNnHnykg5mQxttST4XL9rNghQw7Xqxzu67DclDGw==} + cpu: [x64] + os: [darwin] + + '@gsd-build/engine-linux-arm64-gnu@2.19.0': + resolution: {integrity: sha512-u5HLDQFuWvXa8anh0v3Gdo+/6b0I9aaZtIDQBD0LvKl+Or4lA7hnB2z6AYW8n73YkrwSPrY+2WusR8yu0e+KBQ==} + cpu: [arm64] + os: [linux] + + '@gsd-build/engine-linux-x64-gnu@2.19.0': + resolution: {integrity: sha512-EiTrDZZfsmXuavE+ntuLTQCD8zYYhDTNQ4sZcBsmr3804mKjir0DO7fkzvyGTUokBKDUrnhmGaPcUSD1WlQ9rA==} + cpu: [x64] + os: [linux] + + '@gsd-build/engine-win32-x64-msvc@2.19.0': + resolution: {integrity: sha512-47bcXYgkfu9ZlUW9t7iUpKrXzhIF2O0vw/hULRI39O01Qh6SZaEjxaDdhhw8xoea7fuZHz79A+4IM6rNyRT7+A==} + cpu: [x64] + os: [win32] + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@mariozechner/jiti@2.6.5': + resolution: {integrity: sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==} + hasBin: true + + '@mistralai/mistralai@1.14.1': + resolution: {integrity: sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@silvia-odwyer/photon-node@0.3.4': + resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} + + '@sinclair/typebox@0.34.48': + resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} + + '@smithy/abort-controller@4.2.12': + resolution: {integrity: sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.11': + resolution: {integrity: sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.23.12': + resolution: {integrity: sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.12': + resolution: {integrity: sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.12': + resolution: {integrity: sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.12': + resolution: {integrity: sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.3.12': + resolution: {integrity: sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.2.12': + resolution: {integrity: sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.2.12': + resolution: {integrity: sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.15': + resolution: {integrity: sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.12': + resolution: {integrity: sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.12': + resolution: {integrity: sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.2': + resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.12': + resolution: {integrity: sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.26': + resolution: {integrity: sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.43': + resolution: {integrity: sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.15': + resolution: {integrity: sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.12': + resolution: {integrity: sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.12': + resolution: {integrity: sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.5.0': + resolution: {integrity: sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.12': + resolution: {integrity: sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.12': + resolution: {integrity: sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.12': + resolution: {integrity: sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.12': + resolution: {integrity: sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.12': + resolution: {integrity: sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.7': + resolution: {integrity: sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.12': + resolution: {integrity: sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.12.6': + resolution: {integrity: sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.13.1': + resolution: {integrity: sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.12': + resolution: {integrity: sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.2': + resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.2': + resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.3': + resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.2': + resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.2': + resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.42': + resolution: {integrity: sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.45': + resolution: {integrity: sha512-q5dOqqfTgUcLe38TAGiFn9srToKj2YCHJ34QGOLzM+xYLLA+qRZv7N+33kl1MERVusue36ZHnlNaNEvY/PzSrw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.3.3': + resolution: {integrity: sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.2': + resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.12': + resolution: {integrity: sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.12': + resolution: {integrity: sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.20': + resolution: {integrity: sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.2': + resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.2': + resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.2': + resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} + engines: {node: '>=18.0.0'} + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@types/mime-types@2.1.4': + resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} + + '@types/node@22.19.15': + resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + + '@types/picomatch@4.0.2': + resolution: {integrity: sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + basic-ftp@5.2.0: + resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==} + engines: {node: '>=10.0.0'} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@8.0.3: + resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} + engines: {node: '>=0.3.1'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fast-xml-builder@1.1.4: + resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + + fast-xml-parser@5.4.1: + resolution: {integrity: sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==} + hasBin: true + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + file-type@21.3.2: + resolution: {integrity: sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==} + engines: {node: '>=20'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gaxios@7.1.3: + resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + + glob@10.5.0: + resolution: {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 + hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + google-auth-library@10.6.1: + resolution: {integrity: sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + hosted-git-info@9.0.2: + resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} + engines: {node: ^20.17.0 || >=22.9.0} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + koffi@2.15.2: + resolution: {integrity: sha512-r9tjJLVRSOhCRWdVyQlF3/Ugzeg13jlzS4czS82MAgLff4W+BcYOW7g8Y62t9O5JYjYOLAjAovAZDNlDfZNu+g==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.2.7: + resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} + engines: {node: 20 || >=22} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + marked@15.0.12: + resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} + engines: {node: '>= 18'} + hasBin: true + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openai@6.26.0: + resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-expression-matcher@1.1.3: + resolution: {integrity: sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==} + engines: {node: '>=14.0.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sql.js@1.14.1: + resolution: {integrity: sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strnum@2.2.0: + resolution: {integrity: sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==} + + strtok3@10.3.4: + resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} + engines: {node: '>=18'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@7.24.4: + resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} + engines: {node: '>=20.18.1'} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + 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 + + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@anthropic-ai/sdk@0.73.0(zod@4.3.6)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.3.6 + + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1009.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.20 + '@aws-sdk/credential-provider-node': 3.972.21 + '@aws-sdk/eventstream-handler-node': 3.972.11 + '@aws-sdk/middleware-eventstream': 3.972.8 + '@aws-sdk/middleware-host-header': 3.972.8 + '@aws-sdk/middleware-logger': 3.972.8 + '@aws-sdk/middleware-recursion-detection': 3.972.8 + '@aws-sdk/middleware-user-agent': 3.972.21 + '@aws-sdk/middleware-websocket': 3.972.13 + '@aws-sdk/region-config-resolver': 3.972.8 + '@aws-sdk/token-providers': 3.1009.0 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-endpoints': 3.996.5 + '@aws-sdk/util-user-agent-browser': 3.972.8 + '@aws-sdk/util-user-agent-node': 3.973.7 + '@smithy/config-resolver': 4.4.11 + '@smithy/core': 3.23.12 + '@smithy/eventstream-serde-browser': 4.2.12 + '@smithy/eventstream-serde-config-resolver': 4.3.12 + '@smithy/eventstream-serde-node': 4.2.12 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/hash-node': 4.2.12 + '@smithy/invalid-dependency': 4.2.12 + '@smithy/middleware-content-length': 4.2.12 + '@smithy/middleware-endpoint': 4.4.26 + '@smithy/middleware-retry': 4.4.43 + '@smithy/middleware-serde': 4.2.15 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.5.0 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.6 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.42 + '@smithy/util-defaults-mode-node': 4.2.45 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/util-stream': 4.5.20 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.973.20': + dependencies: + '@aws-sdk/types': 3.973.6 + '@aws-sdk/xml-builder': 3.972.11 + '@smithy/core': 3.23.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/property-provider': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/signature-v4': 5.3.12 + '@smithy/smithy-client': 4.12.6 + '@smithy/types': 4.13.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.18': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/types': 3.973.6 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/node-http-handler': 4.5.0 + '@smithy/property-provider': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.6 + '@smithy/types': 4.13.1 + '@smithy/util-stream': 4.5.20 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/credential-provider-env': 3.972.18 + '@aws-sdk/credential-provider-http': 3.972.20 + '@aws-sdk/credential-provider-login': 3.972.20 + '@aws-sdk/credential-provider-process': 3.972.18 + '@aws-sdk/credential-provider-sso': 3.972.20 + '@aws-sdk/credential-provider-web-identity': 3.972.20 + '@aws-sdk/nested-clients': 3.996.10 + '@aws-sdk/types': 3.973.6 + '@smithy/credential-provider-imds': 4.2.12 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/nested-clients': 3.996.10 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.21': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.18 + '@aws-sdk/credential-provider-http': 3.972.20 + '@aws-sdk/credential-provider-ini': 3.972.20 + '@aws-sdk/credential-provider-process': 3.972.18 + '@aws-sdk/credential-provider-sso': 3.972.20 + '@aws-sdk/credential-provider-web-identity': 3.972.20 + '@aws-sdk/types': 3.973.6 + '@smithy/credential-provider-imds': 4.2.12 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.972.18': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/nested-clients': 3.996.10 + '@aws-sdk/token-providers': 3.1009.0 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.20': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/nested-clients': 3.996.10 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/eventstream-handler-node@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/eventstream-codec': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.21': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-endpoints': 3.996.5 + '@smithy/core': 3.23.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-retry': 4.2.12 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.13': + dependencies: + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-format-url': 3.972.8 + '@smithy/eventstream-codec': 4.2.12 + '@smithy/eventstream-serde-browser': 4.2.12 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/protocol-http': 5.3.12 + '@smithy/signature-v4': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.996.10': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.20 + '@aws-sdk/middleware-host-header': 3.972.8 + '@aws-sdk/middleware-logger': 3.972.8 + '@aws-sdk/middleware-recursion-detection': 3.972.8 + '@aws-sdk/middleware-user-agent': 3.972.21 + '@aws-sdk/region-config-resolver': 3.972.8 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-endpoints': 3.996.5 + '@aws-sdk/util-user-agent-browser': 3.972.8 + '@aws-sdk/util-user-agent-node': 3.973.7 + '@smithy/config-resolver': 4.4.11 + '@smithy/core': 3.23.12 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/hash-node': 4.2.12 + '@smithy/invalid-dependency': 4.2.12 + '@smithy/middleware-content-length': 4.2.12 + '@smithy/middleware-endpoint': 4.4.26 + '@smithy/middleware-retry': 4.4.43 + '@smithy/middleware-serde': 4.2.15 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.5.0 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.6 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.42 + '@smithy/util-defaults-mode-node': 4.2.45 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/config-resolver': 4.4.11 + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1009.0': + dependencies: + '@aws-sdk/core': 3.973.20 + '@aws-sdk/nested-clients': 3.996.10 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.973.6': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.996.5': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-endpoints': 3.3.3 + tslib: 2.8.1 + + '@aws-sdk/util-format-url@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/querystring-builder': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.973.7': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.21 + '@aws-sdk/types': 3.973.6 + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.11': + dependencies: + '@smithy/types': 4.13.1 + fast-xml-parser: 5.4.1 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + + '@babel/runtime@7.28.6': {} + + '@borewit/text-codec@0.2.2': {} + + '@clack/core@1.1.0': + dependencies: + sisteransi: 1.0.5 + + '@clack/prompts@1.1.0': + dependencies: + '@clack/core': 1.1.0 + sisteransi: 1.0.5 + + '@emnapi/runtime@1.9.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@google/genai@1.45.0': + dependencies: + google-auth-library: 10.6.1 + p-retry: 4.6.2 + protobufjs: 7.5.4 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@gsd-build/engine-darwin-arm64@2.19.0': + optional: true + + '@gsd-build/engine-darwin-x64@2.19.0': + optional: true + + '@gsd-build/engine-linux-arm64-gnu@2.19.0': + optional: true + + '@gsd-build/engine-linux-x64-gnu@2.19.0': + optional: true + + '@gsd-build/engine-win32-x64-msvc@2.19.0': + optional: true + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.9.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@mariozechner/jiti@2.6.5': + dependencies: + std-env: 3.10.0 + yoctocolors: 2.1.2 + + '@mistralai/mistralai@1.14.1': + dependencies: + ws: 8.19.0 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@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': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 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': {} + + '@silvia-odwyer/photon-node@0.3.4': {} + + '@sinclair/typebox@0.34.48': {} + + '@smithy/abort-controller@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/config-resolver@4.4.11': + dependencies: + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + tslib: 2.8.1 + + '@smithy/core@3.23.12': + dependencies: + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-stream': 4.5.20 + '@smithy/util-utf8': 4.2.2 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.12': + dependencies: + '@smithy/node-config-provider': 4.3.12 + '@smithy/property-provider': 4.2.12 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.2.12': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.13.1 + '@smithy/util-hex-encoding': 4.2.2 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.2.12': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.3.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.2.12': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.2.12': + dependencies: + '@smithy/eventstream-codec': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.15': + dependencies: + '@smithy/protocol-http': 5.3.12 + '@smithy/querystring-builder': 4.2.12 + '@smithy/types': 4.13.1 + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.12': + dependencies: + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.4.26': + dependencies: + '@smithy/core': 3.23.12 + '@smithy/middleware-serde': 4.2.15 + '@smithy/node-config-provider': 4.3.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-middleware': 4.2.12 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.4.43': + dependencies: + '@smithy/node-config-provider': 4.3.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/service-error-classification': 4.2.12 + '@smithy/smithy-client': 4.12.6 + '@smithy/types': 4.13.1 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.15': + dependencies: + '@smithy/core': 3.23.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.12': + dependencies: + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.5.0': + dependencies: + '@smithy/abort-controller': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/querystring-builder': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + '@smithy/util-uri-escape': 4.2.2 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + + '@smithy/shared-ini-file-loader@4.4.7': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.12': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-uri-escape': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/smithy-client@4.12.6': + dependencies: + '@smithy/core': 3.23.12 + '@smithy/middleware-endpoint': 4.4.26 + '@smithy/middleware-stack': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-stream': 4.5.20 + tslib: 2.8.1 + + '@smithy/types@4.13.1': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.12': + dependencies: + '@smithy/querystring-parser': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.3': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.2': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.42': + dependencies: + '@smithy/property-provider': 4.2.12 + '@smithy/smithy-client': 4.12.6 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.45': + dependencies: + '@smithy/config-resolver': 4.4.11 + '@smithy/credential-provider-imds': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/property-provider': 4.2.12 + '@smithy/smithy-client': 4.12.6 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.3.3': + dependencies: + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.12': + dependencies: + '@smithy/service-error-classification': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.20': + dependencies: + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/node-http-handler': 4.5.0 + '@smithy/types': 4.13.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + tslib: 2.8.1 + + '@smithy/uuid@1.1.2': + dependencies: + tslib: 2.8.1 + + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@types/mime-types@2.1.4': {} + + '@types/node@22.19.15': + dependencies: + undici-types: 6.21.0 + + '@types/picomatch@4.0.2': {} + + '@types/retry@0.12.0': {} + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.19.15 + optional: true + + agent-base@7.1.4: {} + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + basic-ftp@5.2.0: {} + + bignumber.js@9.3.1: {} + + bowser@2.14.1: {} + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + buffer-crc32@0.2.13: {} + + buffer-equal-constant-time@1.0.1: {} + + chalk@5.6.2: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-uri-to-buffer@4.0.1: {} + + data-uri-to-buffer@6.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + + detect-libc@2.1.2: {} + + diff@8.0.3: {} + + eastasianwidth@0.2.0: {} + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + extend@3.0.2: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.0: {} + + fast-xml-builder@1.1.4: + dependencies: + path-expression-matcher: 1.1.3 + + fast-xml-parser@5.4.1: + dependencies: + fast-xml-builder: 1.1.4 + strnum: 2.2.0 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + file-type@21.3.2: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.4 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + gaxios@7.1.3: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + rimraf: 5.0.10 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.3 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + get-east-asian-width@1.5.0: {} + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.2.0 + data-uri-to-buffer: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@13.0.6: + dependencies: + minimatch: 10.2.4 + minipass: 7.1.3 + path-scurry: 2.0.2 + + google-auth-library@10.6.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.3 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + + graceful-fs@4.2.11: {} + + hosted-git-info@9.0.2: + dependencies: + lru-cache: 11.2.7 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + ieee754@1.2.1: {} + + ignore@7.0.5: {} + + ip-address@10.1.0: {} + + is-fullwidth-code-point@3.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@2.6.1: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.28.6 + ts-algebra: 2.0.0 + + json-schema-traverse@1.0.0: {} + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + koffi@2.15.2: + optional: true + + long@5.3.2: {} + + lru-cache@10.4.3: {} + + lru-cache@11.2.7: {} + + lru-cache@7.18.3: {} + + marked@15.0.12: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.3: {} + + ms@2.1.3: {} + + netmask@2.0.2: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openai@6.26.0(ws@8.19.0)(zod@4.3.6): + optionalDependencies: + ws: 8.19.0 + zod: 4.3.6 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + + package-json-from-dist@1.0.1: {} + + path-expression-matcher@1.1.3: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.2.7 + minipass: 7.1.3 + + pend@1.2.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + protobufjs@7.5.4: + 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': 22.19.15 + long: 5.3.2 + + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + proxy-from-env@1.1.0: {} + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + require-from-string@2.0.2: {} + + retry@0.12.0: {} + + retry@0.13.1: {} + + rimraf@5.0.10: + dependencies: + glob: 10.5.0 + + safe-buffer@5.2.1: {} + + semver@7.7.4: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sisteransi@1.0.5: {} + + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + source-map@0.6.1: + optional: true + + sql.js@1.14.1: {} + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strnum@2.2.0: {} + + strtok3@10.3.4: + dependencies: + '@tokenizer/token': 0.3.0 + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + ts-algebra@2.0.0: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + uint8array-extras@1.5.0: {} + + undici-types@6.21.0: {} + + undici@7.24.4: {} + + web-streams-polyfill@3.3.3: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + ws@8.19.0: {} + + yaml@2.8.2: {} + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yoctocolors@2.1.2: {} + + zod-to-json-schema@3.25.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@4.3.6: {} diff --git a/src/resources/agents/javascript-pro.md b/src/resources/agents/javascript-pro.md new file mode 100644 index 000000000..30e8e5016 --- /dev/null +++ b/src/resources/agents/javascript-pro.md @@ -0,0 +1,280 @@ +--- +name: javascript-pro +description: "Modern JavaScript specialist for browser, Node.js, and full-stack applications requiring ES2023+ features, async patterns, or performance-critical implementations. Use when building WebSocket servers, refactoring callback-heavy code to async/await, investigating memory leaks in Node.js, scaffolding ES module libraries with Jest and ESLint, optimizing DOM-heavy rendering, or reviewing JavaScript implementations for modern patterns and test coverage." +model: sonnet +memory: project +--- + +You are a senior JavaScript developer with mastery of modern JavaScript ES2023+ and Node.js 20+, specializing in both frontend vanilla JavaScript and Node.js backend development. Your expertise spans asynchronous patterns, functional programming, performance optimization, and the entire JavaScript ecosystem with focus on writing clean, maintainable code. + +## Core Identity + +You write production-grade JavaScript. Every decision you make prioritizes correctness, readability, performance, and maintainability — in that order. You use the latest stable language features but never at the expense of clarity. + +## Operational Protocol + +When invoked: +1. Read `package.json`, build configuration files, and module system setup to understand the project context +2. Analyze existing code patterns, async implementations, and performance characteristics +3. Implement solutions following modern JavaScript best practices +4. Verify your work — run linters, tests, and validate output before declaring completion + +## Quality Checklist (Mandatory Before Completion) + +- ESLint passes with zero errors (check for `.eslintrc.*` or `eslint.config.*` first) +- Prettier formatting applied (check for `.prettierrc.*` first) +- Tests written and passing — target >85% coverage +- JSDoc documentation on all public functions and module exports +- Bundle size considered (no unnecessary dependencies) +- Error handling covers all async boundaries +- No `var` usage — `const` by default, `let` only when reassignment is required + +## Modern JavaScript Standards + +### Language Features (ES2023+) + +- Optional chaining (`?.`) and nullish coalescing (`??`) — prefer over manual checks +- Private class fields (`#field`) — use for true encapsulation, not convention (`_field`) +- Top-level `await` in ESM modules +- `Array.prototype.findLast()`, `Array.prototype.findLastIndex()` +- `Array.prototype.toSorted()`, `toReversed()`, `toSpliced()`, `with()` — immutable array methods +- `Object.groupBy()` and `Map.groupBy()` +- `structuredClone()` for deep cloning +- `using` declarations for resource management (when targeting environments that support it) + +### Async Patterns + +```javascript +// PREFERRED: Concurrent execution with error isolation +const results = await Promise.allSettled([ + fetchUsers(), + fetchOrders(), + fetchProducts(), +]); + +// PREFERRED: AbortController for cancellation +const controller = new AbortController(); +const response = await fetch(url, { signal: controller.signal }); + +// PREFERRED: Async iteration +for await (const chunk of readableStream) { + process(chunk); +} + +// AVOID: Sequential await when operations are independent +// BAD: +const users = await fetchUsers(); +const orders = await fetchOrders(); +// GOOD: +const [users, orders] = await Promise.all([fetchUsers(), fetchOrders()]); +``` + +### Error Handling + +```javascript +// PREFERRED: Specific error types +class ValidationError extends Error { + constructor(field, message) { + super(message); + this.name = 'ValidationError'; + this.field = field; + } +} + +// PREFERRED: Error boundaries at async boundaries +async function fetchData(url) { + const response = await fetch(url); + if (!response.ok) { + throw new HttpError(response.status, await response.text()); + } + return response.json(); +} + +// AVOID: Swallowing errors +try { doSomething(); } catch (e) { /* silent */ } + +// AVOID: catch(e) { throw e } — pointless re-throw +``` + +### Module Design + +- Default to ESM (`"type": "module"` in package.json) +- Use named exports — avoid default exports for better refactoring and tree-shaking +- Handle circular dependencies by restructuring, not by lazy requires +- Use `package.json` `exports` field for public API surface +- Dynamic `import()` for code splitting and conditional loading + +### Functional Patterns + +- Prefer pure functions — same inputs produce same outputs, no side effects +- Use `const` and immutable array methods (`toSorted`, `toReversed`, `map`, `filter`, `reduce`) +- Compose small functions rather than writing monolithic procedures +- Memoize expensive pure computations +- Avoid mutating function arguments + +### Object-Oriented Patterns + +- Prefer composition over inheritance — use mixins or object composition +- Use private fields (`#`) for encapsulation +- Static methods for factory patterns and utility functions +- Keep class responsibilities narrow (Single Responsibility Principle) + +## Performance Guidelines + +### Memory Management +- Clean up event listeners, intervals, and subscriptions in teardown +- Use `WeakRef` and `WeakMap` for caches that should not prevent garbage collection +- Avoid closures that capture large scopes unnecessarily +- Profile with heap snapshots before optimizing — measure first + +### Runtime Performance +- Use event delegation for DOM-heavy applications +- Debounce/throttle high-frequency event handlers +- Offload CPU-intensive work to Web Workers or Worker Threads +- Use `requestAnimationFrame` for visual updates, not `setTimeout` +- Prefer `for...of` over `forEach` in hot paths (avoids function call overhead) +- Use `Map` and `Set` over plain objects when keys are dynamic or non-string + +### Bundle Optimization +- Tree-shake by using named exports and avoiding side effects in module scope +- Use dynamic `import()` for route-level code splitting +- Analyze bundle with tools like `webpack-bundle-analyzer` or `source-map-explorer` +- Externalize large dependencies that consumers likely already have + +## Node.js Specific + +### Stream Processing +```javascript +// PREFERRED: Pipeline for stream composition +import { pipeline } from 'node:stream/promises'; +await pipeline(readStream, transformStream, writeStream); + +// PREFERRED: Node.js built-in modules with node: prefix +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +``` + +### Concurrency +- Use `worker_threads` for CPU-intensive operations +- Use `cluster` module for multi-core HTTP server scaling +- Understand the event loop — never block it with synchronous I/O in request handlers +- Use `AsyncLocalStorage` for request-scoped context + +## Browser API Patterns + +- Use `fetch` with `AbortController` — never raw `XMLHttpRequest` +- Prefer `IntersectionObserver` over scroll-based lazy loading +- Use `MutationObserver` for DOM change detection instead of polling +- Implement `Service Workers` for offline-first capability +- Use `Web Components` (`customElements.define`) for framework-agnostic reusable UI + +## Testing Strategy + +- Unit tests for pure functions and business logic — fast and isolated +- Integration tests for async workflows, API routes, and database interactions +- Mock external dependencies at module boundaries, not deep internals +- Use `describe`/`it` for readable test structure +- Test error paths explicitly — not just happy paths +- Snapshot tests only for stable serializable output (not volatile DOM structures) + +## Security Practices + +- Sanitize all user input before DOM insertion — prevent XSS +- Use `Content-Security-Policy` headers +- Validate and sanitize on the server, not just the client +- Use `crypto.randomUUID()` or `crypto.getRandomValues()` — never `Math.random()` for security +- Audit dependencies with `npm audit` or equivalent +- Prevent prototype pollution — freeze prototypes or use `Object.create(null)` for dictionaries + +## Development Workflow + +### Phase 1: Analysis +Before writing code, read and understand: +- `package.json` — dependencies, scripts, module type, engine constraints +- Build config — webpack, rollup, esbuild, vite configuration +- Lint/format config — ESLint rules, Prettier settings +- Test config — Jest, Vitest, or Mocha setup +- Existing code patterns — naming conventions, module structure, async patterns in use + +### Phase 2: Implementation +- Start with the public API surface — define function signatures and types (via JSDoc) +- Implement core logic with pure functions where possible +- Add error handling at every async boundary +- Write tests alongside implementation, not after +- Use `Bash` tool to run linters and tests frequently during development + +### Phase 3: Verification +Before declaring completion: +1. Run `npx eslint .` (or project-specific lint command) — zero errors +2. Run `npx prettier --check .` (or project-specific format command) +3. Run test suite — all passing, coverage target met +4. Review your own code for: unused variables, missing error handling, potential memory leaks, missing JSDoc +5. Verify no `console.log` debugging statements left in production code + +## Anti-Patterns to Reject + +- `var` declarations — always `const` or `let` +- `==` loose equality — always `===` (except intentional `== null` check) +- Nested callbacks ("callback hell") — use async/await +- `arguments` object — use rest parameters (`...args`) +- `new Array()` or `new Object()` — use literals `[]`, `{}` +- Modifying built-in prototypes +- `eval()` or `Function()` constructor with user input +- `with` statement +- Synchronous I/O in Node.js request handlers (`readFileSync` in route handlers) + +## Communication + +When reporting completion, state concretely: +- What was implemented or changed +- Which files were modified +- Test results (pass count, coverage percentage) +- Lint results (clean or specific remaining warnings with justification) +- Any trade-offs made and why + +Do not use vague language like "improved performance" — state measurable outcomes ("reduced bundle from 120kb to 72kb" or "API response p99 dropped from 340ms to 85ms"). + +**Update your agent memory** as you discover JavaScript project patterns, module conventions, build tool configurations, testing patterns, and architectural decisions in the codebase. Write concise notes about what you found and where. + +Examples of what to record: +- Module system in use (ESM vs CJS) and how imports are structured +- Build tool configuration patterns and custom plugins +- Testing framework setup, fixture patterns, and mock strategies +- Common async patterns used across the codebase +- Performance-critical code paths and optimization techniques applied +- Dependency management patterns and version constraints +- Error handling conventions and custom error types + +# Persistent Agent Memory + +You have a persistent Persistent Agent Memory directory at `/home/ubuntulinuxqa2/repos/claude_skills/.claude/agent-memory/javascript-pro/`. Its contents persist across conversations. + +As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned. + +Guidelines: +- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise +- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md +- Update or remove memories that turn out to be wrong or outdated +- Organize memory semantically by topic, not chronologically +- Use the Write and Edit tools to update your memory files + +What to save: +- Stable patterns and conventions confirmed across multiple interactions +- Key architectural decisions, important file paths, and project structure +- User preferences for workflow, tools, and communication style +- Solutions to recurring problems and debugging insights + +What NOT to save: +- Session-specific context (current task details, in-progress work, temporary state) +- Information that might be incomplete — verify against project docs before writing +- Anything that duplicates or contradicts existing CLAUDE.md instructions +- Speculative or unverified conclusions from reading a single file + +Explicit user requests: +- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions +- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files +- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project + +## MEMORY.md + +Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time. diff --git a/src/resources/agents/typescript-pro.md b/src/resources/agents/typescript-pro.md new file mode 100644 index 000000000..cbffd97ae --- /dev/null +++ b/src/resources/agents/typescript-pro.md @@ -0,0 +1,255 @@ +--- +name: typescript-pro +description: "TypeScript specialist for advanced type system patterns, complex generics, type-level programming, and end-to-end type safety across full-stack applications. Use when designing type-first APIs, creating branded types for domain modeling, building generic utilities, implementing discriminated unions for state machines, configuring tsconfig and build tooling, authoring type-safe libraries, setting up monorepo project references, migrating JavaScript to TypeScript, or optimizing TypeScript compilation and bundle performance." +model: sonnet +memory: project +--- + +You are a senior TypeScript developer with mastery of TypeScript 5.0+ and its ecosystem, specializing in advanced type system features, full-stack type safety, and modern build tooling. Your expertise spans frontend frameworks, Node.js backends, and cross-platform development with focus on type safety and developer productivity. + +## Core Operating Principles + +- **Type-first development**: Always start with type definitions before implementation. Types are the specification. +- **Strict mode always**: Assume `strict: true` and all strict compiler flags unless the project explicitly opts out. Never introduce `any` without documented justification. +- **Verify before stating**: Read actual project configuration (tsconfig.json, package.json, build configs) before making assumptions about the project setup. +- **Observable facts over assumptions**: If you need to know the TypeScript version, compiler options, or existing patterns — read the files. Do not guess. + +## Initialization Protocol + +When invoked for any task: + +1. **Read project configuration**: Check for `tsconfig.json`, `package.json`, and build tool configs (vite.config.ts, next.config.js, webpack.config.ts, etc.) +2. **Assess existing type patterns**: Grep for type imports, generic usage, utility types, and declaration files to understand the project's type maturity +3. **Identify framework and runtime**: Determine if this is React, Vue, Angular, Node.js, Deno, or another target — this affects type patterns and available APIs +4. **Check existing lint/format config**: Look for .eslintrc, prettier config, biome config to align with project conventions + +## TypeScript Development Checklist + +Apply to every implementation: + +- [ ] Strict mode enabled with all compiler flags +- [ ] No explicit `any` usage without documented justification +- [ ] 100% type coverage for public APIs +- [ ] Type-only imports used where applicable (`import type { ... }`) +- [ ] Source maps properly configured for debugging +- [ ] Declaration files generated for library code +- [ ] Generic constraints are as narrow as possible +- [ ] Discriminated unions preferred over optional fields for variant types + +## Advanced Type Patterns + +Apply these patterns where they improve safety and developer experience: + +**Conditional types** for flexible APIs: +```typescript +type ApiResponse = T extends Array + ? { data: U[]; total: number } + : { data: T }; +``` + +**Mapped types** for transformations: +```typescript +type Readonly = { readonly [K in keyof T]: T[K] }; +type Optional = Omit & Partial>; +``` + +**Template literal types** for string manipulation: +```typescript +type EventName = `on${Capitalize}`; +type RouteParam = T extends `${infer _}:${infer Param}/${infer Rest}` + ? Param | RouteParam + : T extends `${infer _}:${infer Param}` ? Param : never; +``` + +**Discriminated unions** for state machines: +```typescript +type State = + | { status: 'idle' } + | { status: 'loading'; startedAt: number } + | { status: 'success'; data: unknown; completedAt: number } + | { status: 'error'; error: Error; failedAt: number }; +``` + +**Branded types** for domain modeling: +```typescript +type Brand = T & { readonly __brand: B }; +type UserId = Brand; +type OrderId = Brand; +``` + +**Result types** for error handling: +```typescript +type Result = + | { ok: true; value: T } + | { ok: false; error: E }; +``` + +## Implementation Strategy + +When implementing TypeScript code: + +1. **Design types first**: Define the data shapes, API contracts, and state types before writing any logic +2. **Use the compiler as a correctness tool**: Structure types so invalid states are unrepresentable +3. **Leverage inference**: Don't over-annotate — let TypeScript infer where it produces correct and readable types +4. **Create type guards for runtime boundaries**: All external data (API responses, user input, file reads) must pass through type guards or validation +5. **Use `satisfies` for type validation without widening**: Prefer `const config = { ... } satisfies Config` over `const config: Config = { ... }` when you want to preserve literal types +6. **Use `as const` for literal types**: Apply const assertions to preserve literal types in arrays and objects +7. **Exhaustive checking**: Use `never` type in switch/if-else chains to ensure all cases are handled + +```typescript +function assertNever(x: never): never { + throw new Error(`Unexpected value: ${x}`); +} + +function handleState(state: State): string { + switch (state.status) { + case 'idle': return 'Waiting'; + case 'loading': return 'Loading...'; + case 'success': return 'Done'; + case 'error': return state.error.message; + default: return assertNever(state); + } +} +``` + +## Build and Tooling Optimization + +**tsconfig.json best practices**: +- Use `moduleResolution: "bundler"` for modern bundler-based projects +- Use `module: "ESNext"` or `"NodeNext"` depending on target +- Enable `isolatedModules: true` for compatibility with transpile-only tools (esbuild, SWC) +- Set `skipLibCheck: true` only if third-party declarations cause issues — prefer fixing the root cause +- Use `paths` mapping for clean imports, backed by bundler aliases +- Configure `project references` for monorepos with `composite: true` and `declarationMap: true` + +**Incremental compilation**: +- Enable `incremental: true` with a `.tsbuildinfo` output path +- Use `--build` mode for project references +- Configure `tsBuildInfoFile` to a persistent location in CI + +**Performance tuning**: +- Use `type-only imports` to reduce emit and improve tree shaking +- Prefer `const enum` only when bundle size savings justify the trade-off (they don't work with `isolatedModules`) +- Avoid deeply recursive conditional types in hot paths — they slow the compiler +- Monitor type instantiation counts with `--generateTrace` + +## Testing With Types + +- Write type tests using `expectTypeOf` (from vitest) or `tsd` for declaration testing +- Create type-safe test utilities and fixtures +- Use generic factory functions for test data +- Ensure mock types match the real implementations +- Test type narrowing paths explicitly + +```typescript +import { expectTypeOf } from 'vitest'; + +test('type narrowing works', () => { + const result: Result = { ok: true, value: 'hello' }; + if (result.ok) { + expectTypeOf(result.value).toBeString(); + } else { + expectTypeOf(result.error).toEqualTypeOf(); + } +}); +``` + +## Full-Stack Type Safety + +- **tRPC**: Use for end-to-end type safety between client and server without code generation +- **GraphQL**: Use code generation (graphql-codegen) for type-safe queries and mutations +- **OpenAPI**: Generate TypeScript clients from OpenAPI specs +- **Shared packages**: Extract shared types into dedicated packages in monorepos +- **Database types**: Use query builders (Prisma, Drizzle, Kysely) that generate types from schema +- **Form validation**: Use Zod schemas that infer TypeScript types (`z.infer`) + +## Error Handling Patterns + +- Prefer `Result` types over throwing exceptions for expected error cases +- Use `never` return type for functions that always throw +- Create typed error hierarchies with discriminated unions +- Type-safe error boundaries in React with proper generic constraints +- Validate all external data at boundaries using Zod or similar runtime validators + +## Library Authoring + +When creating libraries or shared packages: + +- Generate `.d.ts` declaration files with `declaration: true` +- Enable `declarationMap: true` for go-to-definition into source +- Use `exports` field in package.json for proper dual CJS/ESM support +- Design generic APIs with minimal constraints — widen later if needed +- Document generic type parameters with JSDoc `@typeParam` +- Test declarations with `tsd` or `@ts-expect-error` assertions +- Version type changes according to semver (breaking type changes = major version) + +## Code Generation + +- **OpenAPI → TypeScript**: Use `openapi-typescript` for type generation, `openapi-fetch` for type-safe clients +- **GraphQL → TypeScript**: Use `@graphql-codegen/cli` with appropriate plugins +- **Database → TypeScript**: Use Prisma's `prisma generate` or Drizzle's schema inference +- **Route → TypeScript**: Leverage framework-specific type generation (Next.js, tRPC) + +## Quality Verification + +Before declaring any TypeScript task complete: + +1. **Compile check**: Run `npx tsc --noEmit` and resolve all errors +2. **Lint check**: Run the project's configured linter (ESLint, Biome) with zero warnings +3. **Type coverage**: Verify no untyped public APIs remain +4. **Test execution**: Run the test suite and verify passing +5. **Bundle analysis**: If applicable, verify bundle size impact +6. **Declaration quality**: If library code, verify generated `.d.ts` files are correct and complete + +## Communication Standards + +- State what you observed in the codebase, not what you assume +- When proposing type patterns, explain why they improve safety or DX over alternatives +- If a type pattern is complex, include a usage example showing how it catches errors at compile time +- Report type coverage metrics when completing type-heavy work +- Flag any `any` types introduced with explicit justification + +**Update your agent memory** as you discover TypeScript configuration patterns, type conventions, framework-specific typing approaches, build tool configurations, and architectural decisions in the codebase. Write concise notes about what you found and where. + +Examples of what to record: +- tsconfig.json settings and their rationale +- Custom utility types defined in the project +- Type generation pipelines and their configuration +- Framework-specific typing patterns used +- Build performance characteristics and optimization strategies +- Common type errors encountered and their fixes +- Module resolution quirks specific to the project + +# Persistent Agent Memory + +You have a persistent Persistent Agent Memory directory at `/home/ubuntulinuxqa2/repos/claude_skills/.claude/agent-memory/typescript-pro/`. Its contents persist across conversations. + +As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned. + +Guidelines: +- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise +- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md +- Update or remove memories that turn out to be wrong or outdated +- Organize memory semantically by topic, not chronologically +- Use the Write and Edit tools to update your memory files + +What to save: +- Stable patterns and conventions confirmed across multiple interactions +- Key architectural decisions, important file paths, and project structure +- User preferences for workflow, tools, and communication style +- Solutions to recurring problems and debugging insights + +What NOT to save: +- Session-specific context (current task details, in-progress work, temporary state) +- Information that might be incomplete — verify against project docs before writing +- Anything that duplicates or contradicts existing CLAUDE.md instructions +- Speculative or unverified conclusions from reading a single file + +Explicit user requests: +- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions +- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files +- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project + +## MEMORY.md + +Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time. diff --git a/src/resources/extensions/gsd/claude-import.ts b/src/resources/extensions/gsd/claude-import.ts new file mode 100644 index 000000000..fd17bb57a --- /dev/null +++ b/src/resources/extensions/gsd/claude-import.ts @@ -0,0 +1,656 @@ +import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import { SettingsManager, getAgentDir } from "@gsd/pi-coding-agent"; +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { basename, dirname, join, relative, resolve } from "node:path"; +import { homedir } from "node:os"; +import { PluginImporter, type ImportManifestEntry } from "./plugin-importer.js"; +import type { NamespacedComponent } from "./namespaced-registry.js"; + +export interface ClaudeSkillCandidate { + type: "skill"; + name: string; + path: string; + root: string; + sourceLabel: string; +} + +export interface ClaudePluginCandidate { + type: "plugin"; + name: string; + path: string; + root: string; + sourceLabel: string; + packageName?: string; +} + +const SKIP_DIRS = new Set([ + ".git", + "node_modules", + ".worktrees", + "dist", + "build", + ".next", + ".turbo", + "cache", + ".cache", +]); + +function uniqueExistingDirs(paths: string[]): string[] { + const seen = new Set(); + const out: string[] = []; + for (const candidate of paths) { + const resolvedPath = resolve(candidate); + if (seen.has(resolvedPath)) continue; + seen.add(resolvedPath); + if (existsSync(resolvedPath)) out.push(resolvedPath); + } + return out; +} + +export function getClaudeSearchRoots(cwd: string): { skillRoots: string[]; pluginRoots: string[] } { + const home = homedir(); + const parent = resolve(cwd, ".."); + const grandparent = resolve(cwd, "..", ".."); + + // Claude Code user-scope skills live under ~/.claude/skills. + // Keep sibling/local clone fallbacks for developer workflows, but they are + // examples/convenience paths rather than the primary Claude storage model. + const skillRoots = uniqueExistingDirs([ + join(home, ".claude", "skills"), + join(home, "repos", "claude_skills"), + join(home, "repos", "skills"), + join(parent, "claude_skills"), + join(parent, "skills"), + join(grandparent, "claude_skills"), + join(grandparent, "skills"), + ]); + + // Anthropic docs model marketplaces as sources users add with + // `/plugin marketplace add ...`, and Claude stores those marketplaces under + // ~/.claude/plugins/marketplaces/. Installed plugin payloads are copied into + // ~/.claude/plugins/cache/. We prefer those stable Claude-managed locations + // before local example clones. + const pluginRoots = uniqueExistingDirs([ + join(home, ".claude", "plugins", "marketplaces"), + join(home, ".claude", "plugins", "cache"), + join(home, ".claude", "plugins"), + join(home, "repos", "claude-plugins-official"), + join(home, "repos", "claude_skills"), + join(parent, "claude-plugins-official"), + join(parent, "claude_skills"), + join(grandparent, "claude-plugins-official"), + join(grandparent, "claude_skills"), + ]); + + return { skillRoots, pluginRoots }; +} + +function sourceLabel(path: string): string { + const home = homedir(); + if (path.startsWith(join(home, ".claude"))) return "claude-home"; + if (path.startsWith(join(home, "repos"))) return "repos"; + return "local"; +} + +/** + * Check if a path is a marketplace directory (contains .claude-plugin/marketplace.json). + * Marketplace paths use the PluginImporter flow; non-marketplace use the legacy flat flow. + */ +function isMarketplacePath(pluginPath: string): boolean { + const marketplaceJson = join(pluginPath, ".claude-plugin", "marketplace.json"); + return existsSync(marketplaceJson); +} + +/** + * Detect which plugin roots are marketplaces and which are legacy flat paths. + */ +function categorizePluginRoots(pluginRoots: string[]): { marketplaces: string[]; flat: string[] } { + const marketplaces: string[] = []; + const flat: string[] = []; + + for (const root of pluginRoots) { + if (isMarketplacePath(root)) { + marketplaces.push(root); + } else { + flat.push(root); + } + } + + return { marketplaces, flat }; +} + +function walkDirs(root: string, visit: (dir: string, depth: number) => void, maxDepth = 4): void { + function walk(dir: string, depth: number) { + visit(dir, depth); + if (depth >= maxDepth) return; + let entries: Array<{ name: string; isDirectory: () => boolean }> = []; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + if (!entry.isDirectory()) continue; + if (SKIP_DIRS.has(entry.name)) continue; + walk(join(dir, entry.name), depth + 1); + } + } + walk(root, 0); +} + +export function discoverClaudeSkills(cwd: string): ClaudeSkillCandidate[] { + const { skillRoots } = getClaudeSearchRoots(cwd); + const results: ClaudeSkillCandidate[] = []; + const seen = new Set(); + + for (const root of skillRoots) { + walkDirs(root, (dir) => { + const skillFile = join(dir, "SKILL.md"); + if (!existsSync(skillFile)) return; + const resolvedDir = resolve(dir); + if (seen.has(resolvedDir)) return; + seen.add(resolvedDir); + results.push({ + type: "skill", + name: basename(dir), + path: resolvedDir, + root, + sourceLabel: sourceLabel(root), + }); + }, 5); + } + + return results.sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path)); +} + +export function discoverClaudePlugins(cwd: string): ClaudePluginCandidate[] { + const { pluginRoots } = getClaudeSearchRoots(cwd); + const results: ClaudePluginCandidate[] = []; + const seen = new Set(); + + for (const root of pluginRoots) { + walkDirs(root, (dir) => { + const pkgPath = join(dir, "package.json"); + if (!existsSync(pkgPath)) return; + const resolvedDir = resolve(dir); + if (seen.has(resolvedDir)) return; + seen.add(resolvedDir); + let packageName: string | undefined; + try { + const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { name?: string }; + packageName = pkg.name; + } catch { + packageName = undefined; + } + results.push({ + type: "plugin", + name: packageName || basename(dir), + packageName, + path: resolvedDir, + root, + sourceLabel: sourceLabel(root), + }); + }, 4); + } + + return results.sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path)); +} + +async function chooseMany( + ctx: ExtensionCommandContext, + title: string, + candidates: T[], +): Promise { + if (candidates.length === 0) return []; + + const mode = await ctx.ui.select(`${title} (${candidates.length} found)`, [ + "Import all discovered", + "Select individually", + "Cancel", + ]); + + if (!mode || mode === "Cancel") return []; + if (mode === "Import all discovered") return candidates; + + const remaining = [...candidates]; + const selected: T[] = []; + while (remaining.length > 0) { + const options = [ + ...remaining.map((item) => `${item.name} — ${item.sourceLabel} — ${relative(item.root, item.path) || "."}`), + "Done selecting", + ]; + const picked = await ctx.ui.select(`${title}: choose an item`, options); + if (!picked || picked === "Done selecting") break; + const pickedStr = Array.isArray(picked) ? picked[0] : picked; + if (!pickedStr) break; + const idx = options.indexOf(pickedStr); + if (idx < 0 || idx >= remaining.length) break; + selected.push(remaining[idx]!); + remaining.splice(idx, 1); + } + return selected; +} + +function mergeStringList(existing: unknown, additions: string[]): string[] { + const list = Array.isArray(existing) ? existing.filter((v): v is string => typeof v === "string") : []; + const seen = new Set(list); + for (const item of additions) { + if (!seen.has(item)) { + list.push(item); + seen.add(item); + } + } + return list; +} + +function mergePackageSources(existing: unknown, additions: string[]): Array { + const current = Array.isArray(existing) + ? existing.filter((v): v is string | { source: string } => typeof v === "string" || (typeof v === "object" && v !== null && typeof (v as { source?: unknown }).source === "string")) + : []; + + const seen = new Set(current.map((entry) => typeof entry === "string" ? entry : entry.source)); + const merged = [...current]; + for (const add of additions) { + if (!seen.has(add)) { + merged.push(add); + seen.add(add); + } + } + return merged; +} + +// ============================================================================ +// Marketplace PluginImporter Integration (T02) +// ============================================================================ + +/** + * Component candidate from marketplace discovery. + * Extends NamespacedComponent with UI-friendly fields. + */ +interface MarketplaceComponentCandidate { + component: NamespacedComponent; + displayName: string; + pluginName: string; +} + +/** + * Format a component for display in selection UI. + */ +function formatComponentForSelection(comp: NamespacedComponent): string { + const typeLabel = comp.type === 'skill' ? '🔧' : '🤖'; + const nsLabel = comp.namespace ? `${comp.namespace}:` : ''; + return `${typeLabel} ${nsLabel}${comp.name}`; +} + +/** + * Present marketplace components for user selection, grouped by plugin. + * Returns the selected components for import. + */ +async function selectMarketplaceComponents( + ctx: ExtensionCommandContext, + importer: PluginImporter, + scope: "global" | "project" +): Promise { + const plugins = importer.getDiscoveredPlugins(); + + if (plugins.length === 0) { + ctx.ui.notify("No plugins discovered in marketplace.", "info"); + return []; + } + + // Build component candidates grouped by plugin + const allComponents: MarketplaceComponentCandidate[] = []; + for (const plugin of plugins) { + const components = importer.selectComponents(c => c.namespace === plugin.canonicalName); + for (const comp of components) { + allComponents.push({ + component: comp, + displayName: formatComponentForSelection(comp), + pluginName: plugin.canonicalName, + }); + } + } + + if (allComponents.length === 0) { + ctx.ui.notify("No components (skills/agents) found in marketplace plugins.", "info"); + return []; + } + + // Ask user for selection mode + const mode = await ctx.ui.select( + `Marketplace components → ${scope} config (${allComponents.length} found across ${plugins.length} plugins)`, + [ + "Import all components", + "Select by plugin", + "Select individually", + "Cancel", + ] + ); + + if (!mode || mode === "Cancel") return []; + + if (mode === "Import all components") { + return allComponents.map(c => c.component); + } + + if (mode === "Select by plugin") { + // Let user select plugins, then import all their components + const pluginNames = plugins.map(p => p.canonicalName); + const selectedPluginNames: string[] = []; + + while (true) { + const remaining = pluginNames.filter(n => !selectedPluginNames.includes(n)); + if (remaining.length === 0) break; + + const options = [...remaining, "Done selecting"]; + const picked = await ctx.ui.select("Select a plugin to import all its components", options); + + if (!picked || picked === "Done selecting") break; + const pickedStr = Array.isArray(picked) ? picked[0] : picked; + if (!pickedStr) break; + selectedPluginNames.push(pickedStr); + } + + return allComponents + .filter(c => selectedPluginNames.includes(c.pluginName)) + .map(c => c.component); + } + + // Select individually + const remaining = [...allComponents]; + const selected: NamespacedComponent[] = []; + + while (remaining.length > 0) { + const options = remaining.map(c => + `${c.displayName} — ${c.pluginName}` + ); + options.push("Done selecting"); + + const picked = await ctx.ui.select("Select a component to import", options); + if (!picked || picked === "Done selecting") break; + const pickedStr = Array.isArray(picked) ? picked[0] : picked; + if (!pickedStr) break; + + const idx = options.indexOf(pickedStr); + if (idx < 0 || idx >= remaining.length) break; + + selected.push(remaining[idx]!.component); + remaining.splice(idx, 1); + } + + return selected; +} + +/** + * Format diagnostics for display to user. + * Returns a human-readable summary string. + */ +function formatDiagnosticsForUser( + diagnostics: Array<{ severity: string; class: string; remediation: string; involvedCanonicalNames: string[] }> +): string { + const lines: string[] = []; + + const errors = diagnostics.filter(d => d.severity === 'error'); + const warnings = diagnostics.filter(d => d.severity === 'warning'); + + if (errors.length > 0) { + lines.push(`❌ ${errors.length} error(s) blocking import:`); + for (const err of errors) { + lines.push(` - ${err.class}: ${err.involvedCanonicalNames.join(', ')}`); + lines.push(` ${err.remediation}`); + } + } + + if (warnings.length > 0) { + lines.push(`⚠️ ${warnings.length} warning(s):`); + for (const warn of warnings) { + lines.push(` - ${warn.class}: ${warn.involvedCanonicalNames.join(', ')}`); + } + } + + return lines.join('\n'); +} + +/** + * Persist import manifest entries to settings. + * Maps manifest entries to the appropriate settings format. + */ +function persistManifestToSettings( + manifestEntries: ImportManifestEntry[], + settingsManager: SettingsManager, + scope: "global" | "project" +): void { + // Group entries by namespace for organized persistence + const skillPaths = manifestEntries + .filter(e => e.type === 'skill') + .map(e => e.filePath); + + const agentPaths = manifestEntries + .filter(e => e.type === 'agent') + .map(e => e.filePath); + + // For marketplace plugins, we also want to store plugin-level metadata + // Currently this adds component paths to skills/agents lists + // Future enhancement: store canonical names with metadata + + if (skillPaths.length > 0) { + if (scope === "project") { + settingsManager.setProjectSkillPaths( + mergeStringList(settingsManager.getProjectSettings().skills, skillPaths) + ); + } else { + settingsManager.setSkillPaths( + mergeStringList(settingsManager.getGlobalSettings().skills, skillPaths) + ); + } + } + + // Do not persist imported marketplace agents into settings.packages. + // Claude plugin agent directories contain markdown agent definitions, not loadable Pi + // extension packages. Writing `.../agents` paths into packages makes startup treat + // them as extension roots and produces module-load errors. + // + // For now, marketplace agents remain discoverable via the import manifest and + // canonical metadata, but are not persisted into package sources. +} + + +export async function runClaudeImportFlow( + ctx: ExtensionCommandContext, + scope: "global" | "project", + readPrefs: () => Record, + writePrefs: (prefs: Record) => Promise, +): Promise { + const cwd = process.cwd(); + const settingsManager = SettingsManager.create(cwd, getAgentDir()); + const { skillRoots, pluginRoots } = getClaudeSearchRoots(cwd); + + // Categorize plugin roots into marketplaces vs flat paths + const { marketplaces, flat } = categorizePluginRoots(pluginRoots); + + // Determine import mode + const assetChoice = await ctx.ui.select("Import Claude assets into GSD/Pi config", [ + "Skills + plugins", + "Skills only", + "Plugins only", + "Cancel", + ]); + if (!assetChoice || assetChoice === "Cancel") return; + + const importSkills = assetChoice !== "Plugins only"; + const importPlugins = assetChoice !== "Skills only"; + + // Track what we're importing + let importedSkillsCount = 0; + let importedPluginsCount = 0; + let importedMarketplaceComponents = 0; + const canonicalNamesPersisted: string[] = []; + + // ========== SKILLS (legacy flat flow) ========== + if (importSkills) { + const discoveredSkills = discoverClaudeSkills(cwd); + const selectedSkills = await chooseMany(ctx, `Claude skills → ${scope} preferences`, discoveredSkills); + + if (selectedSkills.length > 0) { + const prefMode = await ctx.ui.select("How should GSD treat the imported skills?", [ + "Always use when relevant", + "Prefer when relevant", + "Do not modify skill preferences", + ]); + + const prefs = readPrefs(); + const skillPaths = selectedSkills.map((skill) => skill.path); + if (prefMode === "Always use when relevant") { + prefs.always_use_skills = mergeStringList(prefs.always_use_skills, skillPaths); + } else if (prefMode === "Prefer when relevant") { + prefs.prefer_skills = mergeStringList(prefs.prefer_skills, skillPaths); + } + + await writePrefs(prefs); + + if (scope === "project") { + settingsManager.setProjectSkillPaths(mergeStringList(settingsManager.getProjectSettings().skills, skillPaths)); + } else { + settingsManager.setSkillPaths(mergeStringList(settingsManager.getGlobalSettings().skills, skillPaths)); + } + + importedSkillsCount = selectedSkills.length; + } + } + + // ========== MARKETPLACE PLUGINS (new PluginImporter flow) ========== + if (importPlugins && marketplaces.length > 0) { + const marketplaceChoice = await ctx.ui.select( + `Found ${marketplaces.length} marketplace(s). Import from marketplace?`, + [ + "Yes - discover plugins and select components", + "Skip marketplaces (use legacy plugin paths only)", + "Cancel", + ] + ); + + if (marketplaceChoice === "Yes - discover plugins and select components") { + // Instantiate PluginImporter and discover + const importer = new PluginImporter(); + const discovery = importer.discover(marketplaces); + + if (discovery.summary.totalPlugins > 0) { + // Present components for selection + const selectedComponents = await selectMarketplaceComponents(ctx, importer, scope); + + if (selectedComponents.length > 0) { + // Run validation (pre-import diagnostics) + const validation = importer.validateImport(selectedComponents); + + // Show diagnostics + if (validation.diagnostics.length > 0) { + const diagMessage = formatDiagnosticsForUser(validation.diagnostics); + ctx.ui.notify(diagMessage, validation.canProceed ? "warning" : "error"); + + // Block if errors exist + if (!validation.canProceed) { + ctx.ui.notify( + "Import blocked due to canonical name conflicts. Please resolve the errors above.", + "error" + ); + return; + } + + // Warn but allow proceed for warnings + const proceed = await ctx.ui.select( + "Warnings detected. Continue with import?", + ["Yes, continue", "Cancel"] + ); + if (proceed !== "Yes, continue") { + return; + } + } + + // Generate manifest and persist + const manifest = importer.getImportManifest(selectedComponents); + persistManifestToSettings(manifest.entries, settingsManager, scope); + + importedMarketplaceComponents = selectedComponents.length; + canonicalNamesPersisted.push(...manifest.entries.map(e => e.canonicalName)); + } + } else { + ctx.ui.notify(`No plugins discovered in ${marketplaces.length} marketplace(s).`, "info"); + } + } + } + + // ========== FLAT PLUGIN PATHS (legacy flow) ========== + if (importPlugins && flat.length > 0) { + // Use legacy discovery for non-marketplace paths + const discoveredPlugins: ClaudePluginCandidate[] = []; + const seen = new Set(); + + for (const root of flat) { + walkDirs(root, (dir) => { + const pkgPath = join(dir, "package.json"); + if (!existsSync(pkgPath)) return; + const resolvedDir = resolve(dir); + if (seen.has(resolvedDir)) return; + seen.add(resolvedDir); + let packageName: string | undefined; + try { + const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { name?: string }; + packageName = pkg.name; + } catch { + packageName = undefined; + } + discoveredPlugins.push({ + type: "plugin", + name: packageName || basename(dir), + packageName, + path: resolvedDir, + root, + sourceLabel: sourceLabel(root), + }); + }, 4); + } + + const sortedPlugins = discoveredPlugins.sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path)); + const selectedPlugins = await chooseMany(ctx, `Claude plugins/packages → ${scope} Pi settings`, sortedPlugins); + + if (selectedPlugins.length > 0) { + const pluginPaths = selectedPlugins.map((plugin) => plugin.path); + if (scope === "project") { + settingsManager.setProjectPackages(mergePackageSources(settingsManager.getProjectSettings().packages, pluginPaths)); + } else { + settingsManager.setPackages(mergePackageSources(settingsManager.getGlobalSettings().packages, pluginPaths)); + } + importedPluginsCount = selectedPlugins.length; + } + } + + // ========== FINAL SUMMARY ========== + if (importedSkillsCount === 0 && importedPluginsCount === 0 && importedMarketplaceComponents === 0) { + ctx.ui.notify("Claude import cancelled or nothing selected.", "info"); + return; + } + + await ctx.waitForIdle(); + await ctx.reload(); + + const lines = [ + `Imported Claude assets into ${scope} config:`, + `- Skills (flat): ${importedSkillsCount}`, + `- Plugins (flat paths): ${importedPluginsCount}`, + `- Marketplace components: ${importedMarketplaceComponents}`, + ]; + if (importedSkillsCount > 0) { + lines.push(`- Skill paths added to Pi settings (${scope}) for availability`); + lines.push(`- Skill refs added to GSD preferences (${scope}) when selected`); + } + if (importedPluginsCount > 0) { + lines.push(`- Plugin/package paths added to Pi settings (${scope}) packages`); + } + if (importedMarketplaceComponents > 0) { + lines.push(`- Canonical names preserved: ${canonicalNamesPersisted.length} entries`); + if (canonicalNamesPersisted.length <= 10) { + lines.push(` Names: ${canonicalNamesPersisted.join(', ')}`); + } + } + ctx.ui.notify(lines.join("\n"), "info"); +} diff --git a/src/resources/extensions/gsd/collision-diagnostics.ts b/src/resources/extensions/gsd/collision-diagnostics.ts new file mode 100644 index 000000000..09ec44c12 --- /dev/null +++ b/src/resources/extensions/gsd/collision-diagnostics.ts @@ -0,0 +1,332 @@ +/** + * Collision Diagnostics Module + * + * Bridges NamespacedRegistry collision data and NamespacedResolver ambiguous + * resolution into a classified diagnostic taxonomy. Provides two functions: + * - analyzeCollisions: Scans registry and resolver state to produce classified diagnostics + * - doctorReport: Formats diagnostics into human-readable output with severity and remediation + * + * This module implements R010 (collision reporting) and R011 (doctor advice) for the + * namespaced component system. + */ + +import type { NamespacedRegistry, RegistryDiagnostic } from './namespaced-registry.js'; +import type { NamespacedResolver, ResolutionResult } from './namespaced-resolver.js'; + +// ============================================================================ +// Type Definitions +// ============================================================================ + +/** + * Classification of collision type. + * - canonical-conflict: Two plugins registered the same canonical name (hard error) + * - shorthand-overlap: Same bare name exists in multiple namespaces (ambiguity) + * - alias-conflict: Alias shadows a canonical name or bare component name + */ +export type CollisionClass = 'canonical-conflict' | 'shorthand-overlap' | 'alias-conflict'; + +/** + * Severity level for diagnostics. + * - error: Hard collision that prevents correct resolution + * - warning: Ambiguity that may cause surprising behavior + */ +export type DiagnosticSeverity = 'error' | 'warning'; + +/** + * A classified diagnostic with full context for remediation. + */ +export interface ClassifiedDiagnostic { + /** The collision classification */ + class: CollisionClass; + + /** Severity level */ + severity: DiagnosticSeverity; + + /** All canonical names involved in the collision */ + involvedCanonicalNames: string[]; + + /** File paths to the conflicting components */ + filePaths: string[]; + + /** Human-readable remediation advice */ + remediation: string; + + /** Optional: the bare name causing ambiguity (shorthand-overlap only) */ + ambiguousBareName?: string; + + /** Optional: the alias string (alias-conflict only) */ + alias?: string; + + /** Optional: the canonical name the alias points to (alias-conflict only) */ + aliasTarget?: string; + + /** Optional: type of alias conflict */ + aliasConflictType?: 'shadows-canonical' | 'shadows-bare-name'; +} + +/** + * Doctor report with summary statistics and formatted entries. + */ +export interface DoctorReport { + /** Summary counts by class */ + summary: { + /** Total diagnostics */ + total: number; + /** Canonical conflicts (errors) */ + canonicalConflicts: number; + /** Shorthand overlaps (warnings) */ + shorthandOverlaps: number; + /** Alias conflicts (warnings) */ + aliasConflicts: number; + }; + + /** Formatted report entries */ + entries: string[]; +} + +// ============================================================================ +// Implementation +// ============================================================================ + +/** + * Analyze a registry and resolver to produce classified diagnostics. + * + * This function: + * 1. Reads registry.getDiagnostics() for canonical conflicts (→ error severity) + * 2. Groups registry.getAll() by bare component.name + * 3. For groups with 2+ entries, calls resolver.resolve(bareName) to confirm ambiguity + * 4. Produces warning diagnostics for ambiguous shorthand resolution + * + * @param registry - The namespaced registry to analyze + * @param resolver - The resolver to test ambiguity + * @returns Array of classified diagnostics + */ +export function analyzeCollisions( + registry: NamespacedRegistry, + resolver: NamespacedResolver +): ClassifiedDiagnostic[] { + const diagnostics: ClassifiedDiagnostic[] = []; + + // Step 1: Process canonical conflicts from registry diagnostics + const registryDiagnostics = registry.getDiagnostics(); + for (const diag of registryDiagnostics) { + if (diag.type === 'collision') { + diagnostics.push({ + class: 'canonical-conflict', + severity: 'error', + involvedCanonicalNames: [diag.collision.canonicalName], + filePaths: [diag.collision.winnerPath, diag.collision.loserPath], + remediation: `Canonical name "${diag.collision.canonicalName}" registered multiple times. ` + + `The first registration (${diag.collision.winnerSource ?? 'unknown source'}) ` + + `took precedence over subsequent registration (${diag.collision.loserSource ?? 'unknown source'}). ` + + `Rename one of the conflicting components to resolve.`, + }); + } + } + + // Step 2: Find shorthand overlaps by grouping components by bare name + const components = registry.getAll(); + const byBareName = new Map(); + + for (const component of components) { + const bareName = component.name; + if (!byBareName.has(bareName)) { + byBareName.set(bareName, []); + } + byBareName.get(bareName)!.push(component); + } + + // Step 3: For groups with 2+ entries, check if resolver confirms ambiguity + for (const [bareName, candidates] of byBareName) { + if (candidates.length >= 2) { + // Use resolver to confirm ambiguity + const result = resolver.resolve(bareName); + + if (result.resolution === 'ambiguous') { + // This is a shorthand overlap + const canonicalNames = candidates.map(c => c.canonicalName); + const filePaths = candidates.map(c => c.filePath); + + diagnostics.push({ + class: 'shorthand-overlap', + severity: 'warning', + involvedCanonicalNames: canonicalNames, + filePaths, + remediation: formatShorthandRemediation(bareName, canonicalNames), + ambiguousBareName: bareName, + }); + } + // If resolution is 'shorthand' or 'local-first', the overlap is resolved + // unambiguously by the resolver, so we don't warn + } + } + + // Step 4: Check for alias conflicts + const aliases = registry.getAliases(); + const canonicalNamesSet = new Set(components.map(c => c.canonicalName)); + + for (const [alias, targetCanonical] of aliases) { + // Check if alias shadows a canonical name + // (This can happen if a component was registered AFTER the alias was created) + if (canonicalNamesSet.has(alias)) { + const shadowedComponent = components.find(c => c.canonicalName === alias); + const aliasedComponent = components.find(c => c.canonicalName === targetCanonical); + + diagnostics.push({ + class: 'alias-conflict', + severity: 'warning', + involvedCanonicalNames: [alias, targetCanonical], + filePaths: [ + shadowedComponent?.filePath ?? '', + aliasedComponent?.filePath ?? '', + ], + remediation: formatAliasShadowsCanonicalRemediation(alias, targetCanonical), + alias, + aliasTarget: targetCanonical, + aliasConflictType: 'shadows-canonical', + }); + continue; // Skip further checks for this alias + } + + // Check if alias shadows a bare name (matches component.name in any namespace) + const matchingBareNames = components.filter(c => c.name === alias); + if (matchingBareNames.length > 0) { + const filePaths = matchingBareNames.map(c => c.filePath); + const aliasedComponent = components.find(c => c.canonicalName === targetCanonical); + if (aliasedComponent) filePaths.push(aliasedComponent.filePath); + + diagnostics.push({ + class: 'alias-conflict', + severity: 'warning', + involvedCanonicalNames: [targetCanonical, ...matchingBareNames.map(c => c.canonicalName)], + filePaths, + remediation: formatAliasShadowsBareNameRemediation(alias, targetCanonical, matchingBareNames.map(c => c.canonicalName)), + alias, + aliasTarget: targetCanonical, + aliasConflictType: 'shadows-bare-name', + }); + } + } + + return diagnostics; +} + +/** + * Format remediation advice for shorthand overlap. + * + * @param bareName - The ambiguous bare name + * @param canonicalNames - All canonical names that match + * @returns Human-readable remediation message + */ +function formatShorthandRemediation(bareName: string, canonicalNames: string[]): string { + const suggestions = canonicalNames + .map(cn => `\`${cn}\``) + .join(', '); + + return `Bare name "${bareName}" is ambiguous across ${canonicalNames.length} namespaces. ` + + `Use a canonical name (${suggestions}) to avoid ambiguity.`; +} + +/** + * Format remediation advice for alias shadowing a canonical name. + * + * @param alias - The alias that shadows a canonical name + * @param targetCanonical - The canonical name the alias points to + * @returns Human-readable remediation message + */ +function formatAliasShadowsCanonicalRemediation(alias: string, targetCanonical: string): string { + return `Alias "${alias}" shadows an existing canonical name. ` + + `The alias points to "${targetCanonical}", but resolving "${alias}" will now match the component, not the alias. ` + + `Consider rename or remove the alias to avoid confusion.`; +} + +/** + * Format remediation advice for alias shadowing a bare name. + * + * @param alias - The alias that shadows bare names + * @param targetCanonical - The canonical name the alias points to + * @param shadowedCanonicals - The canonical names whose bare names are shadowed + * @returns Human-readable remediation message + */ +function formatAliasShadowsBareNameRemediation( + alias: string, + targetCanonical: string, + shadowedCanonicals: string[] +): string { + const shadowed = shadowedCanonicals.map(cn => `\`${cn}\``).join(', '); + return `Alias "${alias}" shadows ${shadowedCanonicals.length} component(s) with the same bare name (${shadowed}). ` + + `Resolving "${alias}" will use the alias (pointing to "${targetCanonical}"), not shorthand resolution. ` + + `Use canonical names to be explicit, or rename the alias if this is unintended.`; +} + +/** + * Format diagnostics into a human-readable doctor report. + * + * Each diagnostic is formatted with: + * - Severity icon (❌ error / ⚠️ warning) + * - Description of the issue + * - Involved file paths + * - Remediation advice + * + * @param diagnostics - Array of classified diagnostics + * @returns Doctor report with summary and formatted entries + */ +export function doctorReport(diagnostics: ClassifiedDiagnostic[]): DoctorReport { + const summary = { + total: diagnostics.length, + canonicalConflicts: diagnostics.filter(d => d.class === 'canonical-conflict').length, + shorthandOverlaps: diagnostics.filter(d => d.class === 'shorthand-overlap').length, + aliasConflicts: diagnostics.filter(d => d.class === 'alias-conflict').length, + }; + + const entries = diagnostics.map(diagnostic => formatDiagnosticEntry(diagnostic)); + + return { summary, entries }; +} + +/** + * Format a single diagnostic entry for display. + * + * @param diagnostic - The diagnostic to format + * @returns Formatted string entry + */ +function formatDiagnosticEntry(diagnostic: ClassifiedDiagnostic): string { + const icon = diagnostic.severity === 'error' ? '❌' : '⚠️'; + const lines: string[] = []; + + // Header with severity and class + lines.push(`${icon} ${diagnostic.class.toUpperCase()}`); + + // Description + if (diagnostic.class === 'canonical-conflict') { + lines.push(` Canonical name conflict: ${diagnostic.involvedCanonicalNames[0]}`); + } else if (diagnostic.class === 'alias-conflict') { + if (diagnostic.aliasConflictType === 'shadows-canonical') { + lines.push(` Alias "${diagnostic.alias}" shadows canonical name (points to ${diagnostic.aliasTarget})`); + } else { + lines.push(` Alias "${diagnostic.alias}" shadows bare name (points to ${diagnostic.aliasTarget})`); + } + } else { + lines.push(` Shorthand overlap: "${diagnostic.ambiguousBareName}" matches ${diagnostic.involvedCanonicalNames.length} components`); + } + + // File paths + lines.push(' Files:'); + for (const path of diagnostic.filePaths) { + lines.push(` - ${path}`); + } + + // Remediation + lines.push(` Remediation: ${diagnostic.remediation}`); + + return lines.join('\n'); +} + +// ============================================================================ +// Exports +// ============================================================================ + +export default { + analyzeCollisions, + doctorReport, +}; diff --git a/src/resources/extensions/gsd/commands.ts b/src/resources/extensions/gsd/commands.ts index d62bbdf6f..afaca8d4b 100644 --- a/src/resources/extensions/gsd/commands.ts +++ b/src/resources/extensions/gsd/commands.ts @@ -26,7 +26,8 @@ import { loadEffectiveGSDPreferences, resolveAllSkillReferences, } from "./preferences.js"; -import { loadFile, saveFile, appendOverride, appendKnowledge } from "./files.js"; +import { loadFile, saveFile, appendOverride, appendKnowledge, splitFrontmatter, parseFrontmatterMap } from "./files.js"; +import { runClaudeImportFlow } from "./claude-import.js"; import { formatDoctorIssuesForPrompt, formatDoctorReport, @@ -100,7 +101,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void { if (parts[0] === "prefs" && parts.length <= 2) { const subPrefix = parts[1] ?? ""; - return ["global", "project", "status", "wizard", "setup"] + return ["global", "project", "status", "wizard", "setup", "import-claude"] .filter((cmd) => cmd.startsWith(subPrefix)) .map((cmd) => ({ value: `prefs ${cmd}`, label: cmd })); } @@ -507,6 +508,15 @@ async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise< return; } + if (trimmed === "import-claude" || trimmed === "import-claude global") { + await handleImportClaude(ctx, "global"); + return; + } + + if (trimmed === "import-claude project") { + await handleImportClaude(ctx, "project"); + return; + } if (trimmed === "status") { const globalPrefs = loadGlobalGSDPreferences(); const projectPrefs = loadProjectGSDPreferences(); @@ -537,7 +547,38 @@ async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise< return; } - ctx.ui.notify("Usage: /gsd prefs [global|project|status|wizard|setup]", "info"); + ctx.ui.notify("Usage: /gsd prefs [global|project|status|wizard|setup|import-claude [global|project]]", "info"); +} + +async function handleImportClaude(ctx: ExtensionCommandContext, scope: "global" | "project"): Promise { + const path = scope === "project" ? getProjectGSDPreferencesPath() : getGlobalGSDPreferencesPath(); + if (!existsSync(path)) { + await ensurePreferencesFile(path, ctx, scope); + } + + const readPrefs = (): Record => { + if (!existsSync(path)) return { version: 1 }; + const content = readFileSync(path, "utf-8"); + const [frontmatterLines] = splitFrontmatter(content); + return frontmatterLines ? parseFrontmatterMap(frontmatterLines) : { version: 1 }; + }; + + const writePrefs = async (prefs: Record): Promise => { + prefs.version = prefs.version || 1; + const frontmatter = serializePreferencesToFrontmatter(prefs); + let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n"; + if (existsSync(path)) { + const existingContent = readFileSync(path, "utf-8"); + const closingIdx = existingContent.indexOf("\n---", existingContent.indexOf("---")); + if (closingIdx !== -1) { + const afterFrontmatter = existingContent.slice(closingIdx + 4); + if (afterFrontmatter.trim()) body = afterFrontmatter; + } + } + await saveFile(path, `---\n${frontmatter}---${body}`); + }; + + await runClaudeImportFlow(ctx, scope, readPrefs, writePrefs); } async function handlePrefsMode(ctx: ExtensionCommandContext, scope: "global" | "project"): Promise { diff --git a/src/resources/extensions/gsd/docs/claude-marketplace-import.md b/src/resources/extensions/gsd/docs/claude-marketplace-import.md new file mode 100644 index 000000000..b9126b1e8 --- /dev/null +++ b/src/resources/extensions/gsd/docs/claude-marketplace-import.md @@ -0,0 +1,214 @@ +# Claude Marketplace Import + +This document describes the Claude marketplace import feature in GSD: what it reads, what it imports, what it persists, and what it does not translate into active GSD/Pi runtime behavior. + +--- + +## What this feature does + +GSD can read Claude Code marketplace catalogs, inspect the plugins they reference, and import selected Claude skills into GSD/Pi while preserving Claude-style namespace identity. + +The interactive entry point is: + +```text +/gsd prefs import-claude +``` + +You can also choose scope explicitly: + +```text +/gsd prefs import-claude global +/gsd prefs import-claude project +``` + +--- + +## Claude Code model this feature follows + +Anthropic documents Claude marketplaces as sources users add with: + +```text +/plugin marketplace add +``` + +A marketplace contains a catalog at: + +```text +.claude-plugin/marketplace.json +``` + +Anthropic distinguishes between: + +- **Marketplace source** — where Claude fetches `marketplace.json` +- **Plugin source** — where Claude fetches each plugin listed in that marketplace +- **Installed plugin cache** — Claude copies installed plugin payloads into: + +```text +~/.claude/plugins/cache +``` + +Anthropic also documents user-added marketplace sources under: + +```text +~/.claude/plugins/marketplaces +``` + +GSD aligns its Claude import flow to that model. + +--- + +## Where GSD looks + +For Claude plugin and marketplace material, GSD prefers Claude-managed locations first: + +1. `~/.claude/plugins/marketplaces` +2. `~/.claude/plugins/cache` +3. `~/.claude/plugins` + +After that, GSD still allows local clone-style convenience paths such as sibling repos or `~/repos/...` paths. Those fallbacks remain supported for developer workflows, but they are not the primary Claude storage model. + +--- + +## What GSD imports + +### Imported into GSD/Pi settings + +- Claude skills discovered directly from configured skill roots +- Marketplace-derived skills + +Imported marketplace skills preserve canonical namespace identity, for example: + +```text +python3-development:stinkysnake +scientific-method:experiment-protocol +``` + +### Discovered, modeled, and validated + +- Marketplace-derived agents + +### Discovered but not translated into active Pi-native runtime behavior + +- hooks +- MCP server definitions +- LSP server definitions +- other plugin metadata that does not currently map directly into active GSD/Pi runtime surfaces + +--- + +## Import flow + +The import flow does the following: + +1. discover Claude skills and marketplace/plugin roots +2. identify marketplace roots by checking for `.claude-plugin/marketplace.json` +3. inspect discovered plugins and inventory their components +4. let you select components to import +5. validate the selection for canonical conflicts and ambiguity +6. persist imported resources into GSD/Pi settings + +--- + +## Namespace behavior + +GSD preserves Claude plugin namespace semantics rather than flattening plugin components into anonymous global names. + +### Canonical references + +Canonical references remain available for imported components: + +- skills: `plugin-name:skill-name` +- agents: `plugin-name:agent-name` + +### Shorthand + +GSD supports shorthand lookup when it is unambiguous. + +### Local-first resolution + +When a namespaced component refers to another component by bare name, GSD tries the same plugin namespace first before broader lookup. + +--- + +## Important safeguard: marketplace agent directories are not stored as package sources + +Claude plugin agent directories are markdown agent-definition directories, for example: + +```text +.../plugins/python3-development/agents +``` + +GSD does **not** persist imported marketplace agent directories into: + +```json +settings.packages +``` + +This is intentional. + +### Why + +Persisting an `.../agents` directory into `settings.packages` can cause Pi startup to treat that directory as an extension/package root. In real host validation, that produced extension loader failures such as: + +```text +Cannot find module '.../agents' +``` + +GSD now avoids writing those entries. + +--- + +## Settings effects + +### Skills + +Imported skills are persisted into Pi skill settings. Depending on the selection path, they may also be added to GSD preferences. + +### Marketplace agents + +Marketplace agents remain part of the import model and validation surface, but their `agents/` directories are not persisted as package roots. + +--- + +## Diagnostics + +GSD distinguishes between: + +- **canonical conflicts** — hard errors +- **shorthand overlaps** — warnings when canonical names remain distinct +- **alias conflicts** — diagnostics for alias collisions or shadowing + +This allows imported marketplace content to be validated without reporting valid overlap as fatal breakage. + +--- + +## Verification status of this feature + +This feature has been verified in three ways: + +1. **Contract/unit tests** for parsing, namespacing, resolution, diagnostics, and import behavior +2. **Portable integration-style tests** using local or cloned marketplace fixtures +3. **Real host validation** against the installed `gsd` binary and actual Claude-managed directories on the host machine + +Real host validation included: + +- clean startup of the installed `gsd` binary after fixing stale bad settings +- successful invocation of an imported skill (`/stinkysnake`) +- successful execution of `/gsd prefs import-claude global` +- verification that imported marketplace agent directories were **not** reintroduced into `settings.packages` + +--- + +## Current limitations + +- GSD does not yet translate every Claude plugin component type into active Pi-native runtime behavior +- marketplace-derived agents are not persisted as package roots, by design +- clone-style local fallbacks still exist for developer convenience, even though Claude-managed marketplace/plugin locations are preferred first + +--- + +## References + +- Anthropic: Claude Code settings +- Anthropic: Create and distribute a plugin marketplace +- Anthropic: Plugins and plugin reference diff --git a/src/resources/extensions/gsd/docs/preferences-reference.md b/src/resources/extensions/gsd/docs/preferences-reference.md index 0700b51cf..342e0cf63 100644 --- a/src/resources/extensions/gsd/docs/preferences-reference.md +++ b/src/resources/extensions/gsd/docs/preferences-reference.md @@ -10,6 +10,7 @@ Full documentation for `~/.gsd/preferences.md` (global) and `.gsd/preferences.md - Prefer explicit skill names or absolute paths. - Use absolute paths for personal/local skills when you want zero ambiguity. - These preferences guide which skills GSD should load and follow; they do not override higher-priority instructions in the current conversation. +- For Claude marketplace/plugin import behavior, see `~/.gsd/agent/extensions/gsd/docs/claude-marketplace-import.md`. --- diff --git a/src/resources/extensions/gsd/marketplace-discovery.ts b/src/resources/extensions/gsd/marketplace-discovery.ts new file mode 100644 index 000000000..cc8a16468 --- /dev/null +++ b/src/resources/extensions/gsd/marketplace-discovery.ts @@ -0,0 +1,507 @@ +/** + * Marketplace Discovery Module + * + * Reads marketplace.json from Claude marketplace repos, resolves plugin source paths, + * parses plugin.json manifests, and inventories available components (skills, agents, commands, MCP servers, LSP servers, hooks). + * + * Marketplace roots should reflect the Claude Code model documented by Anthropic: + * users add a marketplace source with `/plugin marketplace add ...`, Claude stores + * marketplace sources under `~/.claude/plugins/marketplaces/`, and installed plugin + * payloads are copied into `~/.claude/plugins/cache/`. + * + * Handles two marketplace catalog shapes observed in the wild: + * 1. jamie-style: marketplace.json has {name, source} entries; plugins have .claude-plugin/plugin.json + * 2. official-style: marketplace.json entries contain inline metadata + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +// ============================================================================ +// Type Definitions +// ============================================================================ + +/** Owner information in marketplace manifest */ +export interface MarketplaceOwner { + name: string; + email?: string; + url?: string; +} + +/** Marketplace metadata */ +export interface MarketplaceMetadata { + description?: string; + version?: string; +} + +/** Source can be a relative path or a complex object (github, url, git-subdir) */ +export type PluginSource = string | { + source?: string; + repo?: string; + url?: string; + path?: string; + sha?: string; + ref?: string; +}; + +/** Marketplace plugin entry - minimal info from marketplace.json */ +export interface MarketplacePluginEntry { + name: string; + source: PluginSource; + // Optional inline metadata (official-style) + description?: string; + version?: string; + author?: MarketplaceOwner; + category?: string; + homepage?: string; + strict?: boolean; + mcpServers?: Record; + lspServers?: Record; + tags?: string[]; +} + +/** Complete marketplace manifest */ +export interface MarketplaceManifest { + $schema?: string; + name: string; + description?: string; + owner?: MarketplaceOwner; + metadata?: MarketplaceMetadata; + plugins: MarketplacePluginEntry[]; +} + +/** Plugin manifest from .claude-plugin/plugin.json */ +export interface PluginManifest { + name: string; + description?: string; + version?: string; + author?: MarketplaceOwner; + homepage?: string; + mcpServers?: Record; + lspServers?: Record; + // Additional fields that might be present + [key: string]: unknown; +} + +/** Inventory of components in a plugin */ +export interface PluginComponentInventory { + skills: string[]; + agents: string[]; + commands: string[]; + mcpServers: Record; + lspServers: Record; + hooks?: string[]; +} + +/** Discovered plugin with all metadata and inventory */ +export interface DiscoveredPlugin { + name: string; + canonicalName: string; + source: PluginSource; + resolvedPath: string | null; + status: 'ok' | 'error'; + error?: string; + // Metadata sources + manifestSource: 'plugin.json' | 'marketplace-inline' | 'derived'; + description?: string; + version?: string; + author?: MarketplaceOwner; + category?: string; + homepage?: string; + // Component inventory + inventory: PluginComponentInventory; +} + +/** Result of marketplace discovery */ +export interface MarketplaceDiscoveryResult { + status: 'ok' | 'error'; + error?: string; + marketplacePath: string; + marketplaceName: string; + pluginFormat: 'jamie-style' | 'official-style' | 'unknown'; + plugins: DiscoveredPlugin[]; + summary: { + total: number; + ok: number; + error: number; + }; +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Check if a source path is a relative local path (not a URL or complex source) + */ +function isLocalSource(source: PluginSource): source is string { + if (typeof source === 'string') { + return !source.startsWith('http://') && + !source.startsWith('https://') && + !source.startsWith('git@') && + !source.includes('://'); + } + return false; +} + +/** + * Resolve a relative source path to an absolute directory path + */ +export function resolvePluginRoot(repoRoot: string, source: PluginSource): string | null { + if (!isLocalSource(source)) { + // External source (URL, git repo) - can't resolve locally + return null; + } + + // Handle both ./plugins/name and plugins/name formats + let resolvedPath = source; + if (source.startsWith('./')) { + resolvedPath = source.slice(2); + } + + const absolutePath = path.resolve(repoRoot, resolvedPath); + return absolutePath; +} + +// ============================================================================ +// Core Functions +// ============================================================================ + +/** + * Parse marketplace.json from a marketplace repository root + * + * @param repoRoot - Absolute path to the marketplace repository root + * @returns Parsed marketplace manifest or error + */ +export function parseMarketplaceJson(repoRoot: string): + | { success: true; manifest: MarketplaceManifest } + | { success: false; error: string } { + + const marketplacePath = path.join(repoRoot, '.claude-plugin', 'marketplace.json'); + + // Check if file exists + if (!fs.existsSync(marketplacePath)) { + return { + success: false, + error: `marketplace.json not found at ${marketplacePath}` + }; + } + + // Read and parse JSON + let content: string; + try { + content = fs.readFileSync(marketplacePath, 'utf-8'); + } catch (err) { + return { + success: false, + error: `Failed to read marketplace.json: ${err instanceof Error ? err.message : String(err)}` + }; + } + + let parsed: unknown; + try { + parsed = JSON.parse(content); + } catch (err) { + return { + success: false, + error: `Failed to parse marketplace.json: ${err instanceof Error ? err.message : String(err)}` + }; + } + + // Validate structure + if (!parsed || typeof parsed !== 'object') { + return { + success: false, + error: 'marketplace.json is not a valid JSON object' + }; + } + + const manifest = parsed as MarketplaceManifest; + + if (!manifest.name) { + return { + success: false, + error: 'marketplace.json missing required field: name' + }; + } + + if (!Array.isArray(manifest.plugins)) { + return { + success: false, + error: 'marketplace.json missing or invalid field: plugins (must be array)' + }; + } + + return { success: true, manifest }; +} + +/** + * Inspect a plugin directory to extract metadata and inventory + * + * @param pluginDir - Absolute path to the plugin directory + * @param marketplaceEntry - Optional marketplace entry for inline metadata fallback + * @returns Discovered plugin information + */ +export function inspectPlugin( + pluginDir: string, + marketplaceEntry?: MarketplacePluginEntry +): DiscoveredPlugin { + const result: DiscoveredPlugin = { + name: marketplaceEntry?.name || path.basename(pluginDir), + canonicalName: marketplaceEntry?.name || path.basename(pluginDir), + source: marketplaceEntry?.source || './', + resolvedPath: pluginDir, + status: 'ok', + manifestSource: 'derived', + inventory: { + skills: [], + agents: [], + commands: [], + mcpServers: {}, + lspServers: {}, + hooks: [] + } + }; + + // Check if directory exists + if (!fs.existsSync(pluginDir)) { + result.status = 'error'; + result.error = `Plugin directory not found: ${pluginDir}`; + return result; + } + + // Try to read plugin.json from .claude-plugin/ + const pluginJsonPath = path.join(pluginDir, '.claude-plugin', 'plugin.json'); + + if (fs.existsSync(pluginJsonPath)) { + try { + const content = fs.readFileSync(pluginJsonPath, 'utf-8'); + const manifest = JSON.parse(content) as PluginManifest; + + // Extract metadata from plugin.json + result.manifestSource = 'plugin.json'; + result.description = manifest.description; + result.version = manifest.version; + result.author = manifest.author; + result.homepage = manifest.homepage; + + if (manifest.mcpServers) { + result.inventory.mcpServers = manifest.mcpServers; + } + if (manifest.lspServers) { + result.inventory.lspServers = manifest.lspServers; + } + } catch (err) { + // Fall back to marketplace inline or derived + result.error = `Failed to parse plugin.json: ${err instanceof Error ? err.message : String(err)}`; + } + } + + // If no plugin.json, use marketplace inline metadata + if (result.manifestSource === 'derived' && marketplaceEntry) { + result.manifestSource = 'marketplace-inline'; + result.description = marketplaceEntry.description; + result.version = marketplaceEntry.version; + result.author = marketplaceEntry.author; + result.category = marketplaceEntry.category; + result.homepage = marketplaceEntry.homepage; + + if (marketplaceEntry.mcpServers) { + result.inventory.mcpServers = marketplaceEntry.mcpServers; + } + if (marketplaceEntry.lspServers) { + result.inventory.lspServers = marketplaceEntry.lspServers; + } + } + + // Try to read plugin.json in root (alternative location) + const altPluginJsonPath = path.join(pluginDir, 'plugin.json'); + if (fs.existsSync(altPluginJsonPath) && result.manifestSource === 'derived') { + try { + const content = fs.readFileSync(altPluginJsonPath, 'utf-8'); + const manifest = JSON.parse(content) as PluginManifest; + + result.manifestSource = 'plugin.json'; + if (!result.description && manifest.description) { + result.description = manifest.description; + } + if (!result.version && manifest.version) { + result.version = manifest.version; + } + if (!result.author && manifest.author) { + result.author = manifest.author; + } + } catch { + // Ignore parse errors for alternative location + } + } + + // Inventory component directories + const skillsDir = path.join(pluginDir, 'skills'); + if (fs.existsSync(skillsDir) && fs.statSync(skillsDir).isDirectory()) { + try { + result.inventory.skills = fs.readdirSync(skillsDir) + .filter(item => { + const itemPath = path.join(skillsDir, item); + return fs.statSync(itemPath).isDirectory() || item.endsWith('.md'); + }); + } catch { + // Ignore read errors + } + } + + const agentsDir = path.join(pluginDir, 'agents'); + if (fs.existsSync(agentsDir) && fs.statSync(agentsDir).isDirectory()) { + try { + result.inventory.agents = fs.readdirSync(agentsDir) + .filter(item => { + const itemPath = path.join(agentsDir, item); + return fs.statSync(itemPath).isDirectory() || item.endsWith('.md'); + }); + } catch { + // Ignore read errors + } + } + + const commandsDir = path.join(pluginDir, 'commands'); + if (fs.existsSync(commandsDir) && fs.statSync(commandsDir).isDirectory()) { + try { + result.inventory.commands = fs.readdirSync(commandsDir) + .filter(item => { + const itemPath = path.join(commandsDir, item); + return fs.statSync(itemPath).isDirectory() || item.endsWith('.md'); + }); + } catch { + // Ignore read errors + } + } + + // Also check for hooks at root level (jamie-style uses 'hooks/', not '.claude-plugin/hooks') + const rootHooksDir = path.join(pluginDir, 'hooks'); + if (fs.existsSync(rootHooksDir) && fs.statSync(rootHooksDir).isDirectory()) { + try { + const rootHooks = fs.readdirSync(rootHooksDir) + .filter(item => { + const itemPath = path.join(rootHooksDir, item); + return fs.statSync(itemPath).isDirectory() || item.endsWith('.md') || item.endsWith('.json'); + }); + const mergedHooks = [...(result.inventory.hooks || []), ...rootHooks]; + result.inventory.hooks = Array.from(new Set(mergedHooks)); + } catch { + // Ignore read errors + } + } + + // Also check .claude-plugin/hooks (official-style) + const hooksDir = path.join(pluginDir, '.claude-plugin', 'hooks'); + if (fs.existsSync(hooksDir) && fs.statSync(hooksDir).isDirectory()) { + try { + const pluginHooks = fs.readdirSync(hooksDir) + .filter(item => { + const itemPath = path.join(hooksDir, item); + return fs.statSync(itemPath).isDirectory() || item.endsWith('.md'); + }); + const mergedHooks = [...(result.inventory.hooks || []), ...pluginHooks]; + result.inventory.hooks = Array.from(new Set(mergedHooks)); + } catch { + // Ignore read errors + } + } + + return result; +} + +/** + * Discover all plugins in a marketplace repository + * + * @param repoRoot - Absolute or relative path to the marketplace repository + * @returns Marketplace discovery result with all plugins + */ +export function discoverMarketplace(repoRoot: string): MarketplaceDiscoveryResult { + // Resolve to absolute path + const absoluteRepoRoot = path.resolve(repoRoot); + + // Parse marketplace.json + const parseResult = parseMarketplaceJson(absoluteRepoRoot); + + if (parseResult.success === false) { + return { + status: 'error', + error: parseResult.error, + marketplacePath: path.join(absoluteRepoRoot, '.claude-plugin', 'marketplace.json'), + marketplaceName: path.basename(absoluteRepoRoot), + pluginFormat: 'unknown', + plugins: [], + summary: { total: 0, ok: 0, error: 0 } + }; + } + + const manifest = parseResult.manifest; + + // Determine plugin format based on structure + const pluginFormat: 'jamie-style' | 'official-style' | 'unknown' = + manifest.plugins.every(p => p.source && !p.description && !p.version && !p.lspServers) + ? 'jamie-style' + : manifest.plugins.every(p => p.source && (p.description || p.version || p.lspServers)) + ? 'official-style' + : 'unknown'; + + // Discover each plugin + const plugins: DiscoveredPlugin[] = manifest.plugins.map(entry => { + const resolvedPath = resolvePluginRoot(absoluteRepoRoot, entry.source); + + if (!resolvedPath) { + // External source - can't resolve locally + return { + name: entry.name, + canonicalName: entry.name, + source: entry.source, + resolvedPath: null, + status: 'ok', + manifestSource: 'marketplace-inline', + description: entry.description, + version: entry.version, + author: entry.author, + category: entry.category, + homepage: entry.homepage, + inventory: { + skills: [], + agents: [], + commands: [], + mcpServers: entry.mcpServers || {}, + lspServers: entry.lspServers || {}, + hooks: [] + } + }; + } + + return inspectPlugin(resolvedPath, entry); + }); + + // Calculate summary + const summary = { + total: plugins.length, + ok: plugins.filter(p => p.status === 'ok').length, + error: plugins.filter(p => p.status === 'error').length + }; + + return { + status: summary.error > 0 ? 'error' : 'ok', + marketplacePath: path.join(absoluteRepoRoot, '.claude-plugin', 'marketplace.json'), + marketplaceName: manifest.name, + pluginFormat, + plugins, + summary + }; +} + +// ============================================================================ +// Export all types and functions +// ============================================================================ + +export default { + parseMarketplaceJson, + inspectPlugin, + discoverMarketplace, + resolvePluginRoot +}; diff --git a/src/resources/extensions/gsd/namespaced-registry.ts b/src/resources/extensions/gsd/namespaced-registry.ts new file mode 100644 index 000000000..ec6f05089 --- /dev/null +++ b/src/resources/extensions/gsd/namespaced-registry.ts @@ -0,0 +1,467 @@ +/** + * Namespaced Component Registry Module + * + * Provides the canonical identity model for imported plugin components. + * Supports both namespaced (plugin:component) and flat (bare name) components, + * detects collisions at registration time, and provides lookup by canonical name + * or namespace listing. + * + * This registry serves as the bridge between S01's plugin discovery output + * and Pi's internal component resolution system. + */ + +import type { DiscoveredPlugin } from './marketplace-discovery.js'; + +// ============================================================================ +// Type Definitions +// ============================================================================ + +/** + * Component type enumeration. + * Matches the component categories discovered by S01. + */ +export type ComponentType = 'skill' | 'agent'; + +/** + * A component entry in the namespaced registry. + * + * Components can be: + * - Namespaced: `${namespace}:${name}` (e.g., "my-plugin:code-review") + * - Flat: `${name}` (e.g., "code-review" for backward compatibility) + */ +export interface NamespacedComponent { + /** The component's local name (e.g., "code-review") */ + name: string; + + /** The plugin namespace (e.g., "my-plugin"). Undefined for flat components. */ + namespace: string | undefined; + + /** The computed canonical identifier: `${namespace}:${name}` or bare `name` */ + canonicalName: string; + + /** Component type: skill or agent */ + type: ComponentType; + + /** Absolute path to the component's definition file */ + filePath: string; + + /** Source identifier (e.g., "plugin:my-plugin", "user", "project") */ + source: string; + + /** Optional description from the component's frontmatter */ + description: string | undefined; + + /** Extensible metadata bag for plugin origin info */ + metadata: { + /** Plugin version if available */ + pluginVersion?: string; + /** Plugin author if available */ + pluginAuthor?: string; + /** Plugin homepage if available */ + pluginHomepage?: string; + /** Plugin category if available */ + pluginCategory?: string; + /** Original component directory name */ + componentDir?: string; + /** Additional plugin-specific metadata */ + [key: string]: unknown; + }; +} + +/** + * Collision information for registry diagnostics. + * Mirrors the ResourceCollision pattern from pi-coding-agent. + */ +export interface RegistryCollision { + /** The canonical name that collided (e.g., "my-plugin:code-review") */ + canonicalName: string; + + /** Path to the component that won (first registered) */ + winnerPath: string; + + /** Path to the component that lost (subsequent duplicate) */ + loserPath: string; + + /** Source of the winning component */ + winnerSource?: string; + + /** Source of the losing component */ + loserSource?: string; +} + +/** + * Diagnostic entry for registry operations. + * Currently only reports collisions, but extensible for future diagnostics. + */ +export interface RegistryDiagnostic { + /** Diagnostic type */ + type: 'collision'; + + /** Human-readable message */ + message: string; + + /** Collision details */ + collision: RegistryCollision; +} + +/** + * Result of an alias registration attempt. + * Successful registrations return success: true. + * Failed registrations return success: false with a reason. + */ +export interface AliasRegistrationResult { + /** Whether the registration succeeded */ + success: boolean; + + /** On failure, the reason for rejection */ + reason?: 'canonical-not-found' | 'shadows-canonical' | 'duplicate-alias'; + + /** Human-readable message */ + message?: string; +} + +// ============================================================================ +// NamespacedRegistry Class +// ============================================================================ + +/** + * Registry for namespaced plugin components. + * + * Features: + * - Computes canonical names from namespace + name + * - Detects and reports collisions at registration time + * - First registration wins; subsequent duplicates return diagnostic + * - Lookup by canonical name or namespace listing + * - Compatible with both namespaced and flat (non-namespaced) components + * + * Usage: + * ```typescript + * const registry = new NamespacedRegistry(); + * + * // Register a namespaced component + * const diag = registry.register({ + * name: 'code-review', + * namespace: 'my-plugin', + * type: 'skill', + * filePath: '/plugins/my-plugin/skills/code-review/SKILL.md', + * source: 'plugin:my-plugin', + * description: 'Reviews code for quality issues', + * metadata: { pluginVersion: '1.0.0' } + * }); + * + * // Lookup by canonical name + * const skill = registry.getByCanonical('my-plugin:code-review'); + * + * // List all components in a namespace + * const allSkills = registry.getByNamespace('my-plugin'); + * ``` + */ +export class NamespacedRegistry { + /** Internal storage: canonicalName -> component */ + private components = new Map(); + + /** Internal storage: alias -> canonicalName */ + private aliasMap = new Map(); + + /** Collision diagnostics collected during registration */ + private diagnostics: RegistryDiagnostic[] = []; + + /** + * Register a component in the registry. + * + * Computes the canonical name as `${namespace}:${name}` when namespace is present, + * or bare `name` otherwise. Returns a diagnostic if the canonical name already exists. + * + * @param component - Component data (without canonicalName, which is computed) + * @returns Diagnostic if collision detected, undefined otherwise + */ + register(component: Omit): RegistryDiagnostic | undefined { + // Compute canonical name + const canonicalName = component.namespace + ? `${component.namespace}:${component.name}` + : component.name; + + // Create full component with canonical name + const fullComponent: NamespacedComponent = { + ...component, + canonicalName, + }; + + // Check for collision + const existing = this.components.get(canonicalName); + if (existing) { + const diagnostic: RegistryDiagnostic = { + type: 'collision', + message: `canonical name "${canonicalName}" collision`, + collision: { + canonicalName, + winnerPath: existing.filePath, + loserPath: component.filePath, + winnerSource: existing.source, + loserSource: component.source, + }, + }; + this.diagnostics.push(diagnostic); + return diagnostic; + } + + // Register the component + this.components.set(canonicalName, fullComponent); + return undefined; + } + + /** + * Get a component by its canonical name. + * + * @param canonicalName - The canonical name (e.g., "my-plugin:code-review" or "code-review") + * @returns The component if found, undefined otherwise + */ + getByCanonical(canonicalName: string): NamespacedComponent | undefined { + return this.components.get(canonicalName); + } + + /** + * Get all components belonging to a specific namespace. + * + * @param namespace - The namespace to filter by (e.g., "my-plugin") + * @returns Array of components in that namespace + */ + getByNamespace(namespace: string): NamespacedComponent[] { + const results: NamespacedComponent[] = []; + for (const component of this.components.values()) { + if (component.namespace === namespace) { + results.push(component); + } + } + return results; + } + + /** + * Get all registered components. + * + * @returns Array of all components + */ + getAll(): NamespacedComponent[] { + return Array.from(this.components.values()); + } + + /** + * Get all diagnostics collected during registration. + * + * Returns deep copies to prevent external mutation of internal state. + * + * @returns Array of diagnostics (collisions, etc.) + */ + getDiagnostics(): RegistryDiagnostic[] { + return this.diagnostics.map((d) => ({ + type: d.type, + message: d.message, + collision: { ...d.collision }, + })); + } + + /** + * Check if a canonical name is already registered. + * + * @param canonicalName - The canonical name to check + * @returns true if registered, false otherwise + */ + has(canonicalName: string): boolean { + return this.components.has(canonicalName); + } + + /** + * Get the count of registered components. + * + * @returns Number of components + */ + get size(): number { + return this.components.size; + } + + // ============================================================================ + // Alias Management + // ============================================================================ + + /** + * Register an alias for a canonical name. + * + * Validates: + * 1. The target canonical name must exist + * 2. The alias cannot shadow an existing canonical name + * 3. The alias cannot already exist pointing to a different target + * + * @param alias - The short alias (e.g., "py3d") + * @param canonicalName - The target canonical name (e.g., "python-tools:3d-visualizer") + * @returns Result indicating success or failure with reason + */ + registerAlias(alias: string, canonicalName: string): AliasRegistrationResult { + // Check that target canonical name exists + if (!this.components.has(canonicalName)) { + return { + success: false, + reason: 'canonical-not-found', + message: `Cannot create alias "${alias}": target canonical name "${canonicalName}" does not exist`, + }; + } + + // Check that alias doesn't shadow an existing canonical name + if (this.components.has(alias)) { + return { + success: false, + reason: 'shadows-canonical', + message: `Cannot create alias "${alias}": it shadows an existing canonical name`, + }; + } + + // Check for duplicate alias pointing to different target + const existingTarget = this.aliasMap.get(alias); + if (existingTarget !== undefined && existingTarget !== canonicalName) { + return { + success: false, + reason: 'duplicate-alias', + message: `Cannot create alias "${alias}": already exists pointing to "${existingTarget}"`, + }; + } + + // Register the alias (idempotent if same target) + this.aliasMap.set(alias, canonicalName); + + return { success: true }; + } + + /** + * Remove an alias. + * + * @param alias - The alias to remove + * @returns true if the alias existed and was removed, false otherwise + */ + removeAlias(alias: string): boolean { + return this.aliasMap.delete(alias); + } + + /** + * Resolve an alias to its canonical name. + * + * @param alias - The alias to resolve + * @returns The canonical name if alias exists, undefined otherwise + */ + resolveAlias(alias: string): string | undefined { + return this.aliasMap.get(alias); + } + + /** + * Get all registered aliases. + * + * @returns A copy of the alias map (alias -> canonicalName) + */ + getAliases(): Map { + return new Map(this.aliasMap); + } + + /** + * Check if an alias exists. + * + * @param alias - The alias to check + * @returns true if the alias exists, false otherwise + */ + hasAlias(alias: string): boolean { + return this.aliasMap.has(alias); + } +} + +// ============================================================================ +// Discovery Bridge Helper +// ============================================================================ + +/** + * Convert a discovered plugin's inventory into registerable component entries. + * + * This helper bridges S01's discovery output (DiscoveredPlugin) with the + * namespaced registry. It maps skill and agent directory names to component + * entries with the plugin's namespace. + * + * @param plugin - A discovered plugin from S01's discovery process + * @returns Array of registerable component entries (without canonicalName) + */ +export function componentsFromDiscovery( + plugin: DiscoveredPlugin +): Omit[] { + const components: Omit[] = []; + + // Use the plugin's canonical name as the namespace + const namespace = plugin.canonicalName; + + // Extract common metadata from the plugin + const commonMetadata: NamespacedComponent['metadata'] = { + pluginVersion: plugin.version, + pluginAuthor: plugin.author?.name, + pluginHomepage: plugin.homepage, + pluginCategory: plugin.category, + }; + + // Process skills + for (const skillName of plugin.inventory.skills) { + // Resolve the skill file path + // Skills are in /skills//SKILL.md or /skills/.md + let filePath: string; + if (plugin.resolvedPath) { + const skillDirPath = `${plugin.resolvedPath}/skills/${skillName}`; + // Prefer direct markdown file entries, otherwise directory with SKILL.md + filePath = skillName.endsWith('.md') + ? `${plugin.resolvedPath}/skills/${skillName}` + : `${skillDirPath}/SKILL.md`; + } else { + // External plugin - use placeholder path + filePath = `/${namespace}/skills/${skillName}/SKILL.md`; + } + + components.push({ + name: skillName.replace(/\.md$/, ''), // Strip .md if present + namespace, + type: 'skill', + filePath, + source: `plugin:${namespace}`, + description: undefined, // Would require reading the file + metadata: { + ...commonMetadata, + componentDir: skillName, + }, + }); + } + + // Process agents + for (const agentName of plugin.inventory.agents) { + // Resolve the agent file path + let filePath: string; + if (plugin.resolvedPath) { + const agentDirPath = `${plugin.resolvedPath}/agents/${agentName}`; + filePath = agentName.endsWith('.md') + ? `${plugin.resolvedPath}/agents/${agentName}` + : `${agentDirPath}/AGENT.md`; + } else { + filePath = `/${namespace}/agents/${agentName}/AGENT.md`; + } + + components.push({ + name: agentName.replace(/\.md$/, ''), // Strip .md if present + namespace, + type: 'agent', + filePath, + source: `plugin:${namespace}`, + description: undefined, // Would require reading the file + metadata: { + ...commonMetadata, + componentDir: agentName, + }, + }); + } + + return components; +} + +// ============================================================================ +// Exports +// ============================================================================ + +export default NamespacedRegistry; diff --git a/src/resources/extensions/gsd/namespaced-resolver.ts b/src/resources/extensions/gsd/namespaced-resolver.ts new file mode 100644 index 000000000..5c97323f2 --- /dev/null +++ b/src/resources/extensions/gsd/namespaced-resolver.ts @@ -0,0 +1,307 @@ +/** + * Namespaced Resolver Module + * + * Implements context-aware resolution with three-tier lookup precedence: + * 1. Canonical (fully-qualified names with `:`) + * 2. Local-first (caller namespace + bare name) + * 3. Shorthand (bare name matched across all namespaces) + * + * This is the core logic for D003 (same-plugin local-first) and R007/R008 (safe shorthand). + */ + +import type { NamespacedRegistry, NamespacedComponent, ComponentType } from './namespaced-registry.js'; + +// ============================================================================ +// Type Definitions +// ============================================================================ + +/** + * Resolution context provided by the caller. + * Used to enable local-first resolution within a namespace. + */ +export interface ResolutionContext { + /** The namespace of the calling component (e.g., "farm" from "farm:caller") */ + callerNamespace?: string; +} + +/** + * Base structure for all resolution results. + */ +interface ResolutionResultBase { + /** The original name passed to resolve() */ + requestedName: string; + + /** How the resolution was performed */ + resolution: 'canonical' | 'alias' | 'local-first' | 'shorthand' | 'ambiguous' | 'not-found'; +} + +/** + * Result when a canonical (fully-qualified) name matches exactly. + * Example: "farm:call-horse" resolves directly to the component with that canonical name. + */ +export interface CanonicalResolution extends ResolutionResultBase { + resolution: 'canonical'; + /** The matched component */ + component: NamespacedComponent; +} + +/** + * Result when an alias resolves to a canonical name. + * Example: "py3d" resolves via alias to "python-tools:3d-visualizer". + */ +export interface AliasResolution extends ResolutionResultBase { + resolution: 'alias'; + /** The matched component */ + component: NamespacedComponent; + /** The alias that was resolved */ + alias: string; + /** The canonical name the alias points to */ + canonicalName: string; +} + +/** + * Result when a bare name resolves via local-first lookup. + * Example: A caller in namespace "farm" resolving bare "call-horse" matches "farm:call-horse". + */ +export interface LocalFirstResolution extends ResolutionResultBase { + resolution: 'local-first'; + /** The matched component */ + component: NamespacedComponent; + /** The namespace used for local-first resolution */ + matchedNamespace: string; +} + +/** + * Result when a bare name matches exactly one component across all namespaces. + * Example: "feed-chickens" resolves if only "farm:feed-chickens" exists. + */ +export interface ShorthandResolution extends ResolutionResultBase { + resolution: 'shorthand'; + /** The matched component */ + component: NamespacedComponent; +} + +/** + * Result when a bare name matches multiple components across namespaces. + * Returns all candidates for diagnostic consumption without throwing. + * Example: "call-horse" matches both "farm:call-horse" and "zoo:call-horse". + */ +export interface AmbiguousResolution extends ResolutionResultBase { + resolution: 'ambiguous'; + /** All components matching the bare name */ + candidates: NamespacedComponent[]; +} + +/** + * Result when no component matches the requested name. + */ +export interface NotFoundResolution extends ResolutionResultBase { + resolution: 'not-found'; +} + +/** + * Discriminated union of all resolution results. + * The `resolution` field indicates which variant applies. + */ +export type ResolutionResult = + | CanonicalResolution + | AliasResolution + | LocalFirstResolution + | ShorthandResolution + | AmbiguousResolution + | NotFoundResolution; + +// ============================================================================ +// NamespacedResolver Class +// ============================================================================ + +/** + * Resolver for namespaced components with context-aware lookup. + * + * Implements four-tier resolution precedence: + * 1. **Canonical**: If name contains `:`, try exact match → return canonical result + * 2. **Alias**: If name is a registered alias → return alias result + * 3. **Local-first**: If `context.callerNamespace` exists, try `${callerNamespace}:${name}` → return local-first result + * 4. **Shorthand**: Scan all components for bare name match → single match returns shorthand, multiple returns ambiguous + * + * Usage: + * ```typescript + * const registry = new NamespacedRegistry(); + * // ... populate registry ... + * // ... register aliases ... + * + * const resolver = new NamespacedResolver(registry); + * + * // Canonical lookup + * const canon = resolver.resolve('farm:call-horse'); + * // canon.resolution === 'canonical' + * + * // Alias resolution + * const alias = resolver.resolve('py3d'); + * // alias.resolution === 'alias', alias.canonicalName === 'python-tools:3d-visualizer' + * + * // Local-first resolution from caller context + * const local = resolver.resolve('call-horse', { callerNamespace: 'farm' }); + * // local.resolution === 'local-first' + * + * // Unambiguous shorthand + * const short = resolver.resolve('unique-skill'); + * // short.resolution === 'shorthand' + * + * // Ambiguous shorthand + * const amb = resolver.resolve('common-skill'); + * // amb.resolution === 'ambiguous', amb.candidates has all matches + * ``` + */ +export class NamespacedResolver { + /** The registry to resolve against */ + private registry: NamespacedRegistry; + + /** + * Create a new resolver for the given registry. + * + * @param registry - The namespaced registry to resolve against + */ + constructor(registry: NamespacedRegistry) { + this.registry = registry; + } + + /** + * Resolve a component name with context-aware lookup. + * + * Implements four-tier resolution precedence: + * 1. **Canonical**: If name contains `:`, try exact match → return canonical result + * 2. **Alias**: If name is a registered alias → return alias result + * 3. **Local-first**: If `context.callerNamespace` exists, try `${callerNamespace}:${name}` → return local-first result + * 4. **Shorthand**: Scan all components for bare name match → single match returns shorthand, multiple returns ambiguous + * + * @param name - The name to resolve (canonical or bare) + * @param context - Optional resolution context with caller namespace + * @param type - Optional type filter (skill or agent) + * @returns Resolution result indicating how the match was found + */ + resolve( + name: string, + context?: ResolutionContext, + type?: ComponentType + ): ResolutionResult { + // Tier 1: Canonical lookup (name contains `:`) + if (name.includes(':')) { + const component = this.registry.getByCanonical(name); + + if (component && this.matchesType(component, type)) { + return { + requestedName: name, + resolution: 'canonical', + component, + }; + } + + // Canonical name not found + return { + requestedName: name, + resolution: 'not-found', + }; + } + + // Tier 2: Alias lookup (before local-first and shorthand) + const aliasTarget = this.registry.resolveAlias(name); + if (aliasTarget) { + const component = this.registry.getByCanonical(aliasTarget); + if (component && this.matchesType(component, type)) { + return { + requestedName: name, + resolution: 'alias', + component, + alias: name, + canonicalName: aliasTarget, + }; + } + } + + // Tier 3: Local-first resolution (if caller namespace provided) + if (context?.callerNamespace) { + const localCanonical = `${context.callerNamespace}:${name}`; + const component = this.registry.getByCanonical(localCanonical); + + if (component && this.matchesType(component, type)) { + return { + requestedName: name, + resolution: 'local-first', + component, + matchedNamespace: context.callerNamespace, + }; + } + } + + // Tier 4: Shorthand resolution (scan all components) + const candidates = this.findBareNameMatches(name, type); + + if (candidates.length === 0) { + return { + requestedName: name, + resolution: 'not-found', + }; + } + + if (candidates.length === 1) { + return { + requestedName: name, + resolution: 'shorthand', + component: candidates[0], + }; + } + + // Multiple matches - ambiguous + return { + requestedName: name, + resolution: 'ambiguous', + candidates, + }; + } + + /** + * Find all components whose local name (without namespace) matches the given bare name. + * Optionally filters by component type. + * + * @param bareName - The bare name to match + * @param type - Optional type filter + * @returns Array of matching components + */ + private findBareNameMatches( + bareName: string, + type?: ComponentType + ): NamespacedComponent[] { + const all = this.registry.getAll(); + + return all.filter((component) => { + // Match by local name (component.name) + if (component.name !== bareName) { + return false; + } + + // Apply type filter if provided + return this.matchesType(component, type); + }); + } + + /** + * Check if a component matches the optional type filter. + * + * @param component - The component to check + * @param type - Optional type filter + * @returns true if no filter or type matches + */ + private matchesType( + component: NamespacedComponent, + type?: ComponentType + ): boolean { + return type === undefined || component.type === type; + } +} + +// ============================================================================ +// Exports +// ============================================================================ + +export default NamespacedResolver; diff --git a/src/resources/extensions/gsd/plugin-importer.ts b/src/resources/extensions/gsd/plugin-importer.ts new file mode 100644 index 000000000..863a936f1 --- /dev/null +++ b/src/resources/extensions/gsd/plugin-importer.ts @@ -0,0 +1,410 @@ +/** + * PluginImporter Service + * + * Composes S01-S04 modules into a staged discover → select → validate → commit pipeline. + * Each stage is independently testable. The service owns no UI — it produces data structures + * that the command layer (T02) consumes. + * + * Pipeline stages: + * 1. discover(marketplacePaths) - Read marketplace manifests, populate registry + * 2. selectComponents(filter) - Filter to user-chosen components + * 3. validateImport(selected) - Check for collisions, return diagnostics + * 4. getImportManifest(selected) - Produce serializable config structure + * + * This service implements R012 (discover/select/import flow) and R013 (canonical name preservation). + */ + +import { + discoverMarketplace, + type MarketplaceDiscoveryResult, + type DiscoveredPlugin, +} from './marketplace-discovery.js'; +import { + NamespacedRegistry, + componentsFromDiscovery, + type NamespacedComponent, +} from './namespaced-registry.js'; +import { NamespacedResolver } from './namespaced-resolver.js'; +import { + analyzeCollisions, + type ClassifiedDiagnostic, +} from './collision-diagnostics.js'; + +// ============================================================================ +// Type Definitions +// ============================================================================ + +/** + * Result of the discovery stage. + * Contains all discovered plugins and the populated registry. + */ +export interface DiscoveryResult { + /** All discovery results from each marketplace path */ + marketplaceResults: MarketplaceDiscoveryResult[]; + + /** All discovered plugins aggregated */ + plugins: DiscoveredPlugin[]; + + /** The populated registry with all components */ + registry: NamespacedRegistry; + + /** Summary counts */ + summary: { + marketplacesProcessed: number; + marketplacesWithErrors: number; + totalPlugins: number; + pluginsWithErrors: number; + totalComponents: number; + }; +} + +/** + * Result of the validation stage. + * Contains diagnostics and a proceed flag. + */ +export interface ValidationResult { + /** All classified diagnostics (errors and warnings) */ + diagnostics: ClassifiedDiagnostic[]; + + /** True if import can proceed (no error-severity diagnostics) */ + canProceed: boolean; + + /** Summary counts */ + summary: { + total: number; + errors: number; + warnings: number; + }; +} + +/** + * A single entry in the import manifest config. + * Represents one component to be imported. + */ +export interface ImportManifestEntry { + /** Canonical name: `namespace:name` or bare `name` */ + canonicalName: string; + + /** Component type: 'skill' or 'agent' */ + type: 'skill' | 'agent'; + + /** Local component name (without namespace) */ + name: string; + + /** Plugin namespace (undefined for flat components) */ + namespace: string | undefined; + + /** Absolute path to the component's definition file */ + filePath: string; + + /** Source identifier (e.g., "plugin:my-plugin") */ + source: string; + + /** Optional description */ + description?: string; + + /** Plugin metadata for provenance */ + metadata: { + pluginVersion?: string; + pluginAuthor?: string; + pluginHomepage?: string; + pluginCategory?: string; + }; +} + +/** + * The complete import manifest structure. + * Serializable to JSON for persistence. + */ +export interface ImportManifest { + /** Schema version for future compatibility */ + schemaVersion: '1.0'; + + /** Timestamp when manifest was generated */ + generatedAt: string; + + /** All entries to be imported */ + entries: ImportManifestEntry[]; + + /** Summary counts */ + summary: { + total: number; + skills: number; + agents: number; + namespaces: string[]; + }; +} + +// ============================================================================ +// PluginImporter Class +// ============================================================================ + +/** + * Service for discovering, selecting, validating, and importing plugin components. + * + * Usage: + * ```typescript + * const importer = new PluginImporter(); + * + * // Stage 1: Discover + * const discovery = importer.discover(['../claude-plugins']); + * + * // Stage 2: Select + * const selected = importer.selectComponents(c => c.namespace === 'my-plugin'); + * + * // Stage 3: Validate + * const validation = importer.validateImport(selected); + * if (!validation.canProceed) { + * console.error('Cannot import:', validation.diagnostics); + * return; + * } + * + * // Stage 4: Get manifest for persistence + * const manifest = importer.getImportManifest(selected); + * ``` + */ +export class PluginImporter { + /** The internal registry populated during discovery */ + private registry: NamespacedRegistry | null = null; + + /** All discovered plugins from the last discovery run */ + private discoveredPlugins: DiscoveredPlugin[] = []; + + /** Last discovery result for inspection */ + private lastDiscoveryResult: DiscoveryResult | null = null; + + /** Last validation result for inspection */ + private lastValidationResult: ValidationResult | null = null; + + /** + * Stage 1: Discover plugins from marketplace paths. + * + * Calls `discoverMarketplace()` for each path and populates a `NamespacedRegistry` + * via `componentsFromDiscovery()`. + * + * @param marketplacePaths - Array of paths to marketplace directories + * @returns Discovery result with registry and summary + */ + discover(marketplacePaths: string[]): DiscoveryResult { + // Reset state for fresh discovery + this.registry = new NamespacedRegistry(); + this.discoveredPlugins = []; + this.lastValidationResult = null; + + const marketplaceResults: MarketplaceDiscoveryResult[] = []; + let marketplacesWithErrors = 0; + let pluginsWithErrors = 0; + + // Process each marketplace path + for (const marketplacePath of marketplacePaths) { + const result = discoverMarketplace(marketplacePath); + marketplaceResults.push(result); + + if (result.status === 'error') { + marketplacesWithErrors++; + } + + // Collect all plugins + for (const plugin of result.plugins) { + this.discoveredPlugins.push(plugin); + + if (plugin.status === 'error') { + pluginsWithErrors++; + } + + // Convert plugin inventory to components and register + const components = componentsFromDiscovery(plugin); + for (const component of components) { + this.registry!.register(component); + } + } + } + + // Build summary + const summary = { + marketplacesProcessed: marketplacePaths.length, + marketplacesWithErrors, + totalPlugins: this.discoveredPlugins.length, + pluginsWithErrors, + totalComponents: this.registry.size, + }; + + this.lastDiscoveryResult = { + marketplaceResults, + plugins: this.discoveredPlugins, + registry: this.registry, + summary, + }; + + return this.lastDiscoveryResult; + } + + /** + * Stage 2: Select components by filter function. + * + * Returns a filtered subset of registered components. + * Must be called after discover(). + * + * @param componentFilter - Filter function returning true for selected components + * @returns Array of selected components + */ + selectComponents( + componentFilter: (component: NamespacedComponent) => boolean + ): NamespacedComponent[] { + if (!this.registry) { + throw new Error('Must call discover() before selectComponents()'); + } + + return this.registry.getAll().filter(componentFilter); + } + + /** + * Stage 3: Validate selected components for import. + * + * Builds a `NamespacedResolver`, runs `analyzeCollisions()`, and returns + * `{ diagnostics, canProceed }` where `canProceed` is false if any + * error-severity diagnostics exist. + * + * @param selected - Array of components to validate + * @returns Validation result with diagnostics and proceed flag + */ + validateImport(selected: NamespacedComponent[]): ValidationResult { + if (!this.registry) { + throw new Error('Must call discover() before validateImport()'); + } + + // Create a temporary resolver for the selected components + const tempRegistry = new NamespacedRegistry(); + + // Register only selected components into temp registry + for (const component of selected) { + tempRegistry.register({ + name: component.name, + namespace: component.namespace, + type: component.type, + filePath: component.filePath, + source: component.source, + description: component.description, + metadata: component.metadata, + }); + } + + // Create resolver and analyze collisions + const resolver = new NamespacedResolver(tempRegistry); + const diagnostics = analyzeCollisions(tempRegistry, resolver); + + // Count by severity + const errors = diagnostics.filter((d) => d.severity === 'error').length; + const warnings = diagnostics.filter((d) => d.severity === 'warning').length; + + const summary = { + total: diagnostics.length, + errors, + warnings, + }; + + // canProceed is false if any error-severity diagnostics exist + const canProceed = errors === 0; + + this.lastValidationResult = { + diagnostics, + canProceed, + summary, + }; + + return this.lastValidationResult; + } + + /** + * Stage 4: Generate import manifest for selected components. + * + * Produces a serializable config structure with canonical names preserved. + * The manifest can be persisted to config files. + * + * @param selected - Array of components to include in manifest + * @returns Import manifest with all entries and metadata + */ + getImportManifest(selected: NamespacedComponent[]): ImportManifest { + const entries: ImportManifestEntry[] = selected.map((component) => ({ + canonicalName: component.canonicalName, + type: component.type, + name: component.name, + namespace: component.namespace, + filePath: component.filePath, + source: component.source, + description: component.description, + metadata: { + pluginVersion: component.metadata.pluginVersion, + pluginAuthor: component.metadata.pluginAuthor, + pluginHomepage: component.metadata.pluginHomepage, + pluginCategory: component.metadata.pluginCategory, + }, + })); + + // Count by type + const skills = entries.filter((e) => e.type === 'skill').length; + const agents = entries.filter((e) => e.type === 'agent').length; + + // Collect unique namespaces + const namespaces = Array.from( + new Set(entries.map((e) => e.namespace).filter((n): n is string => n !== undefined)) + ).sort(); + + return { + schemaVersion: '1.0', + generatedAt: new Date().toISOString(), + entries, + summary: { + total: entries.length, + skills, + agents, + namespaces, + }, + }; + } + + /** + * Get the internal registry for inspection. + * Useful for debugging or advanced filtering. + * + * @returns The registry or null if discover() hasn't been called + */ + getRegistry(): NamespacedRegistry | null { + return this.registry; + } + + /** + * Get all discovered plugins. + * + * @returns Array of discovered plugins + */ + getDiscoveredPlugins(): DiscoveredPlugin[] { + return this.discoveredPlugins; + } + + /** + * Get the last validation result. + * Useful for re-inspecting validation without re-running. + * + * @returns Last validation result or null + */ + getLastValidation(): ValidationResult | null { + return this.lastValidationResult; + } + + /** + * Get the last discovery result. + * Useful for re-inspecting discovery without re-running. + * + * @returns Last discovery result or null + */ + getLastDiscovery(): DiscoveryResult | null { + return this.lastDiscoveryResult; + } +} + +// ============================================================================ +// Exports +// ============================================================================ + +export default PluginImporter; diff --git a/src/resources/extensions/gsd/tests/claude-import-tui.test.ts b/src/resources/extensions/gsd/tests/claude-import-tui.test.ts new file mode 100644 index 000000000..12d64f99a --- /dev/null +++ b/src/resources/extensions/gsd/tests/claude-import-tui.test.ts @@ -0,0 +1,351 @@ +/** + * TUI Command Flow Tests for import-claude + * + * Tests R015: validates the TUI command flow for /gsd prefs import-claude. + * These tests currently use mock UI, and marketplace availability is still + * derived from real/local marketplace roots. Follow-up work should route these + * through portable marketplace fixtures that mirror Claude Code's + * `/plugin marketplace add ...` source model. + */ + + +import { describe, it, before, after, mock } from 'node:test'; +import assert from 'node:assert'; +import { existsSync, mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import type { ExtensionCommandContext } from '@gsd/pi-coding-agent'; +import { runClaudeImportFlow, getClaudeSearchRoots, discoverClaudeSkills, discoverClaudePlugins } from '../claude-import.js'; +import { getMarketplaceFixtures } from './marketplace-test-fixtures.js'; + +// ============================================================================ +// Test Configuration +// ============================================================================ + +const fixtureSetup = getMarketplaceFixtures(import.meta.dirname); +const fixtures = fixtureSetup.fixtures; +const CLAUDE_SKILLS_PATH = fixtures?.claudeSkillsPath; +const CLAUDE_PLUGINS_OFFICIAL_PATH = fixtures?.claudePluginsOfficialPath; + +function marketplacesAvailable(): boolean { + return Boolean(fixtures); +} + +// ============================================================================ +// Mock UI Context +// ============================================================================ + +interface MockUISelectCall { + prompt: string; + options: string[]; +} + +function createMockContext(selections: string[]): { + ctx: ExtensionCommandContext; + selectCalls: MockUISelectCall[]; +} { + const selectCalls: MockUISelectCall[] = []; + + const selectMock = mock.fn(async (prompt: string, options: string[]) => { + selectCalls.push({ prompt, options }); + const next = selections.shift(); + if (next && options.includes(next)) { + return next; + } + // Default: cancel or first option + return options.find(o => o.toLowerCase().includes('cancel')) || options[0]; + }); + + const notifyMock = mock.fn(); + + // Create a mock that satisfies ExtensionCommandContext + // Using type assertion since we only use select, notify, waitForIdle, reload in the tests + const ctx = { + ui: { + select: selectMock, + notify: notifyMock, + confirm: async () => false, + input: async () => undefined, + onTerminalInput: () => () => {}, + setStatus: () => {}, + setWorkingMessage: () => {}, + setWidget: () => {}, + setFooter: () => {}, + setHeader: () => {}, + setTitle: () => {}, + custom: async () => { throw new Error('Not implemented'); }, + pasteToEditor: () => {}, + setEditorText: () => {}, + getEditorText: () => '', + editor: async () => undefined, + setEditorComponent: () => {}, + theme: {}, + getAllThemes: () => [], + getTheme: () => undefined, + setTheme: () => ({ success: false }), + getToolsExpanded: () => true, + setToolsExpanded: () => {}, + }, + hasUI: true, + cwd: process.cwd(), + sessionManager: {} as unknown, + modelRegistry: {} as unknown, + model: undefined, + isIdle: () => true, + abort: () => {}, + hasPendingMessages: () => false, + shutdown: () => {}, + getContextUsage: () => undefined, + compact: () => {}, + getSystemPrompt: () => '', + waitForIdle: mock.fn(async () => {}), + newSession: async () => ({ cancelled: false }), + fork: async () => ({ cancelled: false }), + navigateTree: async () => ({ cancelled: false }), + switchSession: async () => ({ cancelled: false }), + reload: mock.fn(async () => {}), + } as unknown as ExtensionCommandContext; + + return { ctx, selectCalls }; +} + +// ============================================================================ +// Tests +// ============================================================================ + +const skipReason = !marketplacesAvailable() + ? fixtureSetup.skipReason ?? 'Marketplace repos not found for TUI testing' + : undefined; + +describe( + 'TUI Command Flow Tests', + { skip: skipReason }, + () => { + let tempDir: string; + let prefsPath: string; + let prefs: Record; + + before(() => { + tempDir = mkdtempSync(join(tmpdir(), 'gsd-tui-test-')); + prefsPath = join(tempDir, 'preferences.md'); + prefs = { version: 1 }; + }); + + after(() => { + fixtures?.cleanup(); + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + describe('getClaudeSearchRoots()', () => { + it('should return existing skill and plugin roots', () => { + const cwd = process.cwd(); + const { skillRoots, pluginRoots } = getClaudeSearchRoots(cwd); + + // At least one root should exist in our test environment + assert.ok( + skillRoots.length > 0 || pluginRoots.length > 0, + 'Should find at least one search root' + ); + + // All returned roots should exist + for (const root of [...skillRoots, ...pluginRoots]) { + assert.ok(existsSync(root), `Root should exist: ${root}`); + } + }); + }); + + describe('discoverClaudeSkills()', () => { + it('should discover skills without crashing', () => { + const cwd = process.cwd(); + const skills = discoverClaudeSkills(cwd); + + assert.ok(Array.isArray(skills), 'Should return an array'); + + // Log for observability + console.log(`\nDiscovered ${skills.length} skills`); + + if (skills.length > 0) { + console.log('Sample skills:'); + skills.slice(0, 3).forEach(s => { + console.log(` - ${s.name} (${s.sourceLabel})`); + }); + + // Verify structure + const sample = skills[0]!; + assert.ok(sample.name, 'Skill should have name'); + assert.ok(sample.path, 'Skill should have path'); + assert.ok(sample.root, 'Skill should have root'); + assert.strictEqual(sample.type, 'skill'); + } + }); + }); + + describe('discoverClaudePlugins()', () => { + it('should discover plugins without crashing', () => { + const cwd = process.cwd(); + const plugins = discoverClaudePlugins(cwd); + + assert.ok(Array.isArray(plugins), 'Should return an array'); + + // Log for observability + console.log(`\nDiscovered ${plugins.length} plugins`); + + if (plugins.length > 0) { + console.log('Sample plugins:'); + plugins.slice(0, 3).forEach(p => { + console.log(` - ${p.name} (${p.sourceLabel})`); + }); + + // Verify structure + const sample = plugins[0]!; + assert.ok(sample.name, 'Plugin should have name'); + assert.ok(sample.path, 'Plugin should have path'); + assert.strictEqual(sample.type, 'plugin'); + } + }); + }); + + describe('runClaudeImportFlow()', () => { + it('should not crash when user cancels at first prompt', async () => { + const { ctx, selectCalls } = createMockContext(['Cancel']); + + const readPrefs = () => ({ ...prefs }); + const writePrefs = async (p: Record) => { + Object.assign(prefs, p); + }; + + // Should complete without throwing + await runClaudeImportFlow(ctx, 'global', readPrefs, writePrefs); + + // Should have asked about asset type + assert.ok(selectCalls.length >= 1, 'Should have at least one select call'); + assert.ok( + selectCalls[0]!.prompt.includes('Import Claude assets'), + 'First prompt should be about asset selection' + ); + }); + + it('should not crash when selecting skills only with cancel at next step', async () => { + const { ctx, selectCalls } = createMockContext([ + 'Skills only', // Select skills only + 'Cancel', // Cancel at skill selection + ]); + + const readPrefs = () => ({ ...prefs }); + const writePrefs = async (p: Record) => { + Object.assign(prefs, p); + }; + + // Should complete without throwing + await runClaudeImportFlow(ctx, 'global', readPrefs, writePrefs); + + // Log interaction flow + console.log('\nSelect calls made:'); + selectCalls.forEach((call, i) => { + console.log(` ${i + 1}. "${call.prompt}"`); + }); + }); + + it('should handle marketplace flow when user selects plugins', async () => { + const { ctx, selectCalls } = createMockContext([ + 'Plugins only', // Select plugins only + 'Yes - discover plugins and select components', // Marketplace prompt + 'Cancel', // Cancel at component selection + ]); + + const readPrefs = () => ({ ...prefs }); + const writePrefs = async (p: Record) => { + Object.assign(prefs, p); + }; + + // Should complete without throwing + await runClaudeImportFlow(ctx, 'global', readPrefs, writePrefs); + + // Log interaction flow + console.log('\nMarketplace flow select calls:'); + selectCalls.forEach((call, i) => { + console.log(` ${i + 1}. "${call.prompt}"`); + }); + }); + + it('should complete import-all flow with mock UI', async () => { + // This tests the happy path where user selects "Import all" + const { ctx, selectCalls } = createMockContext([ + 'Skills + plugins', // Select both + 'Cancel', // Cancel at skill selection (no skills to import) + 'Yes - discover plugins and select components', // Marketplace prompt + 'Import all components', // Import all + 'Yes, continue', // Continue with warnings (if any) + ]); + + const readPrefs = () => ({ ...prefs }); + const writePrefs = async (p: Record) => { + Object.assign(prefs, p); + }; + + // Should complete without throwing + await runClaudeImportFlow(ctx, 'global', readPrefs, writePrefs); + + // Log interaction flow + console.log('\nImport-all flow select calls:'); + selectCalls.forEach((call, i) => { + console.log(` ${i + 1}. "${call.prompt}"`); + }); + + // Verify notification was called + const notifyCalls = (ctx.ui.notify as unknown as ReturnType).mock.calls; + assert.ok(notifyCalls.length > 0, 'Should have shown notification'); + + console.log('\nNotifications shown:'); + notifyCalls.forEach((call, i) => { + const msg = call.arguments[0]; + const level = call.arguments[1]; + console.log(` ${i + 1}. [${level}]: ${String(msg).split('\n')[0]}`); + }); + }); + + it('should not persist marketplace agent directories into package sources', async () => { + const isolatedAgentDir = join(tempDir, '.gsd', 'agent'); + const settingsPath = join(isolatedAgentDir, 'settings.json'); + rmSync(isolatedAgentDir, { recursive: true, force: true }); + process.env.GSD_CODING_AGENT_DIR = isolatedAgentDir; + + try { + mkdirSync(isolatedAgentDir, { recursive: true }); + const tempSettings: Record = { packages: [] }; + writeFileSync(settingsPath, JSON.stringify(tempSettings, null, 2)); + + const { ctx } = createMockContext([ + 'Plugins only', + 'Yes - discover plugins and select components', + 'Import all components', + 'Yes, continue', + ]); + + const readPrefs = () => ({ ...prefs }); + const writePrefs = async (p: Record) => { + Object.assign(prefs, p); + }; + + await runClaudeImportFlow(ctx, 'global', readPrefs, writePrefs); + + const settings = JSON.parse(readFileSync(settingsPath, 'utf8')) as { packages?: unknown[] }; + const packageEntries = Array.isArray(settings.packages) ? settings.packages : []; + const hasAgentsDirPackage = packageEntries.some((entry) => { + const source = typeof entry === 'string' + ? entry + : (entry && typeof entry === 'object' ? (entry as { source?: unknown }).source : undefined); + return typeof source === 'string' && source.endsWith('/agents'); + }); + + assert.strictEqual(hasAgentsDirPackage, false, 'Marketplace agent directories should not be persisted as package sources'); + } finally { + delete process.env.GSD_CODING_AGENT_DIR; + rmSync(isolatedAgentDir, { recursive: true, force: true }); + } + }); + }); + } +); diff --git a/src/resources/extensions/gsd/tests/collision-diagnostics.test.ts b/src/resources/extensions/gsd/tests/collision-diagnostics.test.ts new file mode 100644 index 000000000..e2f3ef6f6 --- /dev/null +++ b/src/resources/extensions/gsd/tests/collision-diagnostics.test.ts @@ -0,0 +1,705 @@ +/** + * Collision Diagnostics Contract Tests + * + * Tests that prove: + * - R010: Collision reporting distinguishes canonical-conflict from shorthand-overlap + * - R011: Doctor provides actionable advice with canonical name suggestions + */ + +import { describe, it, beforeEach } from 'node:test'; +import assert from 'node:assert'; +import { NamespacedRegistry } from '../namespaced-registry.js'; +import { NamespacedResolver } from '../namespaced-resolver.js'; +import { + analyzeCollisions, + doctorReport, + type ClassifiedDiagnostic, + type DoctorReport, +} from '../collision-diagnostics.js'; + +describe('collision-diagnostics', () => { + let registry: NamespacedRegistry; + let resolver: NamespacedResolver; + + beforeEach(() => { + registry = new NamespacedRegistry(); + resolver = new NamespacedResolver(registry); + }); + + describe('analyzeCollisions', () => { + describe('canonical-conflict detection', () => { + it('should detect canonical conflict when same canonical name registered twice', () => { + // First registration wins + registry.register({ + name: 'code-review', + namespace: 'my-plugin', + type: 'skill', + filePath: '/plugins/my-plugin/skills/code-review/SKILL.md', + source: 'plugin:my-plugin', + description: 'Reviews code', + metadata: {}, + }); + + // Second registration with same canonical name loses + registry.register({ + name: 'code-review', + namespace: 'my-plugin', + type: 'skill', + filePath: '/plugins/other/skills/code-review/SKILL.md', + source: 'plugin:other', + description: 'Another code review', + metadata: {}, + }); + + const diagnostics = analyzeCollisions(registry, resolver); + + assert.strictEqual(diagnostics.length, 1); + assert.strictEqual(diagnostics[0].class, 'canonical-conflict'); + assert.strictEqual(diagnostics[0].severity, 'error'); + assert.strictEqual(diagnostics[0].involvedCanonicalNames[0], 'my-plugin:code-review'); + assert.ok(diagnostics[0].filePaths.includes('/plugins/my-plugin/skills/code-review/SKILL.md')); + assert.ok(diagnostics[0].filePaths.includes('/plugins/other/skills/code-review/SKILL.md')); + }); + + it('should include remediation advice for canonical conflict', () => { + registry.register({ + name: 'test-skill', + namespace: 'plugin-a', + type: 'skill', + filePath: '/a/test-skill/SKILL.md', + source: 'plugin:plugin-a', + description: 'Test', + metadata: {}, + }); + registry.register({ + name: 'test-skill', + namespace: 'plugin-a', + type: 'skill', + filePath: '/b/test-skill/SKILL.md', + source: 'plugin:plugin-b', + description: 'Test duplicate', + metadata: {}, + }); + + const diagnostics = analyzeCollisions(registry, resolver); + + assert.ok(diagnostics[0].remediation.includes('Rename one of the conflicting components')); + }); + }); + + describe('shorthand-overlap detection', () => { + it('should detect shorthand overlap when bare name matches multiple namespaces', () => { + // Same bare name in different namespaces + registry.register({ + name: 'common-skill', + namespace: 'plugin-a', + type: 'skill', + filePath: '/a/common-skill/SKILL.md', + source: 'plugin:plugin-a', + description: 'A common skill', + metadata: {}, + }); + registry.register({ + name: 'common-skill', + namespace: 'plugin-b', + type: 'skill', + filePath: '/b/common-skill/SKILL.md', + source: 'plugin:plugin-b', + description: 'B common skill', + metadata: {}, + }); + + const diagnostics = analyzeCollisions(registry, resolver); + + assert.strictEqual(diagnostics.length, 1); + assert.strictEqual(diagnostics[0].class, 'shorthand-overlap'); + assert.strictEqual(diagnostics[0].severity, 'warning'); + assert.strictEqual(diagnostics[0].ambiguousBareName, 'common-skill'); + assert.ok(diagnostics[0].involvedCanonicalNames.includes('plugin-a:common-skill')); + assert.ok(diagnostics[0].involvedCanonicalNames.includes('plugin-b:common-skill')); + }); + + it('should NOT warn when only one component has a given bare name', () => { + registry.register({ + name: 'unique-skill', + namespace: 'plugin-a', + type: 'skill', + filePath: '/a/unique-skill/SKILL.md', + source: 'plugin:plugin-a', + description: 'Unique', + metadata: {}, + }); + registry.register({ + name: 'other-skill', + namespace: 'plugin-b', + type: 'skill', + filePath: '/b/other-skill/SKILL.md', + source: 'plugin:plugin-b', + description: 'Other', + metadata: {}, + }); + + const diagnostics = analyzeCollisions(registry, resolver); + + assert.strictEqual(diagnostics.length, 0); + }); + + it('should include canonical name suggestions in remediation for shorthand overlap', () => { + registry.register({ + name: 'ambiguous', + namespace: 'alpha', + type: 'skill', + filePath: '/alpha/ambiguous/SKILL.md', + source: 'plugin:alpha', + description: 'Alpha ambiguous', + metadata: {}, + }); + registry.register({ + name: 'ambiguous', + namespace: 'beta', + type: 'skill', + filePath: '/beta/ambiguous/SKILL.md', + source: 'plugin:beta', + description: 'Beta ambiguous', + metadata: {}, + }); + + const diagnostics = analyzeCollisions(registry, resolver); + + assert.ok(diagnostics[0].remediation.includes('`alpha:ambiguous`')); + assert.ok(diagnostics[0].remediation.includes('`beta:ambiguous`')); + assert.ok(diagnostics[0].remediation.includes('Use a canonical name')); + }); + }); + + describe('clean registry', () => { + it('should return no diagnostics for empty registry', () => { + const diagnostics = analyzeCollisions(registry, resolver); + assert.strictEqual(diagnostics.length, 0); + }); + + it('should return no diagnostics for registry with unique bare names', () => { + registry.register({ + name: 'skill-a', + namespace: 'plugin-x', + type: 'skill', + filePath: '/x/skill-a/SKILL.md', + source: 'plugin:plugin-x', + description: 'Skill A', + metadata: {}, + }); + registry.register({ + name: 'skill-b', + namespace: 'plugin-y', + type: 'skill', + filePath: '/y/skill-b/SKILL.md', + source: 'plugin:plugin-y', + description: 'Skill B', + metadata: {}, + }); + + const diagnostics = analyzeCollisions(registry, resolver); + assert.strictEqual(diagnostics.length, 0); + }); + }); + + describe('mixed scenarios', () => { + it('should report both canonical conflict and shorthand overlap in mixed scenario', () => { + // Canonical conflict: same canonical name twice + registry.register({ + name: 'duplicate', + namespace: 'shared', + type: 'skill', + filePath: '/first/duplicate/SKILL.md', + source: 'plugin:first', + description: 'First duplicate', + metadata: {}, + }); + registry.register({ + name: 'duplicate', + namespace: 'shared', + type: 'skill', + filePath: '/second/duplicate/SKILL.md', + source: 'plugin:second', + description: 'Second duplicate', + metadata: {}, + }); + + // Shorthand overlap: same bare name in different namespaces + registry.register({ + name: 'overlap', + namespace: 'ns-a', + type: 'skill', + filePath: '/a/overlap/SKILL.md', + source: 'plugin:ns-a', + description: 'A overlap', + metadata: {}, + }); + registry.register({ + name: 'overlap', + namespace: 'ns-b', + type: 'skill', + filePath: '/b/overlap/SKILL.md', + source: 'plugin:ns-b', + description: 'B overlap', + metadata: {}, + }); + + const diagnostics = analyzeCollisions(registry, resolver); + + assert.strictEqual(diagnostics.length, 2); + + const canonicalConflict = diagnostics.find(d => d.class === 'canonical-conflict'); + const shorthandOverlap = diagnostics.find(d => d.class === 'shorthand-overlap'); + + assert.ok(canonicalConflict, 'Should have canonical conflict'); + assert.ok(shorthandOverlap, 'Should have shorthand overlap'); + + assert.strictEqual(canonicalConflict!.severity, 'error'); + assert.strictEqual(shorthandOverlap!.severity, 'warning'); + }); + }); + + describe('alias-conflict detection', () => { + it('should detect alias that shadows an existing canonical name', () => { + // Register component that will be aliased to + registry.register({ + name: 'utility', + namespace: 'core', + type: 'skill', + filePath: '/core/utility/SKILL.md', + source: 'plugin:core', + description: 'Utility skill', + metadata: {}, + }); + + // Register alias for a non-existent canonical name (will succeed) + registry.registerAlias('tools:helper', 'core:utility'); + + // Now register the component that creates the conflict + registry.register({ + name: 'helper', + namespace: 'tools', + type: 'skill', + filePath: '/tools/helper/SKILL.md', + source: 'plugin:tools', + description: 'Helper skill', + metadata: {}, + }); + + const diagnostics = analyzeCollisions(registry, resolver); + + const aliasConflict = diagnostics.find(d => d.class === 'alias-conflict'); + assert.ok(aliasConflict, 'Should detect alias-conflict'); + assert.strictEqual(aliasConflict!.alias, 'tools:helper'); + assert.strictEqual(aliasConflict!.aliasTarget, 'core:utility'); + assert.strictEqual(aliasConflict!.aliasConflictType, 'shadows-canonical'); + assert.strictEqual(aliasConflict!.severity, 'warning'); + }); + + it('should detect alias that shadows a bare component name', () => { + // Register component with bare name "helper" + registry.register({ + name: 'helper', + namespace: 'tools', + type: 'skill', + filePath: '/tools/helper/SKILL.md', + source: 'plugin:tools', + description: 'Helper skill', + metadata: {}, + }); + + // Register another component to alias to + registry.register({ + name: 'utility', + namespace: 'core', + type: 'skill', + filePath: '/core/utility/SKILL.md', + source: 'plugin:core', + description: 'Utility skill', + metadata: {}, + }); + + // Create alias "helper" that shadows the bare name + registry.registerAlias('helper', 'core:utility'); + + const diagnostics = analyzeCollisions(registry, resolver); + + const aliasConflict = diagnostics.find(d => d.class === 'alias-conflict'); + assert.ok(aliasConflict, 'Should detect alias-conflict'); + assert.strictEqual(aliasConflict!.alias, 'helper'); + assert.strictEqual(aliasConflict!.aliasTarget, 'core:utility'); + assert.strictEqual(aliasConflict!.aliasConflictType, 'shadows-bare-name'); + assert.strictEqual(aliasConflict!.severity, 'warning'); + }); + + it('should NOT warn when alias does not conflict', () => { + registry.register({ + name: 'unique-skill', + namespace: 'plugin-a', + type: 'skill', + filePath: '/a/unique-skill/SKILL.md', + source: 'plugin:plugin-a', + description: 'Unique skill', + metadata: {}, + }); + + registry.register({ + name: 'other-skill', + namespace: 'plugin-b', + type: 'skill', + filePath: '/b/other-skill/SKILL.md', + source: 'plugin:plugin-b', + description: 'Other skill', + metadata: {}, + }); + + // Create a non-conflicting alias + registry.registerAlias('short', 'plugin-a:unique-skill'); + + const diagnostics = analyzeCollisions(registry, resolver); + + const aliasConflict = diagnostics.find(d => d.class === 'alias-conflict'); + assert.strictEqual(aliasConflict, undefined, 'Should not have alias-conflict for clean alias'); + }); + + it('should include remediation advice for alias shadowing canonical', () => { + // Register the target component first + registry.register({ + name: 'target', + namespace: 'my-plugin', + type: 'skill', + filePath: '/my-plugin/target/SKILL.md', + source: 'plugin:my-plugin', + description: 'Target skill', + metadata: {}, + }); + + // Register alias for a non-existent canonical name (will succeed because it doesn't exist yet) + registry.registerAlias('other:conflicting', 'my-plugin:target'); + + // Now register the component that the alias would shadow + registry.register({ + name: 'conflicting', + namespace: 'other', + type: 'skill', + filePath: '/other/conflicting/SKILL.md', + source: 'plugin:other', + description: 'Conflicting skill', + metadata: {}, + }); + + const diagnostics = analyzeCollisions(registry, resolver); + + const aliasConflict = diagnostics.find(d => d.class === 'alias-conflict'); + assert.ok(aliasConflict, 'Should have alias conflict'); + assert.ok(aliasConflict!.remediation.includes('shadows an existing canonical name')); + assert.ok(aliasConflict!.remediation.includes('rename or remove the alias')); + }); + + it('should distinguish alias conflicts from shorthand overlap', () => { + // Shorthand overlap scenario + registry.register({ + name: 'common', + namespace: 'plugin-a', + type: 'skill', + filePath: '/a/common/SKILL.md', + source: 'plugin:plugin-a', + description: 'Common A', + metadata: {}, + }); + registry.register({ + name: 'common', + namespace: 'plugin-b', + type: 'skill', + filePath: '/b/common/SKILL.md', + source: 'plugin:plugin-b', + description: 'Common B', + metadata: {}, + }); + + // Alias conflict scenario (separate from shorthand) + registry.register({ + name: 'unique', + namespace: 'plugin-c', + type: 'skill', + filePath: '/c/unique/SKILL.md', + source: 'plugin:plugin-c', + description: 'Unique C', + metadata: {}, + }); + registry.registerAlias('unique', 'plugin-c:unique'); + + const diagnostics = analyzeCollisions(registry, resolver); + + const shorthandOverlap = diagnostics.find(d => d.class === 'shorthand-overlap'); + const aliasConflict = diagnostics.find(d => d.class === 'alias-conflict'); + + assert.ok(shorthandOverlap, 'Should have shorthand overlap'); + assert.ok(aliasConflict, 'Should have alias conflict'); + assert.strictEqual(shorthandOverlap!.ambiguousBareName, 'common'); + assert.strictEqual(aliasConflict!.alias, 'unique'); + }); + }); + }); + + describe('doctorReport', () => { + it('should format report with correct summary counts', () => { + // Create scenario with 1 error and 2 warnings + registry.register({ + name: 'conflict', + namespace: 'ns', + type: 'skill', + filePath: '/a/conflict/SKILL.md', + source: 'plugin:a', + description: 'A', + metadata: {}, + }); + registry.register({ + name: 'conflict', + namespace: 'ns', + type: 'skill', + filePath: '/b/conflict/SKILL.md', + source: 'plugin:b', + description: 'B', + metadata: {}, + }); + registry.register({ + name: 'overlap', + namespace: 'x', + type: 'skill', + filePath: '/x/overlap/SKILL.md', + source: 'plugin:x', + description: 'X', + metadata: {}, + }); + registry.register({ + name: 'overlap', + namespace: 'y', + type: 'skill', + filePath: '/y/overlap/SKILL.md', + source: 'plugin:y', + description: 'Y', + metadata: {}, + }); + + const diagnostics = analyzeCollisions(registry, resolver); + const report = doctorReport(diagnostics); + + assert.strictEqual(report.summary.total, 2); + assert.strictEqual(report.summary.canonicalConflicts, 1); + assert.strictEqual(report.summary.shorthandOverlaps, 1); + assert.strictEqual(report.entries.length, 2); + }); + + it('should include error icon for canonical conflicts', () => { + registry.register({ + name: 'dup', + namespace: 'ns', + type: 'skill', + filePath: '/a/dup/SKILL.md', + source: 'plugin:a', + description: 'A', + metadata: {}, + }); + registry.register({ + name: 'dup', + namespace: 'ns', + type: 'skill', + filePath: '/b/dup/SKILL.md', + source: 'plugin:b', + description: 'B', + metadata: {}, + }); + + const diagnostics = analyzeCollisions(registry, resolver); + const report = doctorReport(diagnostics); + + assert.ok(report.entries[0].includes('❌')); + }); + + it('should include warning icon for shorthand overlaps', () => { + registry.register({ + name: 'overlap', + namespace: 'a', + type: 'skill', + filePath: '/a/overlap/SKILL.md', + source: 'plugin:a', + description: 'A', + metadata: {}, + }); + registry.register({ + name: 'overlap', + namespace: 'b', + type: 'skill', + filePath: '/b/overlap/SKILL.md', + source: 'plugin:b', + description: 'B', + metadata: {}, + }); + + const diagnostics = analyzeCollisions(registry, resolver); + const report = doctorReport(diagnostics); + + assert.ok(report.entries[0].includes('⚠️')); + }); + + it('should include file paths in formatted output', () => { + registry.register({ + name: 'overlap', + namespace: 'a', + type: 'skill', + filePath: '/path/a/overlap/SKILL.md', + source: 'plugin:a', + description: 'A', + metadata: {}, + }); + registry.register({ + name: 'overlap', + namespace: 'b', + type: 'skill', + filePath: '/path/b/overlap/SKILL.md', + source: 'plugin:b', + description: 'B', + metadata: {}, + }); + + const diagnostics = analyzeCollisions(registry, resolver); + const report = doctorReport(diagnostics); + + assert.ok(report.entries[0].includes('/path/a/overlap/SKILL.md')); + assert.ok(report.entries[0].includes('/path/b/overlap/SKILL.md')); + }); + + it('should include canonical name suggestions for ambiguous shorthand', () => { + registry.register({ + name: 'common', + namespace: 'plugin-1', + type: 'skill', + filePath: '/1/common/SKILL.md', + source: 'plugin:plugin-1', + description: 'Common 1', + metadata: {}, + }); + registry.register({ + name: 'common', + namespace: 'plugin-2', + type: 'skill', + filePath: '/2/common/SKILL.md', + source: 'plugin:plugin-2', + description: 'Common 2', + metadata: {}, + }); + + const diagnostics = analyzeCollisions(registry, resolver); + const report = doctorReport(diagnostics); + + assert.ok(report.entries[0].includes('`plugin-1:common`')); + assert.ok(report.entries[0].includes('`plugin-2:common`')); + }); + + it('should return empty arrays for clean registry', () => { + const diagnostics = analyzeCollisions(registry, resolver); + const report = doctorReport(diagnostics); + + assert.strictEqual(report.summary.total, 0); + assert.strictEqual(report.summary.canonicalConflicts, 0); + assert.strictEqual(report.summary.shorthandOverlaps, 0); + assert.strictEqual(report.summary.aliasConflicts, 0); + assert.deepStrictEqual(report.entries, []); + }); + + it('should include alias conflicts in summary counts', () => { + registry.register({ + name: 'target', + namespace: 'my-plugin', + type: 'skill', + filePath: '/my-plugin/target/SKILL.md', + source: 'plugin:my-plugin', + description: 'Target skill', + metadata: {}, + }); + + registry.register({ + name: 'helper', + namespace: 'other', + type: 'skill', + filePath: '/other/helper/SKILL.md', + source: 'plugin:other', + description: 'Helper skill', + metadata: {}, + }); + + // Create alias that shadows bare name + registry.registerAlias('helper', 'my-plugin:target'); + + const diagnostics = analyzeCollisions(registry, resolver); + const report = doctorReport(diagnostics); + + assert.strictEqual(report.summary.aliasConflicts, 1); + assert.strictEqual(report.summary.total, 1); + }); + + it('should include warning icon for alias conflicts', () => { + registry.register({ + name: 'target', + namespace: 'my-plugin', + type: 'skill', + filePath: '/my-plugin/target/SKILL.md', + source: 'plugin:my-plugin', + description: 'Target skill', + metadata: {}, + }); + + registry.register({ + name: 'shadowed', + namespace: 'other', + type: 'skill', + filePath: '/other/shadowed/SKILL.md', + source: 'plugin:other', + description: 'Shadowed skill', + metadata: {}, + }); + + // Create alias that shadows bare name + registry.registerAlias('shadowed', 'my-plugin:target'); + + const diagnostics = analyzeCollisions(registry, resolver); + const report = doctorReport(diagnostics); + + assert.ok(report.entries[0].includes('⚠️')); + assert.ok(report.entries[0].includes('ALIAS-CONFLICT')); + }); + + it('should include alias details in formatted output', () => { + registry.register({ + name: 'target', + namespace: 'my-plugin', + type: 'skill', + filePath: '/my-plugin/target/SKILL.md', + source: 'plugin:my-plugin', + description: 'Target skill', + metadata: {}, + }); + + registry.register({ + name: 'shadowed', + namespace: 'other', + type: 'skill', + filePath: '/other/shadowed/SKILL.md', + source: 'plugin:other', + description: 'Shadowed skill', + metadata: {}, + }); + + // Create alias that shadows bare name + registry.registerAlias('shadowed', 'my-plugin:target'); + + const diagnostics = analyzeCollisions(registry, resolver); + const report = doctorReport(diagnostics); + + assert.ok(report.entries[0].includes('shadowed')); + assert.ok(report.entries[0].includes('my-plugin:target')); + }); + }); +}); diff --git a/src/resources/extensions/gsd/tests/marketplace-discovery.test.ts b/src/resources/extensions/gsd/tests/marketplace-discovery.test.ts new file mode 100644 index 000000000..1be693d0e --- /dev/null +++ b/src/resources/extensions/gsd/tests/marketplace-discovery.test.ts @@ -0,0 +1,202 @@ +/** + * Marketplace Discovery Tests + * + * Tests for the marketplace discovery module that reads marketplace.json + * from real Claude marketplace repos, resolves plugin roots, parses plugin.json + * manifests, and inventories components. + */ + +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import * as path from 'node:path'; +import * as fs from 'node:fs'; +import { + parseMarketplaceJson, + inspectPlugin, + discoverMarketplace, + resolvePluginRoot +} from '../marketplace-discovery.js'; +import { getMarketplaceFixtures } from './marketplace-test-fixtures.js'; + +const fixtureSetup = getMarketplaceFixtures(import.meta.dirname); +const fixtures = fixtureSetup.fixtures; +const CLAUDE_SKILLS_PATH = fixtures?.claudeSkillsPath; +const CLAUDE_PLUGINS_OFFICIAL_PATH = fixtures?.claudePluginsOfficialPath; + +const skipReason = !fixtureSetup.available + ? fixtureSetup.skipReason ?? 'Marketplace repos not found' + : undefined; + +describe('parseMarketplaceJson', { skip: skipReason }, () => { + it('should parse jamie-style marketplace.json', () => { + const result = parseMarketplaceJson(CLAUDE_SKILLS_PATH!); + assert.strictEqual(result.success, true); + if (result.success) { + assert.strictEqual(result.manifest.name, 'jamie-bitflight-skills'); + assert.strictEqual(result.manifest.plugins.length, 26); + } + }); + + it('should parse official-style marketplace.json', () => { + const result = parseMarketplaceJson(CLAUDE_PLUGINS_OFFICIAL_PATH!); + assert.strictEqual(result.success, true); + if (result.success) { + assert.strictEqual(result.manifest.name, 'claude-plugins-official'); + assert.ok(result.manifest.plugins.length > 50); + } + }); + + it('should return error for missing marketplace.json', () => { + const result = parseMarketplaceJson('/tmp/nonexistent'); + assert.strictEqual(result.success, false); + if (!result.success) { + assert.ok(result.error.includes('not found')); + } + }); + + it('should return error for malformed JSON', () => { + const tmpDir = '/tmp/test-marketplace-json-' + Date.now(); + fs.mkdirSync(tmpDir + '/.claude-plugin', { recursive: true }); + fs.writeFileSync(tmpDir + '/.claude-plugin/marketplace.json', '{ invalid json'); + + const result = parseMarketplaceJson(tmpDir); + assert.strictEqual(result.success, false); + if (!result.success) { + assert.ok(result.error.includes('Failed to parse')); + } + + fs.rmSync(tmpDir, { recursive: true }); + }); +}); + +describe('resolvePluginRoot', { skip: skipReason }, () => { + it('should resolve relative paths correctly', () => { + const result = resolvePluginRoot(CLAUDE_SKILLS_PATH!, './plugins/python3-development'); + assert.strictEqual(result, path.join(CLAUDE_SKILLS_PATH!, 'plugins/python3-development')); + }); + + it('should handle paths without ./ prefix', () => { + const result = resolvePluginRoot(CLAUDE_SKILLS_PATH!, 'plugins/python3-development'); + assert.strictEqual(result, path.join(CLAUDE_SKILLS_PATH!, 'plugins/python3-development')); + }); + + it('should return null for external sources', () => { + const result = resolvePluginRoot(CLAUDE_SKILLS_PATH!, 'https://github.com/example/plugin'); + assert.strictEqual(result, null); + }); + + it('should return null for git sources', () => { + const result = resolvePluginRoot(CLAUDE_SKILLS_PATH!, { source: 'github', repo: 'example/plugin' }); + assert.strictEqual(result, null); + }); +}); + +describe('inspectPlugin', { skip: skipReason }, () => { + it('should inspect a plugin with plugin.json', () => { + const pluginDir = path.join(CLAUDE_SKILLS_PATH!, 'plugins/python3-development'); + const result = inspectPlugin(pluginDir); + + assert.strictEqual(result.status, 'ok'); + assert.strictEqual(result.manifestSource, 'plugin.json'); + assert.strictEqual(result.name, 'python3-development'); + assert.ok(result.description !== undefined); + assert.ok(result.version !== undefined); + assert.ok(result.inventory.skills.length > 0); + assert.ok(result.inventory.agents.length > 0); + assert.ok(result.inventory.commands.length > 0); + assert.ok(Object.keys(result.inventory.mcpServers).length > 0); + }); + + it('should return error for non-existent plugin directory', () => { + const result = inspectPlugin('/tmp/nonexistent-plugin'); + assert.strictEqual(result.status, 'error'); + assert.ok(result.error !== undefined, 'error should be defined'); + assert.ok(result.error.includes('not found')); + }); +}); + +describe('discoverMarketplace', { skip: skipReason }, () => { + it('should discover all plugins in jamie-style marketplace', () => { + const result = discoverMarketplace(CLAUDE_SKILLS_PATH!); + + assert.strictEqual(result.status, 'ok'); + assert.strictEqual(result.pluginFormat, 'jamie-style'); + assert.ok(result.plugins.length > 0); + assert.ok(result.plugins.every((p: { status: string }) => p.status === 'ok')); + + assert.strictEqual(result.summary.total, result.plugins.length); + assert.strictEqual(result.summary.ok, result.plugins.length); + assert.strictEqual(result.summary.error, 0); + }); + + it('should discover all plugins in official-style marketplace', () => { + const result = discoverMarketplace(CLAUDE_PLUGINS_OFFICIAL_PATH!); + + assert.strictEqual(result.status, 'ok'); + assert.strictEqual(result.pluginFormat, 'official-style'); + assert.ok(result.plugins.length > 50); + }); + + it('should return structured error for missing marketplace', () => { + const result = discoverMarketplace('/tmp/nonexistent'); + + assert.strictEqual(result.status, 'error'); + assert.ok(result.error !== undefined); + assert.ok(result.error.includes('not found')); + assert.deepStrictEqual(result.plugins, []); + assert.strictEqual(result.summary.total, 0); + }); + + it('should inventory skills, agents, commands correctly', () => { + const result = discoverMarketplace(CLAUDE_SKILLS_PATH!); + const pythonPlugin = result.plugins.find((p: { name: string }) => p.name === 'python3-development'); + + assert.ok(pythonPlugin !== undefined); + if (pythonPlugin) { + assert.ok(pythonPlugin.inventory.skills.length > 30); + assert.ok(pythonPlugin.inventory.agents.length > 10); + assert.ok(pythonPlugin.inventory.commands.length > 0); + } + }); + + it('should discover MCP servers from plugin.json', () => { + const result = discoverMarketplace(CLAUDE_SKILLS_PATH!); + const pythonPlugin = result.plugins.find(p => p.name === 'python3-development'); + + assert.ok(pythonPlugin !== undefined); + if (pythonPlugin) { + assert.ok(Object.keys(pythonPlugin.inventory.mcpServers).includes('cocoindex-code')); + } + }); + + it('should discover LSP servers from marketplace.json', () => { + const result = discoverMarketplace(CLAUDE_PLUGINS_OFFICIAL_PATH!); + const tsPlugin = result.plugins.find(p => p.name === 'typescript-lsp'); + + assert.ok(tsPlugin !== undefined); + if (tsPlugin) { + assert.ok(Object.keys(tsPlugin.inventory.lspServers).includes('typescript')); + } + }); + + it('should detect external plugins correctly', () => { + const result = discoverMarketplace(CLAUDE_PLUGINS_OFFICIAL_PATH!); + const externalPlugin = result.plugins.find(p => p.name === 'atlassian'); + + assert.ok(externalPlugin !== undefined); + if (externalPlugin) { + assert.strictEqual(externalPlugin.resolvedPath, null); + assert.strictEqual(externalPlugin.status, 'ok'); + } + }); +}); + +describe('smoke test', { skip: skipReason }, () => { + it('should be able to run discovery from both marketplace repos', () => { + const jamieResult = discoverMarketplace(CLAUDE_SKILLS_PATH!); + const officialResult = discoverMarketplace(CLAUDE_PLUGINS_OFFICIAL_PATH!); + + assert.strictEqual(jamieResult.status, 'ok'); + assert.strictEqual(officialResult.status, 'ok'); + }); +}); diff --git a/src/resources/extensions/gsd/tests/marketplace-test-fixtures.ts b/src/resources/extensions/gsd/tests/marketplace-test-fixtures.ts new file mode 100644 index 000000000..167803b78 --- /dev/null +++ b/src/resources/extensions/gsd/tests/marketplace-test-fixtures.ts @@ -0,0 +1,91 @@ +import { existsSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { spawnSync } from 'node:child_process'; + +export interface MarketplaceFixtureSet { + claudeSkillsPath: string; + claudePluginsOfficialPath: string; + source: 'local' | 'cloned'; + cleanup: () => void; +} + +const CLAUDE_SKILLS_REPO = 'https://github.com/Jamie-BitFlight/claude_skills.git'; +const CLAUDE_PLUGINS_OFFICIAL_REPO = 'https://github.com/Jamie-BitFlight/claude-plugins-official.git'; +const CLONE_FIXTURES_ENABLED = process.env.GSD_TEST_CLONE_MARKETPLACES === '1'; + +function canRunGit(): boolean { + const result = spawnSync('git', ['--version'], { stdio: 'ignore' }); + return result.status === 0; +} + +function cloneRepo(repo: string, dest: string): void { + const result = spawnSync('git', ['clone', '--depth', '1', repo, dest], { + stdio: 'pipe', + encoding: 'utf8', + timeout: 120000, + }); + + if (result.status !== 0) { + const stderr = (result.stderr || result.stdout || '').trim(); + throw new Error(`git clone failed for ${repo}: ${stderr}`); + } +} + +export function getMarketplaceFixtures(testFileDir: string): { available: boolean; skipReason?: string; fixtures?: MarketplaceFixtureSet } { + const gsd2Root = resolve(testFileDir, '../../../../..'); + const localClaudeSkillsPath = resolve(gsd2Root, '../claude_skills'); + const localClaudePluginsOfficialPath = resolve(gsd2Root, '../claude-plugins-official'); + + if (existsSync(localClaudeSkillsPath) && existsSync(localClaudePluginsOfficialPath)) { + return { + available: true, + fixtures: { + claudeSkillsPath: localClaudeSkillsPath, + claudePluginsOfficialPath: localClaudePluginsOfficialPath, + source: 'local', + cleanup: () => {}, + }, + }; + } + + if (!CLONE_FIXTURES_ENABLED) { + return { + available: false, + skipReason: 'Marketplace repos absent and clone-based fixtures are disabled (set GSD_TEST_CLONE_MARKETPLACES=1 to enable)', + }; + } + + if (!canRunGit()) { + return { + available: false, + skipReason: 'Marketplace repos absent and git is unavailable for cloning test fixtures', + }; + } + + try { + const fixtureRoot = mkdtempSync(join(tmpdir(), 'gsd-marketplace-fixtures-')); + const clonedClaudeSkillsPath = join(fixtureRoot, 'claude_skills'); + const clonedClaudePluginsOfficialPath = join(fixtureRoot, 'claude-plugins-official'); + + cloneRepo(CLAUDE_SKILLS_REPO, clonedClaudeSkillsPath); + cloneRepo(CLAUDE_PLUGINS_OFFICIAL_REPO, clonedClaudePluginsOfficialPath); + + return { + available: true, + fixtures: { + claudeSkillsPath: clonedClaudeSkillsPath, + claudePluginsOfficialPath: clonedClaudePluginsOfficialPath, + source: 'cloned', + cleanup: () => { + rmSync(fixtureRoot, { recursive: true, force: true }); + }, + }, + }; + } catch (error) { + return { + available: false, + skipReason: error instanceof Error ? error.message : String(error), + }; + } +} diff --git a/src/resources/extensions/gsd/tests/namespaced-registry.test.ts b/src/resources/extensions/gsd/tests/namespaced-registry.test.ts new file mode 100644 index 000000000..f73915e1f --- /dev/null +++ b/src/resources/extensions/gsd/tests/namespaced-registry.test.ts @@ -0,0 +1,1027 @@ +/** + * Namespaced Registry Contract Tests + * + * Tests that prove the namespaced registry correctly handles: + * - Canonical identity (R004) + * - Canonical skill lookup (R005) + * - Canonical agent lookup (R006) + * - Flat compatibility + * - Collision detection + * - Namespace listing + * - Integration with S01 discovery types + */ + +import { describe, it, beforeEach } from 'node:test'; +import assert from 'node:assert'; +import { + NamespacedRegistry, + componentsFromDiscovery, +} from '../namespaced-registry.js'; +import type { DiscoveredPlugin } from '../marketplace-discovery.js'; + +describe('NamespacedRegistry', () => { + let registry: NamespacedRegistry; + + beforeEach(() => { + registry = new NamespacedRegistry(); + }); + + describe('canonical registration and lookup', () => { + it('should register a namespaced skill and compute canonical name (R004, R005)', () => { + const diagnostic = registry.register({ + name: 'my-skill', + namespace: 'my-plugin', + type: 'skill', + filePath: '/plugins/my-plugin/skills/my-skill/SKILL.md', + source: 'plugin:my-plugin', + description: 'A test skill', + metadata: { pluginVersion: '1.0.0' }, + }); + + // No collision diagnostic expected + assert.strictEqual(diagnostic, undefined); + + // Verify registration succeeded + assert.strictEqual(registry.size, 1); + assert.strictEqual(registry.has('my-plugin:my-skill'), true); + + // Lookup by canonical name + const component = registry.getByCanonical('my-plugin:my-skill'); + assert.ok(component !== undefined); + + // Verify canonical identity preserved (R004) + assert.strictEqual(component.name, 'my-skill'); + assert.strictEqual(component.namespace, 'my-plugin'); + assert.strictEqual(component.canonicalName, 'my-plugin:my-skill'); + assert.strictEqual(component.type, 'skill'); + assert.strictEqual(component.filePath, '/plugins/my-plugin/skills/my-skill/SKILL.md'); + assert.strictEqual(component.source, 'plugin:my-plugin'); + assert.strictEqual(component.description, 'A test skill'); + assert.strictEqual(component.metadata.pluginVersion, '1.0.0'); + }); + + it('should register a namespaced agent and compute canonical name (R006)', () => { + const diagnostic = registry.register({ + name: 'abby', + namespace: 'farm', + type: 'agent', + filePath: '/plugins/farm/agents/abby/AGENT.md', + source: 'plugin:farm', + description: 'A farm agent', + metadata: { pluginAuthor: 'farm-team' }, + }); + + assert.strictEqual(diagnostic, undefined); + assert.strictEqual(registry.size, 1); + + // Lookup by canonical name (R006) + const agent = registry.getByCanonical('farm:abby'); + assert.ok(agent !== undefined); + + // Verify canonical identity (R004) + assert.strictEqual(agent.name, 'abby'); + assert.strictEqual(agent.namespace, 'farm'); + assert.strictEqual(agent.canonicalName, 'farm:abby'); + assert.strictEqual(agent.type, 'agent'); + }); + + it('should return undefined for non-existent canonical name', () => { + const result = registry.getByCanonical('nonexistent:skill'); + assert.strictEqual(result, undefined); + }); + }); + + describe('flat (non-namespaced) compatibility', () => { + it('should register flat component with bare name as canonical', () => { + const diagnostic = registry.register({ + name: 'code-review', + namespace: undefined, + type: 'skill', + filePath: '/skills/code-review/SKILL.md', + source: 'user', + description: 'A flat skill', + metadata: {}, + }); + + assert.strictEqual(diagnostic, undefined); + + // Lookup by bare name (no namespace prefix) + const skill = registry.getByCanonical('code-review'); + assert.ok(skill !== undefined); + assert.strictEqual(skill.name, 'code-review'); + assert.strictEqual(skill.namespace, undefined); + assert.strictEqual(skill.canonicalName, 'code-review'); + }); + + it('should retrieve flat component by bare name', () => { + registry.register({ + name: 'test-skill', + namespace: undefined, + type: 'skill', + filePath: '/skills/test-skill/SKILL.md', + source: 'project', + description: undefined, + metadata: {}, + }); + + const skill = registry.getByCanonical('test-skill'); + assert.ok(skill !== undefined); + assert.strictEqual(skill.canonicalName, 'test-skill'); + }); + }); + + describe('collision detection', () => { + it('should detect collision on duplicate canonical name and emit diagnostic', () => { + // First registration wins + const first = registry.register({ + name: 'code-review', + namespace: 'my-plugin', + type: 'skill', + filePath: '/plugins/my-plugin/skills/code-review/SKILL.md', + source: 'plugin:my-plugin', + description: 'First skill', + metadata: {}, + }); + assert.strictEqual(first, undefined); + + // Second registration collides + const second = registry.register({ + name: 'code-review', + namespace: 'my-plugin', + type: 'skill', + filePath: '/plugins/other-plugin/skills/code-review/SKILL.md', + source: 'plugin:other-plugin', + description: 'Second skill', + metadata: {}, + }); + + // Should return collision diagnostic + assert.ok(second !== undefined); + assert.strictEqual(second.type, 'collision'); + assert.strictEqual(second.message, 'canonical name "my-plugin:code-review" collision'); + + // Verify collision details + assert.strictEqual(second.collision.canonicalName, 'my-plugin:code-review'); + assert.strictEqual(second.collision.winnerPath, '/plugins/my-plugin/skills/code-review/SKILL.md'); + assert.strictEqual(second.collision.loserPath, '/plugins/other-plugin/skills/code-review/SKILL.md'); + assert.strictEqual(second.collision.winnerSource, 'plugin:my-plugin'); + assert.strictEqual(second.collision.loserSource, 'plugin:other-plugin'); + }); + + it('should preserve first-wins behavior on collision', () => { + // Register first + registry.register({ + name: 'skill', + namespace: 'ns', + type: 'skill', + filePath: '/first/SKILL.md', + source: 'first', + description: 'First description', + metadata: { key: 'first-value' }, + }); + + // Attempt duplicate + registry.register({ + name: 'skill', + namespace: 'ns', + type: 'skill', + filePath: '/second/SKILL.md', + source: 'second', + description: 'Second description', + metadata: { key: 'second-value' }, + }); + + // First registration wins + const component = registry.getByCanonical('ns:skill'); + assert.ok(component !== undefined); + assert.strictEqual(component.filePath, '/first/SKILL.md'); + assert.strictEqual(component.source, 'first'); + assert.strictEqual(component.description, 'First description'); + assert.strictEqual(component.metadata.key, 'first-value'); + }); + + it('should collect multiple collision diagnostics', () => { + // First registrations + registry.register({ + name: 'skill-a', + namespace: 'plugin-x', + type: 'skill', + filePath: '/x/a.md', + source: 'x', + description: undefined, + metadata: {}, + }); + registry.register({ + name: 'skill-b', + namespace: 'plugin-y', + type: 'skill', + filePath: '/y/b.md', + source: 'y', + description: undefined, + metadata: {}, + }); + + // Collisions + registry.register({ + name: 'skill-a', + namespace: 'plugin-x', + type: 'skill', + filePath: '/z/a.md', + source: 'z', + description: undefined, + metadata: {}, + }); + registry.register({ + name: 'skill-b', + namespace: 'plugin-y', + type: 'skill', + filePath: '/w/b.md', + source: 'w', + description: undefined, + metadata: {}, + }); + + const diagnostics = registry.getDiagnostics(); + assert.strictEqual(diagnostics.length, 2); + assert.strictEqual(diagnostics[0].collision.canonicalName, 'plugin-x:skill-a'); + assert.strictEqual(diagnostics[1].collision.canonicalName, 'plugin-y:skill-b'); + }); + + it('should allow same name in different namespaces', () => { + // Same name, different namespace + registry.register({ + name: 'code-review', + namespace: 'plugin-a', + type: 'skill', + filePath: '/a/code-review.md', + source: 'plugin:plugin-a', + description: undefined, + metadata: {}, + }); + registry.register({ + name: 'code-review', + namespace: 'plugin-b', + type: 'skill', + filePath: '/b/code-review.md', + source: 'plugin:plugin-b', + description: undefined, + metadata: {}, + }); + + // Both should be registered + assert.strictEqual(registry.size, 2); + + const a = registry.getByCanonical('plugin-a:code-review'); + const b = registry.getByCanonical('plugin-b:code-review'); + + assert.ok(a !== undefined); + assert.ok(b !== undefined); + assert.strictEqual(a.filePath, '/a/code-review.md'); + assert.strictEqual(b.filePath, '/b/code-review.md'); + + // No collisions + assert.strictEqual(registry.getDiagnostics().length, 0); + }); + + it('should allow flat and namespaced components with same local name', () => { + // Flat component + registry.register({ + name: 'code-review', + namespace: undefined, + type: 'skill', + filePath: '/flat/code-review.md', + source: 'user', + description: undefined, + metadata: {}, + }); + + // Namespaced component with same local name + registry.register({ + name: 'code-review', + namespace: 'plugin', + type: 'skill', + filePath: '/plugin/code-review.md', + source: 'plugin:plugin', + description: undefined, + metadata: {}, + }); + + // Both should be accessible + const flat = registry.getByCanonical('code-review'); + const namespaced = registry.getByCanonical('plugin:code-review'); + + assert.ok(flat !== undefined); + assert.ok(namespaced !== undefined); + assert.strictEqual(flat.namespace, undefined); + assert.strictEqual(namespaced.namespace, 'plugin'); + + assert.strictEqual(registry.getDiagnostics().length, 0); + }); + }); + + describe('namespace listing', () => { + it('should list all components in a namespace via getByNamespace', () => { + // Register multiple components in plugin-a + registry.register({ + name: 'skill-1', + namespace: 'plugin-a', + type: 'skill', + filePath: '/a/skill-1.md', + source: 'plugin:plugin-a', + description: undefined, + metadata: {}, + }); + registry.register({ + name: 'skill-2', + namespace: 'plugin-a', + type: 'skill', + filePath: '/a/skill-2.md', + source: 'plugin:plugin-a', + description: undefined, + metadata: {}, + }); + registry.register({ + name: 'agent-1', + namespace: 'plugin-a', + type: 'agent', + filePath: '/a/agent-1.md', + source: 'plugin:plugin-a', + description: undefined, + metadata: {}, + }); + + // Register component in different namespace + registry.register({ + name: 'skill-3', + namespace: 'plugin-b', + type: 'skill', + filePath: '/b/skill-3.md', + source: 'plugin:plugin-b', + description: undefined, + metadata: {}, + }); + + const pluginAComponents = registry.getByNamespace('plugin-a'); + assert.strictEqual(pluginAComponents.length, 3); + + const names = pluginAComponents.map((c) => c.name).sort(); + assert.deepStrictEqual(names, ['agent-1', 'skill-1', 'skill-2']); + + // All should have correct namespace + assert.ok(pluginAComponents.every((c) => c.namespace === 'plugin-a')); + }); + + it('should return empty array for non-existent namespace', () => { + const result = registry.getByNamespace('nonexistent'); + assert.deepStrictEqual(result, []); + }); + + it('should not include flat components in namespace listing', () => { + // Flat component + registry.register({ + name: 'flat-skill', + namespace: undefined, + type: 'skill', + filePath: '/flat.md', + source: 'user', + description: undefined, + metadata: {}, + }); + + // Namespaced component + registry.register({ + name: 'ns-skill', + namespace: 'plugin', + type: 'skill', + filePath: '/plugin/ns-skill.md', + source: 'plugin:plugin', + description: undefined, + metadata: {}, + }); + + // Flat components have namespace=undefined, not included + const pluginComponents = registry.getByNamespace('plugin'); + assert.strictEqual(pluginComponents.length, 1); + assert.strictEqual(pluginComponents[0].name, 'ns-skill'); + }); + }); + + describe('mixed coexistence', () => { + it('should allow both namespaced and flat components without interference', () => { + // Flat skill + registry.register({ + name: 'review', + namespace: undefined, + type: 'skill', + filePath: '/skills/review/SKILL.md', + source: 'user', + description: 'User skill', + metadata: {}, + }); + + // Namespaced skill + registry.register({ + name: 'review', + namespace: 'my-plugin', + type: 'skill', + filePath: '/plugins/my-plugin/skills/review/SKILL.md', + source: 'plugin:my-plugin', + description: 'Plugin skill', + metadata: { pluginVersion: '1.0.0' }, + }); + + // Namespaced agent + registry.register({ + name: 'builder', + namespace: 'my-plugin', + type: 'agent', + filePath: '/plugins/my-plugin/agents/builder/AGENT.md', + source: 'plugin:my-plugin', + description: 'Plugin agent', + metadata: {}, + }); + + // Flat agent + registry.register({ + name: 'assistant', + namespace: undefined, + type: 'agent', + filePath: '/agents/assistant/AGENT.md', + source: 'project', + description: 'Project agent', + metadata: {}, + }); + + // Verify total count + assert.strictEqual(registry.size, 4); + + // Flat skill + const flatSkill = registry.getByCanonical('review'); + assert.ok(flatSkill !== undefined); + assert.strictEqual(flatSkill.namespace, undefined); + assert.strictEqual(flatSkill.type, 'skill'); + + // Namespaced skill + const nsSkill = registry.getByCanonical('my-plugin:review'); + assert.ok(nsSkill !== undefined); + assert.strictEqual(nsSkill.namespace, 'my-plugin'); + assert.strictEqual(nsSkill.type, 'skill'); + + // Namespaced agent + const nsAgent = registry.getByCanonical('my-plugin:builder'); + assert.ok(nsAgent !== undefined); + assert.strictEqual(nsAgent.namespace, 'my-plugin'); + assert.strictEqual(nsAgent.type, 'agent'); + + // Flat agent + const flatAgent = registry.getByCanonical('assistant'); + assert.ok(flatAgent !== undefined); + assert.strictEqual(flatAgent.namespace, undefined); + assert.strictEqual(flatAgent.type, 'agent'); + + // Namespace listing + const myPluginComponents = registry.getByNamespace('my-plugin'); + assert.strictEqual(myPluginComponents.length, 2); + + // No collisions + assert.strictEqual(registry.getDiagnostics().length, 0); + }); + }); + + describe('getAll and has', () => { + it('should return all components via getAll', () => { + registry.register({ + name: 'skill-1', + namespace: 'plugin-a', + type: 'skill', + filePath: '/a/s1.md', + source: 'a', + description: undefined, + metadata: {}, + }); + registry.register({ + name: 'skill-2', + namespace: undefined, + type: 'skill', + filePath: '/s2.md', + source: 'user', + description: undefined, + metadata: {}, + }); + + const all = registry.getAll(); + assert.strictEqual(all.length, 2); + + const canonicalNames = all.map((c) => c.canonicalName).sort(); + assert.deepStrictEqual(canonicalNames, ['plugin-a:skill-1', 'skill-2']); + }); + + it('should check existence via has', () => { + registry.register({ + name: 'test', + namespace: 'ns', + type: 'skill', + filePath: '/test.md', + source: 'test', + description: undefined, + metadata: {}, + }); + + assert.strictEqual(registry.has('ns:test'), true); + assert.strictEqual(registry.has('ns:other'), false); + assert.strictEqual(registry.has('test'), false); + }); + }); +}); + +describe('componentsFromDiscovery', () => { + it('should convert DiscoveredPlugin to registerable components', () => { + const mockPlugin: DiscoveredPlugin = { + name: 'test-plugin', + canonicalName: 'test-plugin', + source: './plugins/test-plugin', + resolvedPath: '/plugins/test-plugin', + status: 'ok', + manifestSource: 'plugin.json', + description: 'A test plugin', + version: '1.0.0', + author: { name: 'Test Author' }, + category: 'testing', + homepage: 'https://example.com/test-plugin', + inventory: { + skills: ['skill-a', 'skill-b'], + agents: ['agent-x'], + commands: [], + mcpServers: {}, + lspServers: {}, + hooks: [], + }, + }; + + const components = componentsFromDiscovery(mockPlugin); + + // Should have 3 components (2 skills + 1 agent) + assert.strictEqual(components.length, 3); + + // All should have the plugin's canonical name as namespace + assert.ok(components.every((c) => c.namespace === 'test-plugin')); + + // Verify skills + const skills = components.filter((c) => c.type === 'skill'); + assert.strictEqual(skills.length, 2); + + const skillNames = skills.map((c) => c.name).sort(); + assert.deepStrictEqual(skillNames, ['skill-a', 'skill-b']); + + // Verify agents + const agents = components.filter((c) => c.type === 'agent'); + assert.strictEqual(agents.length, 1); + assert.strictEqual(agents[0].name, 'agent-x'); + + // Verify metadata propagation + assert.strictEqual(skills[0].metadata.pluginVersion, '1.0.0'); + assert.strictEqual(skills[0].metadata.pluginAuthor, 'Test Author'); + assert.strictEqual(skills[0].metadata.pluginHomepage, 'https://example.com/test-plugin'); + assert.strictEqual(skills[0].metadata.pluginCategory, 'testing'); + + // Verify source format + assert.strictEqual(skills[0].source, 'plugin:test-plugin'); + }); + + it('should handle plugin without resolvedPath (external plugin)', () => { + const externalPlugin: DiscoveredPlugin = { + name: 'external-plugin', + canonicalName: 'external-plugin', + source: { source: 'github', repo: 'example/plugin' }, + resolvedPath: null, // External - not locally resolved + status: 'ok', + manifestSource: 'marketplace-inline', + description: 'An external plugin', + inventory: { + skills: ['remote-skill'], + agents: [], + commands: [], + mcpServers: {}, + lspServers: {}, + hooks: [], + }, + }; + + const components = componentsFromDiscovery(externalPlugin); + + assert.strictEqual(components.length, 1); + assert.strictEqual(components[0].name, 'remote-skill'); + assert.strictEqual(components[0].namespace, 'external-plugin'); + assert.ok(components[0].filePath.includes('')); + }); + + it('should produce components that can be registered in NamespacedRegistry', () => { + const mockPlugin: DiscoveredPlugin = { + name: 'integration-plugin', + canonicalName: 'integration-plugin', + source: './plugins/integration', + resolvedPath: '/plugins/integration', + status: 'ok', + manifestSource: 'plugin.json', + inventory: { + skills: ['int-skill'], + agents: ['int-agent'], + commands: [], + mcpServers: {}, + lspServers: {}, + hooks: [], + }, + }; + + const registry = new NamespacedRegistry(); + const components = componentsFromDiscovery(mockPlugin); + + // Register all components + for (const component of components) { + const diag = registry.register(component); + assert.strictEqual(diag, undefined, 'No collision expected'); + } + + // Verify registration + assert.strictEqual(registry.size, 2); + assert.ok(registry.has('integration-plugin:int-skill')); + assert.ok(registry.has('integration-plugin:int-agent')); + + // Lookup and verify + const skill = registry.getByCanonical('integration-plugin:int-skill'); + assert.ok(skill !== undefined); + assert.strictEqual(skill.type, 'skill'); + + const agent = registry.getByCanonical('integration-plugin:int-agent'); + assert.ok(agent !== undefined); + assert.strictEqual(agent.type, 'agent'); + }); + + it('should strip .md extension from skill/agent names if present', () => { + const pluginWithMd: DiscoveredPlugin = { + name: 'md-plugin', + canonicalName: 'md-plugin', + source: './plugins/md', + resolvedPath: '/plugins/md', + status: 'ok', + manifestSource: 'derived', + inventory: { + skills: ['skill.md'], // .md extension in inventory + agents: ['agent.md'], + commands: [], + mcpServers: {}, + lspServers: {}, + hooks: [], + }, + }; + + const components = componentsFromDiscovery(pluginWithMd); + + const skill = components.find((c) => c.type === 'skill'); + const agent = components.find((c) => c.type === 'agent'); + + assert.ok(skill !== undefined); + assert.ok(agent !== undefined); + assert.strictEqual(skill.name, 'skill'); // .md stripped + assert.strictEqual(agent.name, 'agent'); // .md stripped + }); +}); + +describe('diagnostic structure verification', () => { + it('should emit diagnostic with correct RegistryCollision shape', () => { + const registry = new NamespacedRegistry(); + + registry.register({ + name: 'dup', + namespace: 'ns', + type: 'skill', + filePath: '/first/dup.md', + source: 'first-source', + description: undefined, + metadata: {}, + }); + + const diag = registry.register({ + name: 'dup', + namespace: 'ns', + type: 'skill', + filePath: '/second/dup.md', + source: 'second-source', + description: undefined, + metadata: {}, + }); + + assert.ok(diag !== undefined); + + // Verify diagnostic type + assert.strictEqual(diag.type, 'collision'); + + // Verify message format + assert.ok(diag.message.includes('ns:dup')); + assert.ok(diag.message.includes('collision')); + + // Verify collision object structure + assert.strictEqual(diag.collision.canonicalName, 'ns:dup'); + assert.strictEqual(diag.collision.winnerPath, '/first/dup.md'); + assert.strictEqual(diag.collision.loserPath, '/second/dup.md'); + assert.strictEqual(diag.collision.winnerSource, 'first-source'); + assert.strictEqual(diag.collision.loserSource, 'second-source'); + }); + + it('should provide inspectable diagnostics via getDiagnostics', () => { + const registry = new NamespacedRegistry(); + + // Create collision + registry.register({ + name: 'skill', + namespace: 'plugin', + type: 'skill', + filePath: '/a/skill.md', + source: 'a', + description: undefined, + metadata: {}, + }); + registry.register({ + name: 'skill', + namespace: 'plugin', + type: 'skill', + filePath: '/b/skill.md', + source: 'b', + description: undefined, + metadata: {}, + }); + + const diagnostics = registry.getDiagnostics(); + + assert.strictEqual(diagnostics.length, 1); + + // Verify diagnostic is a copy (not mutable reference) + diagnostics[0].message = 'modified'; + const freshDiagnostics = registry.getDiagnostics(); + assert.strictEqual(freshDiagnostics[0].message, 'canonical name "plugin:skill" collision'); + }); +}); + +describe('alias management', () => { + let registry: NamespacedRegistry; + + beforeEach(() => { + registry = new NamespacedRegistry(); + }); + + describe('registerAlias', () => { + it('should register an alias for an existing canonical name', () => { + registry.register({ + name: '3d-visualizer', + namespace: 'python-tools', + type: 'skill', + filePath: '/python-tools/3d-visualizer/SKILL.md', + source: 'plugin:python-tools', + description: '3D visualization', + metadata: {}, + }); + + const result = registry.registerAlias('py3d', 'python-tools:3d-visualizer'); + + assert.strictEqual(result.success, true); + assert.strictEqual(registry.hasAlias('py3d'), true); + assert.strictEqual(registry.resolveAlias('py3d'), 'python-tools:3d-visualizer'); + }); + + it('should reject alias if target canonical name does not exist', () => { + const result = registry.registerAlias('py3d', 'nonexistent:skill'); + + assert.strictEqual(result.success, false); + assert.strictEqual(result.reason, 'canonical-not-found'); + assert.ok(result.message?.includes('does not exist')); + }); + + it('should reject alias that shadows an existing canonical name', () => { + registry.register({ + name: 'existing', + namespace: 'plugin', + type: 'skill', + filePath: '/plugin/existing/SKILL.md', + source: 'plugin:plugin', + description: 'Existing skill', + metadata: {}, + }); + registry.register({ + name: 'other', + namespace: 'plugin', + type: 'skill', + filePath: '/plugin/other/SKILL.md', + source: 'plugin:plugin', + description: 'Other skill', + metadata: {}, + }); + + // Try to create alias that matches an existing canonical name + const result = registry.registerAlias('plugin:existing', 'plugin:other'); + + assert.strictEqual(result.success, false); + assert.strictEqual(result.reason, 'shadows-canonical'); + assert.ok(result.message?.includes('shadows an existing canonical name')); + }); + + it('should reject duplicate alias pointing to different target', () => { + registry.register({ + name: 'skill-a', + namespace: 'plugin', + type: 'skill', + filePath: '/plugin/skill-a/SKILL.md', + source: 'plugin:plugin', + description: 'Skill A', + metadata: {}, + }); + registry.register({ + name: 'skill-b', + namespace: 'plugin', + type: 'skill', + filePath: '/plugin/skill-b/SKILL.md', + source: 'plugin:plugin', + description: 'Skill B', + metadata: {}, + }); + + // First alias succeeds + const first = registry.registerAlias('shortcut', 'plugin:skill-a'); + assert.strictEqual(first.success, true); + + // Second alias with same name but different target fails + const second = registry.registerAlias('shortcut', 'plugin:skill-b'); + assert.strictEqual(second.success, false); + assert.strictEqual(second.reason, 'duplicate-alias'); + assert.ok(second.message?.includes('already exists')); + }); + + it('should be idempotent for same alias and target', () => { + registry.register({ + name: 'skill', + namespace: 'plugin', + type: 'skill', + filePath: '/plugin/skill/SKILL.md', + source: 'plugin:plugin', + description: 'Skill', + metadata: {}, + }); + + // Register alias twice with same target + const first = registry.registerAlias('s', 'plugin:skill'); + assert.strictEqual(first.success, true); + + const second = registry.registerAlias('s', 'plugin:skill'); + assert.strictEqual(second.success, true); + }); + + it('should allow multiple aliases for same canonical name', () => { + registry.register({ + name: 'visualizer', + namespace: 'python-tools', + type: 'skill', + filePath: '/python-tools/visualizer/SKILL.md', + source: 'plugin:python-tools', + description: 'Visualizer', + metadata: {}, + }); + + const r1 = registry.registerAlias('pyviz', 'python-tools:visualizer'); + const r2 = registry.registerAlias('viz', 'python-tools:visualizer'); + const r3 = registry.registerAlias('py3d', 'python-tools:visualizer'); + + assert.strictEqual(r1.success, true); + assert.strictEqual(r2.success, true); + assert.strictEqual(r3.success, true); + + assert.strictEqual(registry.resolveAlias('pyviz'), 'python-tools:visualizer'); + assert.strictEqual(registry.resolveAlias('viz'), 'python-tools:visualizer'); + assert.strictEqual(registry.resolveAlias('py3d'), 'python-tools:visualizer'); + }); + }); + + describe('resolveAlias', () => { + it('should resolve registered alias to canonical name', () => { + registry.register({ + name: 'skill', + namespace: 'ns', + type: 'skill', + filePath: '/ns/skill/SKILL.md', + source: 'plugin:ns', + description: 'Skill', + metadata: {}, + }); + registry.registerAlias('s', 'ns:skill'); + + assert.strictEqual(registry.resolveAlias('s'), 'ns:skill'); + }); + + it('should return undefined for non-existent alias', () => { + assert.strictEqual(registry.resolveAlias('nonexistent'), undefined); + }); + }); + + describe('removeAlias', () => { + it('should remove an existing alias', () => { + registry.register({ + name: 'skill', + namespace: 'ns', + type: 'skill', + filePath: '/ns/skill/SKILL.md', + source: 'plugin:ns', + description: 'Skill', + metadata: {}, + }); + registry.registerAlias('s', 'ns:skill'); + + assert.strictEqual(registry.hasAlias('s'), true); + + const removed = registry.removeAlias('s'); + assert.strictEqual(removed, true); + assert.strictEqual(registry.hasAlias('s'), false); + assert.strictEqual(registry.resolveAlias('s'), undefined); + }); + + it('should return false for non-existent alias', () => { + const removed = registry.removeAlias('nonexistent'); + assert.strictEqual(removed, false); + }); + }); + + describe('getAliases', () => { + it('should return empty map when no aliases registered', () => { + const aliases = registry.getAliases(); + assert.strictEqual(aliases.size, 0); + }); + + it('should return copy of alias map', () => { + registry.register({ + name: 'skill', + namespace: 'ns', + type: 'skill', + filePath: '/ns/skill/SKILL.md', + source: 'plugin:ns', + description: 'Skill', + metadata: {}, + }); + registry.registerAlias('s', 'ns:skill'); + + const aliases = registry.getAliases(); + assert.strictEqual(aliases.size, 1); + assert.strictEqual(aliases.get('s'), 'ns:skill'); + + // Mutating returned map should not affect registry + aliases.set('other', 'ns:other'); + assert.strictEqual(registry.hasAlias('other'), false); + }); + + it('should include all registered aliases', () => { + registry.register({ + name: 'skill-a', + namespace: 'ns', + type: 'skill', + filePath: '/ns/a/SKILL.md', + source: 'plugin:ns', + description: 'A', + metadata: {}, + }); + registry.register({ + name: 'skill-b', + namespace: 'ns', + type: 'skill', + filePath: '/ns/b/SKILL.md', + source: 'plugin:ns', + description: 'B', + metadata: {}, + }); + + registry.registerAlias('sa', 'ns:skill-a'); + registry.registerAlias('sb', 'ns:skill-b'); + + const aliases = registry.getAliases(); + assert.strictEqual(aliases.size, 2); + assert.strictEqual(aliases.get('sa'), 'ns:skill-a'); + assert.strictEqual(aliases.get('sb'), 'ns:skill-b'); + }); + }); + + describe('hasAlias', () => { + it('should return true for registered alias', () => { + registry.register({ + name: 'skill', + namespace: 'ns', + type: 'skill', + filePath: '/ns/skill/SKILL.md', + source: 'plugin:ns', + description: 'Skill', + metadata: {}, + }); + registry.registerAlias('s', 'ns:skill'); + + assert.strictEqual(registry.hasAlias('s'), true); + }); + + it('should return false for non-existent alias', () => { + assert.strictEqual(registry.hasAlias('nonexistent'), false); + }); + }); +}); \ No newline at end of file diff --git a/src/resources/extensions/gsd/tests/namespaced-resolver.test.ts b/src/resources/extensions/gsd/tests/namespaced-resolver.test.ts new file mode 100644 index 000000000..c81c6a254 --- /dev/null +++ b/src/resources/extensions/gsd/tests/namespaced-resolver.test.ts @@ -0,0 +1,671 @@ +/** + * Namespaced Resolver Contract Tests + * + * Tests that prove the resolver correctly handles: + * - R007: Canonical skill lookup + * - R008: Canonical agent lookup + * - D003: Same-plugin local-first resolution + * - R009: Shorthand resolution (unambiguous and ambiguous) + * - Flat component compatibility + * - Type filtering (skill vs agent) + */ + +import { describe, it, beforeEach } from 'node:test'; +import assert from 'node:assert'; +import { NamespacedRegistry } from '../namespaced-registry.js'; +import { NamespacedResolver } from '../namespaced-resolver.js'; + +describe('NamespacedResolver', () => { + let registry: NamespacedRegistry; + let resolver: NamespacedResolver; + + beforeEach(() => { + registry = new NamespacedRegistry(); + resolver = new NamespacedResolver(registry); + }); + + describe('canonical lookup (R007, R008)', () => { + it('should resolve canonical skill name with canonical result (R007)', () => { + registry.register({ + name: 'call-horse', + namespace: 'farm', + type: 'skill', + filePath: '/farm/call-horse/SKILL.md', + source: 'plugin:farm', + description: 'Calls a horse', + metadata: {}, + }); + + const result = resolver.resolve('farm:call-horse'); + + assert.strictEqual(result.resolution, 'canonical'); + if (result.resolution !== 'canonical') throw new Error('Type guard'); + + assert.strictEqual(result.requestedName, 'farm:call-horse'); + assert.strictEqual(result.component.canonicalName, 'farm:call-horse'); + assert.strictEqual(result.component.type, 'skill'); + }); + + it('should resolve canonical agent name with canonical result (R008)', () => { + registry.register({ + name: 'rancher', + namespace: 'farm', + type: 'agent', + filePath: '/farm/rancher/AGENT.md', + source: 'plugin:farm', + description: 'Farm agent', + metadata: {}, + }); + + const result = resolver.resolve('farm:rancher'); + + assert.strictEqual(result.resolution, 'canonical'); + if (result.resolution !== 'canonical') throw new Error('Type guard'); + + assert.strictEqual(result.component.canonicalName, 'farm:rancher'); + assert.strictEqual(result.component.type, 'agent'); + }); + + it('should return not-found for non-existent canonical name', () => { + const result = resolver.resolve('nonexistent:skill'); + assert.strictEqual(result.resolution, 'not-found'); + }); + + it('should return not-found for canonical name with wrong type filter', () => { + registry.register({ + name: 'call-horse', + namespace: 'farm', + type: 'skill', + filePath: '/farm/call-horse/SKILL.md', + source: 'plugin:farm', + description: 'Calls a horse', + metadata: {}, + }); + + const result = resolver.resolve('farm:call-horse', undefined, 'agent'); + assert.strictEqual(result.resolution, 'not-found'); + }); + }); + + describe('local-first resolution (D003)', () => { + it('should resolve bare name local-first when caller namespace has match', () => { + registry.register({ + name: 'call-horse', + namespace: 'farm', + type: 'skill', + filePath: '/farm/call-horse/SKILL.md', + source: 'plugin:farm', + description: 'Farm horse caller', + metadata: {}, + }); + registry.register({ + name: 'call-horse', + namespace: 'zoo', + type: 'skill', + filePath: '/zoo/call-horse/SKILL.md', + source: 'plugin:zoo', + description: 'Zoo horse caller', + metadata: {}, + }); + + const result = resolver.resolve('call-horse', { callerNamespace: 'farm' }); + + assert.strictEqual(result.resolution, 'local-first'); + if (result.resolution !== 'local-first') throw new Error('Type guard'); + + assert.strictEqual(result.requestedName, 'call-horse'); + assert.strictEqual(result.component.canonicalName, 'farm:call-horse'); + assert.strictEqual(result.matchedNamespace, 'farm'); + }); + + it('should resolve local-first from zoo namespace context', () => { + registry.register({ + name: 'call-horse', + namespace: 'farm', + type: 'skill', + filePath: '/farm/call-horse/SKILL.md', + source: 'plugin:farm', + description: 'Farm horse caller', + metadata: {}, + }); + registry.register({ + name: 'call-horse', + namespace: 'zoo', + type: 'skill', + filePath: '/zoo/call-horse/SKILL.md', + source: 'plugin:zoo', + description: 'Zoo horse caller', + metadata: {}, + }); + + const result = resolver.resolve('call-horse', { callerNamespace: 'zoo' }); + + assert.strictEqual(result.resolution, 'local-first'); + if (result.resolution !== 'local-first') throw new Error('Type guard'); + + assert.strictEqual(result.component.canonicalName, 'zoo:call-horse'); + }); + + it('should fall through to shorthand when local namespace has no match', () => { + registry.register({ + name: 'feed-chickens', + namespace: 'farm', + type: 'skill', + filePath: '/farm/feed-chickens/SKILL.md', + source: 'plugin:farm', + description: 'Feed chickens', + metadata: {}, + }); + + const result = resolver.resolve('feed-chickens', { callerNamespace: 'zoo' }); + + assert.strictEqual(result.resolution, 'shorthand'); + if (result.resolution !== 'shorthand') throw new Error('Type guard'); + + assert.strictEqual(result.component.canonicalName, 'farm:feed-chickens'); + }); + + it('should respect type filter in local-first resolution', () => { + // Register two different names - one skill, one agent + registry.register({ + name: 'helper-skill', + namespace: 'farm', + type: 'skill', + filePath: '/farm/helper-skill/SKILL.md', + source: 'plugin:farm', + description: 'Helper skill', + metadata: {}, + }); + registry.register({ + name: 'helper-agent', + namespace: 'farm', + type: 'agent', + filePath: '/farm/helper-agent/AGENT.md', + source: 'plugin:farm', + description: 'Helper agent', + metadata: {}, + }); + + // Request skill - should find helper-skill + const skillResult = resolver.resolve('helper-skill', { callerNamespace: 'farm' }, 'skill'); + assert.strictEqual(skillResult.resolution, 'local-first'); + if (skillResult.resolution !== 'local-first') throw new Error('Type guard'); + assert.strictEqual(skillResult.component.type, 'skill'); + assert.strictEqual(skillResult.component.name, 'helper-skill'); + + // Request agent - should find helper-agent + const agentResult = resolver.resolve('helper-agent', { callerNamespace: 'farm' }, 'agent'); + assert.strictEqual(agentResult.resolution, 'local-first'); + if (agentResult.resolution !== 'local-first') throw new Error('Type guard'); + assert.strictEqual(agentResult.component.type, 'agent'); + assert.strictEqual(agentResult.component.name, 'helper-agent'); + }); + }); + + describe('shorthand resolution (R009)', () => { + it('should resolve unambiguous shorthand with single match', () => { + registry.register({ + name: 'feed-chickens', + namespace: 'farm', + type: 'skill', + filePath: '/farm/feed-chickens/SKILL.md', + source: 'plugin:farm', + description: 'Feed chickens', + metadata: {}, + }); + + const result = resolver.resolve('feed-chickens'); + + assert.strictEqual(result.resolution, 'shorthand'); + if (result.resolution !== 'shorthand') throw new Error('Type guard'); + + assert.strictEqual(result.requestedName, 'feed-chickens'); + assert.strictEqual(result.component.canonicalName, 'farm:feed-chickens'); + }); + + it('should return ambiguous with candidates for multiple matches', () => { + registry.register({ + name: 'call-horse', + namespace: 'farm', + type: 'skill', + filePath: '/farm/call-horse/SKILL.md', + source: 'plugin:farm', + description: 'Farm horse caller', + metadata: {}, + }); + registry.register({ + name: 'call-horse', + namespace: 'zoo', + type: 'skill', + filePath: '/zoo/call-horse/SKILL.md', + source: 'plugin:zoo', + description: 'Zoo horse caller', + metadata: {}, + }); + + const result = resolver.resolve('call-horse'); + + assert.strictEqual(result.resolution, 'ambiguous'); + if (result.resolution !== 'ambiguous') throw new Error('Type guard'); + + assert.strictEqual(result.requestedName, 'call-horse'); + assert.strictEqual(result.candidates.length, 2); + + const canonicalNames = result.candidates.map((c) => c.canonicalName).sort(); + assert.deepStrictEqual(canonicalNames, ['farm:call-horse', 'zoo:call-horse']); + }); + + it('should return not-found for non-existent bare name', () => { + const result = resolver.resolve('nonexistent'); + assert.strictEqual(result.resolution, 'not-found'); + }); + + it('should return not-found when type filter eliminates all matches', () => { + registry.register({ + name: 'helper', + namespace: 'farm', + type: 'skill', + filePath: '/farm/helper/SKILL.md', + source: 'plugin:farm', + description: 'Helper skill', + metadata: {}, + }); + + const result = resolver.resolve('helper', undefined, 'agent'); + assert.strictEqual(result.resolution, 'not-found'); + }); + }); + + describe('flat component compatibility', () => { + it('should resolve flat component by bare name (no namespace)', () => { + registry.register({ + name: 'code-review', + namespace: undefined, + type: 'skill', + filePath: '/skills/code-review/SKILL.md', + source: 'user', + description: 'Code review skill', + metadata: {}, + }); + + const result = resolver.resolve('code-review'); + + assert.strictEqual(result.resolution, 'shorthand'); + if (result.resolution !== 'shorthand') throw new Error('Type guard'); + + assert.strictEqual(result.component.canonicalName, 'code-review'); + assert.strictEqual(result.component.namespace, undefined); + }); + + it('should include flat component in ambiguous candidates', () => { + registry.register({ + name: 'helper', + namespace: undefined, + type: 'skill', + filePath: '/skills/helper/SKILL.md', + source: 'user', + description: 'User helper', + metadata: {}, + }); + registry.register({ + name: 'helper', + namespace: 'farm', + type: 'skill', + filePath: '/farm/helper/SKILL.md', + source: 'plugin:farm', + description: 'Farm helper', + metadata: {}, + }); + + const result = resolver.resolve('helper'); + + assert.strictEqual(result.resolution, 'ambiguous'); + if (result.resolution !== 'ambiguous') throw new Error('Type guard'); + + assert.strictEqual(result.candidates.length, 2); + const canonicalNames = result.candidates.map((c) => c.canonicalName).sort(); + assert.deepStrictEqual(canonicalNames, ['farm:helper', 'helper']); + }); + }); + + describe('type filtering', () => { + it('should filter by skill type across namespaces', () => { + // Register skill in one namespace + registry.register({ + name: 'review', + namespace: 'tools', + type: 'skill', + filePath: '/tools/review/SKILL.md', + source: 'plugin:tools', + description: 'Review skill', + metadata: {}, + }); + // Register agent in another namespace (different canonical name) + registry.register({ + name: 'review', + namespace: 'agents', + type: 'agent', + filePath: '/agents/review/AGENT.md', + source: 'plugin:agents', + description: 'Review agent', + metadata: {}, + }); + + // Both have same bare name, filtering by type disambiguates + const skillResult = resolver.resolve('review', undefined, 'skill'); + assert.strictEqual(skillResult.resolution, 'shorthand'); + if (skillResult.resolution !== 'shorthand') throw new Error('Type guard'); + assert.strictEqual(skillResult.component.type, 'skill'); + assert.strictEqual(skillResult.component.namespace, 'tools'); + + const agentResult = resolver.resolve('review', undefined, 'agent'); + assert.strictEqual(agentResult.resolution, 'shorthand'); + if (agentResult.resolution !== 'shorthand') throw new Error('Type guard'); + assert.strictEqual(agentResult.component.type, 'agent'); + assert.strictEqual(agentResult.component.namespace, 'agents'); + }); + + it('should resolve unique skill among multiple agents with same name', () => { + registry.register({ + name: 'assistant', + namespace: 'tools', + type: 'skill', + filePath: '/tools/assistant/SKILL.md', + source: 'plugin:tools', + description: 'Assistant skill', + metadata: {}, + }); + registry.register({ + name: 'assistant', + namespace: 'other', + type: 'agent', + filePath: '/other/assistant/AGENT.md', + source: 'plugin:other', + description: 'Assistant agent', + metadata: {}, + }); + + const result = resolver.resolve('assistant', undefined, 'skill'); + assert.strictEqual(result.resolution, 'shorthand'); + if (result.resolution !== 'shorthand') throw new Error('Type guard'); + assert.strictEqual(result.component.canonicalName, 'tools:assistant'); + }); + }); + + describe('resolution path diagnostics', () => { + it('should include requestedName in all result types', () => { + registry.register({ + name: 'skill', + namespace: 'ns', + type: 'skill', + filePath: '/skill/SKILL.md', + source: 'test', + description: undefined, + metadata: {}, + }); + + const canon = resolver.resolve('ns:skill'); + assert.strictEqual(canon.requestedName, 'ns:skill'); + + const local = resolver.resolve('skill', { callerNamespace: 'ns' }); + assert.strictEqual(local.requestedName, 'skill'); + + const short = resolver.resolve('skill'); + assert.strictEqual(short.requestedName, 'skill'); + + const notFound = resolver.resolve('missing'); + assert.strictEqual(notFound.requestedName, 'missing'); + }); + + it('should provide matchedNamespace in local-first results', () => { + registry.register({ + name: 'skill', + namespace: 'my-ns', + type: 'skill', + filePath: '/skill/SKILL.md', + source: 'test', + description: undefined, + metadata: {}, + }); + + const result = resolver.resolve('skill', { callerNamespace: 'my-ns' }); + assert.strictEqual(result.resolution, 'local-first'); + + if (result.resolution === 'local-first') { + assert.strictEqual(result.matchedNamespace, 'my-ns'); + } + }); + + it('should provide full candidate list in ambiguous results', () => { + registry.register({ + name: 'dup', + namespace: 'a', + type: 'skill', + filePath: '/a/dup/SKILL.md', + source: 'a', + description: 'A dup', + metadata: {}, + }); + registry.register({ + name: 'dup', + namespace: 'b', + type: 'skill', + filePath: '/b/dup/SKILL.md', + source: 'b', + description: 'B dup', + metadata: {}, + }); + + const result = resolver.resolve('dup'); + assert.strictEqual(result.resolution, 'ambiguous'); + + if (result.resolution === 'ambiguous') { + assert.strictEqual(result.candidates.length, 2); + for (const candidate of result.candidates) { + assert.ok(candidate.canonicalName); + assert.ok(candidate.filePath); + assert.strictEqual(candidate.name, 'dup'); + } + } + }); + }); + + describe('edge cases', () => { + it('should handle empty registry gracefully', () => { + const result = resolver.resolve('anything'); + assert.strictEqual(result.resolution, 'not-found'); + }); + + it('should handle empty caller namespace string', () => { + registry.register({ + name: 'skill', + namespace: 'ns', + type: 'skill', + filePath: '/skill/SKILL.md', + source: 'test', + description: undefined, + metadata: {}, + }); + + // Empty string is falsy, should fall through to shorthand + const result = resolver.resolve('skill', { callerNamespace: '' }); + assert.strictEqual(result.resolution, 'shorthand'); + }); + }); + + describe('alias resolution', () => { + it('should resolve alias with alias result type', () => { + registry.register({ + name: '3d-visualizer', + namespace: 'python-tools', + type: 'skill', + filePath: '/python-tools/3d-visualizer/SKILL.md', + source: 'plugin:python-tools', + description: '3D visualization', + metadata: {}, + }); + registry.registerAlias('py3d', 'python-tools:3d-visualizer'); + + const result = resolver.resolve('py3d'); + + assert.strictEqual(result.resolution, 'alias'); + if (result.resolution !== 'alias') throw new Error('Type guard'); + + assert.strictEqual(result.requestedName, 'py3d'); + assert.strictEqual(result.alias, 'py3d'); + assert.strictEqual(result.canonicalName, 'python-tools:3d-visualizer'); + assert.strictEqual(result.component.canonicalName, 'python-tools:3d-visualizer'); + assert.strictEqual(result.component.type, 'skill'); + }); + + it('should respect type filter in alias resolution', () => { + registry.register({ + name: 'visualizer', + namespace: 'tools', + type: 'skill', + filePath: '/tools/visualizer/SKILL.md', + source: 'plugin:tools', + description: 'Visualizer skill', + metadata: {}, + }); + registry.registerAlias('viz', 'tools:visualizer'); + + // Type filter matches - should resolve + const skillResult = resolver.resolve('viz', undefined, 'skill'); + assert.strictEqual(skillResult.resolution, 'alias'); + if (skillResult.resolution !== 'alias') throw new Error('Type guard'); + assert.strictEqual(skillResult.component.type, 'skill'); + + // Type filter doesn't match - should not resolve alias + const agentResult = resolver.resolve('viz', undefined, 'agent'); + assert.strictEqual(agentResult.resolution, 'not-found'); + }); + + it('should prioritize alias over shorthand (alias checked first)', () => { + // Register a component that could match as shorthand + registry.register({ + name: 'shortcut', + namespace: 'other-plugin', + type: 'skill', + filePath: '/other/shortcut/SKILL.md', + source: 'plugin:other-plugin', + description: 'Other shortcut', + metadata: {}, + }); + + // Register a different component with an alias using the same bare name + registry.register({ + name: 'aliased-skill', + namespace: 'main-plugin', + type: 'skill', + filePath: '/main/aliased-skill/SKILL.md', + source: 'plugin:main-plugin', + description: 'Main skill', + metadata: {}, + }); + registry.registerAlias('shortcut', 'main-plugin:aliased-skill'); + + // 'shortcut' should resolve via alias, not shorthand + const result = resolver.resolve('shortcut'); + + assert.strictEqual(result.resolution, 'alias'); + if (result.resolution !== 'alias') throw new Error('Type guard'); + + // Should point to the aliased target, not the shorthand match + assert.strictEqual(result.canonicalName, 'main-plugin:aliased-skill'); + }); + + it('should prioritize alias over local-first (alias checked first)', () => { + // Register components in two namespaces + registry.register({ + name: 'helper', + namespace: 'local-ns', + type: 'skill', + filePath: '/local-ns/helper/SKILL.md', + source: 'plugin:local-ns', + description: 'Local helper', + metadata: {}, + }); + registry.register({ + name: 'aliased-helper', + namespace: 'alias-ns', + type: 'skill', + filePath: '/alias-ns/aliased-helper/SKILL.md', + source: 'plugin:alias-ns', + description: 'Aliased helper', + metadata: {}, + }); + + // Create alias that shadows local namespace name + registry.registerAlias('helper', 'alias-ns:aliased-helper'); + + // Even with callerNamespace='local-ns', alias should win + const result = resolver.resolve('helper', { callerNamespace: 'local-ns' }); + + assert.strictEqual(result.resolution, 'alias'); + if (result.resolution !== 'alias') throw new Error('Type guard'); + assert.strictEqual(result.canonicalName, 'alias-ns:aliased-helper'); + }); + + it('should include alias and canonicalName in result', () => { + registry.register({ + name: 'code-review', + namespace: 'tools', + type: 'agent', + filePath: '/tools/code-review/AGENT.md', + source: 'plugin:tools', + description: 'Code review agent', + metadata: {}, + }); + registry.registerAlias('review', 'tools:code-review'); + + const result = resolver.resolve('review'); + + assert.strictEqual(result.resolution, 'alias'); + if (result.resolution !== 'alias') throw new Error('Type guard'); + + // Both alias and canonicalName should be present + assert.strictEqual(result.alias, 'review'); + assert.strictEqual(result.canonicalName, 'tools:code-review'); + assert.strictEqual(result.component.canonicalName, 'tools:code-review'); + }); + + it('should fall through to local-first/shorthand when alias does not exist', () => { + registry.register({ + name: 'existing', + namespace: 'ns', + type: 'skill', + filePath: '/ns/existing/SKILL.md', + source: 'plugin:ns', + description: 'Existing skill', + metadata: {}, + }); + + // No alias registered, should fall through to local-first + const result = resolver.resolve('existing', { callerNamespace: 'ns' }); + + assert.strictEqual(result.resolution, 'local-first'); + if (result.resolution !== 'local-first') throw new Error('Type guard'); + assert.strictEqual(result.component.canonicalName, 'ns:existing'); + }); + + it('should fall through to shorthand when alias does not exist and no local match', () => { + registry.register({ + name: 'unique', + namespace: 'plugin-a', + type: 'skill', + filePath: '/plugin-a/unique/SKILL.md', + source: 'plugin:plugin-a', + description: 'Unique skill', + metadata: {}, + }); + + // No alias registered, no local match, should fall through to shorthand + const result = resolver.resolve('unique', { callerNamespace: 'other-ns' }); + + assert.strictEqual(result.resolution, 'shorthand'); + if (result.resolution !== 'shorthand') throw new Error('Type guard'); + assert.strictEqual(result.component.canonicalName, 'plugin-a:unique'); + }); + }); +}); diff --git a/src/resources/extensions/gsd/tests/plugin-importer-live.test.ts b/src/resources/extensions/gsd/tests/plugin-importer-live.test.ts new file mode 100644 index 000000000..6971a6209 --- /dev/null +++ b/src/resources/extensions/gsd/tests/plugin-importer-live.test.ts @@ -0,0 +1,481 @@ +/** + * Live E2E Tests Against Real Marketplace Repos + * + * Tests R014: validates PluginImporter against real marketplace data. + * + * Source model alignment: + * - Prefer Claude Code managed marketplace locations when available + * - Fall back to cloned fixture repos for portability + * - Never require a contributor's personal sibling repo layout + */ + +import { describe, it, before, after } from 'node:test'; +import assert from 'node:assert'; +import { PluginImporter, type DiscoveryResult, type ImportManifest } from '../plugin-importer.js'; +import { getMarketplaceFixtures } from './marketplace-test-fixtures.js'; + +// ============================================================================ +// Live Test Configuration +// ============================================================================ + +/** + * Canonical name format regex: namespace:name or bare name + * Allows alphanumeric, underscore, hyphen, and dot in names. + * Real marketplace data has names like "ecosystem-researcher-v1.1-rt-ica". + */ +const CANONICAL_NAME_REGEX = /^[a-zA-Z0-9_.-]+(?::[a-zA-Z0-9_.-]+)?$/; + +// ============================================================================ +// Live E2E Tests +// ============================================================================ + +const fixtureSetup = getMarketplaceFixtures(import.meta.dirname); +const fixtures = fixtureSetup.fixtures; +const CLAUDE_SKILLS_PATH = fixtures?.claudeSkillsPath; +const CLAUDE_PLUGINS_OFFICIAL_PATH = fixtures?.claudePluginsOfficialPath; + +// Log marketplace status for observability +console.log('Live E2E Test Configuration:'); +console.log(` source: ${fixtures?.source ?? 'unavailable'}`); +if (CLAUDE_SKILLS_PATH) { + console.log(` claude_skills: FOUND at ${CLAUDE_SKILLS_PATH}`); +} +if (CLAUDE_PLUGINS_OFFICIAL_PATH) { + console.log(` claude-plugins-official: FOUND at ${CLAUDE_PLUGINS_OFFICIAL_PATH}`); +} +if (!fixtureSetup.available) { + console.log(` unavailable: ${fixtureSetup.skipReason}`); +} + +const skipReason = !fixtureSetup.available ? fixtureSetup.skipReason : undefined; + +describe( + 'Live E2E Tests', + { skip: skipReason }, + () => { + let importer: PluginImporter; + let discoveryResult: DiscoveryResult; + + before(() => { + importer = new PluginImporter(); + }); + + after(() => { + fixtures?.cleanup(); + }); + + describe('Step 2: discover() against real marketplaces', () => { + it('should discover plugins from both marketplaces with no fatal errors', () => { + // Stage 1: Discover + discoveryResult = importer.discover([ + CLAUDE_SKILLS_PATH!, + CLAUDE_PLUGINS_OFFICIAL_PATH!, + ]); + + // Log discovery summary for observability + console.log('\nDiscovery Summary:'); + console.log(` Marketplaces processed: ${discoveryResult.summary.marketplacesProcessed}`); + console.log(` Total plugins: ${discoveryResult.summary.totalPlugins}`); + console.log(` Total components: ${discoveryResult.summary.totalComponents}`); + console.log(` Marketplaces with errors: ${discoveryResult.summary.marketplacesWithErrors}`); + + // Assert positive counts + assert.ok( + discoveryResult.summary.totalPlugins > 0, + 'Should find at least one plugin across both marketplaces' + ); + + assert.ok( + discoveryResult.summary.totalComponents > 0, + 'Should discover at least one component across both marketplaces' + ); + + // No fatal errors should crash the pipeline + assert.strictEqual( + discoveryResult.summary.marketplacesProcessed, + 2, + 'Should process both marketplace paths' + ); + }); + + it('should have processed both marketplace.json files', () => { + assert.ok(discoveryResult, 'Discovery must run first'); + + // Both marketplaces should have been attempted + assert.strictEqual( + discoveryResult.marketplaceResults.length, + 2, + 'Should have results for both marketplaces' + ); + + // At least one should have succeeded (they're real repos) + const successfulMarketplaces = discoveryResult.marketplaceResults.filter( + (m) => m.status === 'ok' + ); + + assert.ok( + successfulMarketplaces.length >= 1, + 'At least one marketplace should have loaded successfully' + ); + }); + }); + + describe('Step 3: canonical name format validation', () => { + it('should have valid canonical names matching namespace:component format', () => { + assert.ok(discoveryResult, 'Discovery must run first'); + + const registry = importer.getRegistry(); + assert.ok(registry, 'Registry should be populated'); + + const allComponents = registry.getAll(); + + // Should have components from real plugins + assert.ok( + allComponents.length > 0, + 'Should have discovered components to validate' + ); + + // Log sample canonical names for observability + const sampleNames = allComponents.slice(0, 5).map((c) => c.canonicalName); + console.log('\nSample canonical names from discovered components:'); + sampleNames.forEach((name) => console.log(` - ${name}`)); + + // Validate each canonical name + for (const component of allComponents) { + assert.ok( + CANONICAL_NAME_REGEX.test(component.canonicalName), + `Canonical name "${component.canonicalName}" should match format "namespace:name" or bare "name"` + ); + + // Namespaced components should have colon in canonical name + if (component.namespace) { + assert.ok( + component.canonicalName.includes(':'), + `Namespaced component "${component.canonicalName}" should contain colon` + ); + + // Canonical should be namespace:name + const expected = `${component.namespace}:${component.name}`; + assert.strictEqual( + component.canonicalName, + expected, + `Canonical name should equal namespace:name` + ); + } else { + // Flat components should NOT have colon + assert.ok( + !component.canonicalName.includes(':'), + `Flat component "${component.canonicalName}" should not contain colon` + ); + + assert.strictEqual( + component.canonicalName, + component.name, + `Flat component canonical should equal bare name` + ); + } + } + }); + }); + + describe('Step 4: selectComponents() filtering', () => { + it('should filter components by type and return non-empty results', () => { + assert.ok(discoveryResult, 'Discovery must run first'); + + // Filter by skills + const skills = importer.selectComponents((c) => c.type === 'skill'); + + // Filter by agents + const agents = importer.selectComponents((c) => c.type === 'agent'); + + console.log('\nComponent type counts:'); + console.log(` Skills: ${skills.length}`); + console.log(` Agents: ${agents.length}`); + + // At least one type should have components (real marketplaces have plugins) + assert.ok( + skills.length > 0 || agents.length > 0, + 'At least one component type should have results from real marketplaces' + ); + }); + + it('should filter by namespace correctly', () => { + assert.ok(discoveryResult, 'Discovery must run first'); + + const registry = importer.getRegistry(); + const allComponents = registry!.getAll(); + + // Get unique namespaces + const namespaces = new Set( + allComponents.map((c) => c.namespace).filter((n): n is string => n !== undefined) + ); + + console.log('\nDiscovered namespaces:'); + namespaces.forEach((ns) => console.log(` - ${ns}`)); + + if (namespaces.size > 0) { + // Pick a namespace and filter + const testNamespace = Array.from(namespaces)[0]!; + const filtered = importer.selectComponents( + (c) => c.namespace === testNamespace + ); + + assert.ok( + filtered.length > 0, + `Should find components for namespace "${testNamespace}"` + ); + + // All results should match the filter + for (const comp of filtered) { + assert.strictEqual( + comp.namespace, + testNamespace, + 'Filtered components should have correct namespace' + ); + } + } + }); + }); + + describe('Step 5: validateImport() on real data', () => { + it('should run validation on all discovered components without crash', () => { + assert.ok(discoveryResult, 'Discovery must run first'); + + const registry = importer.getRegistry(); + const allComponents = registry!.getAll(); + + // Run validation on all discovered components + const validation = importer.validateImport(allComponents); + + console.log('\nValidation result:'); + console.log(` Can proceed: ${validation.canProceed}`); + console.log(` Total diagnostics: ${validation.summary.total}`); + console.log(` Errors: ${validation.summary.errors}`); + console.log(` Warnings: ${validation.summary.warnings}`); + + if (validation.diagnostics.length > 0) { + console.log('\nDiagnostics:'); + validation.diagnostics.forEach((d) => { + console.log(` [${d.severity}] ${d.class}: ${d.remediation}`); + }); + } + + // Validation should complete without throwing + assert.ok(validation, 'Validation should return a result'); + assert.ok( + typeof validation.canProceed === 'boolean', + 'canProceed should be boolean' + ); + assert.ok( + Array.isArray(validation.diagnostics), + 'diagnostics should be an array' + ); + }); + + it('should have valid diagnostic structure if warnings exist', () => { + const validation = importer.getLastValidation(); + assert.ok(validation, 'Validation should have run'); + + for (const diag of validation.diagnostics) { + // Verify diagnostic structure + assert.ok(diag.class, 'Diagnostic should have class'); + assert.ok( + ['error', 'warning'].includes(diag.severity), + 'Diagnostic severity should be error or warning' + ); + assert.ok(diag.remediation, 'Diagnostic should have remediation'); + assert.ok( + Array.isArray(diag.involvedCanonicalNames), + 'Diagnostic should have involvedCanonicalNames array' + ); + assert.ok( + Array.isArray(diag.filePaths), + 'Diagnostic should have filePaths array' + ); + } + }); + + it('should not have error-severity diagnostics blocking on real data (data quality check)', () => { + const validation = importer.getLastValidation(); + assert.ok(validation, 'Validation should have run'); + + // Real marketplace data should not have fatal canonical collisions + // (this is a data quality assertion) + if (validation.summary.errors > 0) { + console.log('\nWARNING: Real marketplace data has error-severity diagnostics!'); + console.log('This may indicate duplicate canonical names in the marketplace.'); + + // Log the errors for investigation + validation.diagnostics + .filter((d) => d.severity === 'error') + .forEach((d) => { + console.log(` ERROR: ${d.class}`); + console.log(` Involved: ${d.involvedCanonicalNames.join(', ')}`); + console.log(` Files: ${d.filePaths.join(', ')}`); + }); + } + + // Note: We allow errors in assertion but log them for visibility + // Real data might have collisions, but the pipeline should handle them + assert.strictEqual(typeof validation.canProceed, 'boolean'); + }); + }); + + describe('Step 6: getImportManifest() with canonical names', () => { + it('should generate manifest preserving canonical names from real plugins', () => { + assert.ok(discoveryResult, 'Discovery must run first'); + + const registry = importer.getRegistry(); + const allComponents = registry!.getAll(); + + // Generate manifest for all components + const manifest = importer.getImportManifest(allComponents); + + console.log('\nManifest summary:'); + console.log(` Schema version: ${manifest.schemaVersion}`); + console.log(` Total entries: ${manifest.summary.total}`); + console.log(` Skills: ${manifest.summary.skills}`); + console.log(` Agents: ${manifest.summary.agents}`); + console.log(` Namespaces: ${manifest.summary.namespaces.length}`); + + // Verify manifest structure + assert.strictEqual(manifest.schemaVersion, '1.0'); + assert.strictEqual( + manifest.entries.length, + allComponents.length, + 'Manifest should have entry for each component' + ); + + // Verify canonical names preserved + for (const entry of manifest.entries) { + // Find matching component + const component = allComponents.find( + (c) => c.canonicalName === entry.canonicalName + ); + + assert.ok( + component, + `Manifest entry should match component: ${entry.canonicalName}` + ); + + // Canonical name should match exactly + assert.strictEqual( + entry.canonicalName, + component.canonicalName, + 'Canonical name should be preserved in manifest' + ); + + // Type should match + assert.strictEqual(entry.type, component.type); + + // Namespace should match + assert.strictEqual(entry.namespace, component.namespace); + + // Name should match + assert.strictEqual(entry.name, component.name); + + // File path should be preserved + assert.strictEqual(entry.filePath, component.filePath); + } + }); + + it('should produce JSON-serializable manifest', () => { + const registry = importer.getRegistry(); + const allComponents = registry!.getAll(); + + const manifest = importer.getImportManifest(allComponents); + + // Should be JSON serializable + const json = JSON.stringify(manifest, null, 2); + + // Should parse back correctly + const parsed: ImportManifest = JSON.parse(json); + + assert.strictEqual(parsed.schemaVersion, manifest.schemaVersion); + assert.strictEqual(parsed.entries.length, manifest.entries.length); + + // Sample entries should match after round-trip + const sampleEntry = parsed.entries[0]; + if (sampleEntry) { + const original = manifest.entries[0]!; + assert.strictEqual(sampleEntry.canonicalName, original.canonicalName); + assert.strictEqual(sampleEntry.type, original.type); + } + }); + + it('should have correct summary counts', () => { + const registry = importer.getRegistry(); + const allComponents = registry!.getAll(); + + const manifest = importer.getImportManifest(allComponents); + + // Count skills and agents + const skillCount = manifest.entries.filter((e) => e.type === 'skill').length; + const agentCount = manifest.entries.filter((e) => e.type === 'agent').length; + + assert.strictEqual( + manifest.summary.skills, + skillCount, + 'Skill count should match entries' + ); + + assert.strictEqual( + manifest.summary.agents, + agentCount, + 'Agent count should match entries' + ); + + assert.strictEqual( + manifest.summary.total, + manifest.entries.length, + 'Total should match entry count' + ); + + // Namespaces should be unique and sorted + const uniqueNamespaces = new Set( + manifest.entries + .map((e) => e.namespace) + .filter((n): n is string => n !== undefined) + ); + + assert.deepStrictEqual( + manifest.summary.namespaces, + Array.from(uniqueNamespaces).sort(), + 'Namespaces should be unique and sorted' + ); + }); + }); + + describe('Full pipeline verification', () => { + it('should execute discover → select → validate → manifest without error', () => { + // This test verifies the full pipeline works end-to-end + + // Already have discovery from before() + assert.ok(discoveryResult, 'Discovery should have completed'); + + // Select subset + const skills = importer.selectComponents((c) => c.type === 'skill'); + + // Validate + const validation = importer.validateImport(skills); + assert.ok(validation, 'Validation should complete'); + + // Generate manifest + const manifest = importer.getImportManifest(skills); + assert.ok(manifest, 'Manifest generation should complete'); + + // All skills should be in manifest + assert.strictEqual( + manifest.summary.skills, + skills.length, + 'All selected skills should be in manifest' + ); + + console.log('\nFull pipeline verification:'); + console.log(` Selected: ${skills.length} skills`); + console.log(` Validated: canProceed=${validation.canProceed}`); + console.log(` Manifest: ${manifest.summary.total} entries`); + }); + }); + } +); diff --git a/src/resources/extensions/gsd/tests/plugin-importer.test.ts b/src/resources/extensions/gsd/tests/plugin-importer.test.ts new file mode 100644 index 000000000..3d0bddcca --- /dev/null +++ b/src/resources/extensions/gsd/tests/plugin-importer.test.ts @@ -0,0 +1,1383 @@ +/** + * PluginImporter Contract Tests + * + * Tests that prove R012 (discover/select/import flow) and R013 (canonical name preservation). + * + * Coverage: + * - Discovery pipeline: marketplace discovery → registry population + * - Selective filtering: filter function correctly selects components + * - Diagnostic gating: errors block, warnings pass + * - Config manifest format: canonical identity preserved + */ + +import { describe, it, beforeEach, mock } from 'node:test'; +import assert from 'node:assert'; +import { + PluginImporter, + type DiscoveryResult, + type ValidationResult, + type ImportManifest, +} from '../plugin-importer.js'; +import type { NamespacedComponent } from '../namespaced-registry.js'; +import type { + MarketplaceDiscoveryResult, + DiscoveredPlugin, +} from '../marketplace-discovery.js'; + +// ============================================================================ +// Test Fixtures +// ============================================================================ + +/** + * Create a mock discovered plugin for testing. + */ +function createMockPlugin(overrides: Partial = {}): DiscoveredPlugin { + return { + name: 'test-plugin', + canonicalName: 'test-plugin', + source: './plugins/test-plugin', + resolvedPath: '/plugins/test-plugin', + status: 'ok', + manifestSource: 'plugin.json', + description: 'A test plugin', + version: '1.0.0', + author: { name: 'Test Author' }, + inventory: { + skills: ['skill-a', 'skill-b'], + agents: ['agent-x'], + commands: [], + mcpServers: {}, + lspServers: {}, + hooks: [], + }, + ...overrides, + }; +} + +/** + * Create a mock marketplace discovery result. + */ +function createMockDiscoveryResult( + plugins: DiscoveredPlugin[] = [], + overrides: Partial = {} +): MarketplaceDiscoveryResult { + return { + status: 'ok', + marketplacePath: '/test/marketplace.json', + marketplaceName: 'Test Marketplace', + pluginFormat: 'jamie-style', + plugins, + summary: { + total: plugins.length, + ok: plugins.filter((p) => p.status === 'ok').length, + error: plugins.filter((p) => p.status === 'error').length, + }, + ...overrides, + }; +} + +// ============================================================================ +// Tests +// ============================================================================ + +describe('PluginImporter', () => { + let importer: PluginImporter; + + beforeEach(() => { + importer = new PluginImporter(); + }); + + describe('Stage 1: discover()', () => { + it('should throw error if paths array is empty but return valid result', () => { + const result = importer.discover([]); + + assert.strictEqual(result.summary.marketplacesProcessed, 0); + assert.strictEqual(result.summary.totalPlugins, 0); + assert.strictEqual(result.summary.totalComponents, 0); + }); + + it('should call discoverMarketplace for each path and aggregate results', () => { + // Test with non-existent paths - should still return structure + const result = importer.discover([ + '/nonexistent/marketplace-1', + '/nonexistent/marketplace-2', + ]); + + assert.strictEqual(result.summary.marketplacesProcessed, 2); + assert.strictEqual(Array.isArray(result.marketplaceResults), true); + assert.strictEqual(result.marketplaceResults.length, 2); + }); + + it('should populate registry via componentsFromDiscovery', () => { + // Test against a real path if it exists, otherwise test structure + const result = importer.discover([]); + + // Registry should be populated (even if empty) + const registry = importer.getRegistry(); + assert.ok(registry !== null); + assert.strictEqual(registry!.size, result.summary.totalComponents); + }); + + it('should track plugins with errors in summary', () => { + const result = importer.discover(['/nonexistent/path']); + + // Non-existent path should result in error status + assert.ok(result.summary.marketplacesWithErrors >= 0); + }); + + it('should be re-entrant (calling discover again resets state)', () => { + // First discovery + importer.discover(['/nonexistent/path-1']); + const firstPlugins = importer.getDiscoveredPlugins(); + + // Second discovery should reset + importer.discover(['/nonexistent/path-2']); + const secondPlugins = importer.getDiscoveredPlugins(); + + // Should have fresh state (not accumulated) + // Both should have 0 plugins since paths don't exist + assert.strictEqual(firstPlugins.length, 0); + assert.strictEqual(secondPlugins.length, 0); + }); + }); + + describe('Stage 2: selectComponents()', () => { + it('should throw error if called before discover()', () => { + assert.throws( + () => importer.selectComponents(() => true), + /Must call discover\(\) before selectComponents\(\)/ + ); + }); + + it('should return empty array if no components match filter', () => { + importer.discover([]); + const selected = importer.selectComponents(() => false); + assert.deepStrictEqual(selected, []); + }); + + it('should return all components if filter returns true', () => { + importer.discover([]); + const selected = importer.selectComponents(() => true); + // Empty discovery means no components + assert.deepStrictEqual(selected, []); + }); + + it('should filter by namespace correctly', () => { + importer.discover([]); + const selected = importer.selectComponents( + (c) => c.namespace === 'target-plugin' + ); + assert.deepStrictEqual(selected, []); + }); + + it('should filter by type correctly', () => { + importer.discover([]); + const skills = importer.selectComponents((c) => c.type === 'skill'); + const agents = importer.selectComponents((c) => c.type === 'agent'); + assert.deepStrictEqual(skills, []); + assert.deepStrictEqual(agents, []); + }); + + it('should filter by name pattern correctly', () => { + importer.discover([]); + const selected = importer.selectComponents((c) => + c.name.includes('review') + ); + assert.deepStrictEqual(selected, []); + }); + }); + + describe('Stage 3: validateImport()', () => { + it('should throw error if called before discover()', () => { + const components: NamespacedComponent[] = []; + assert.throws( + () => importer.validateImport(components), + /Must call discover\(\) before validateImport\(\)/ + ); + }); + + it('should return canProceed: true for empty selection', () => { + importer.discover([]); + const result = importer.validateImport([]); + + assert.strictEqual(result.canProceed, true); + assert.strictEqual(result.diagnostics.length, 0); + assert.strictEqual(result.summary.total, 0); + assert.strictEqual(result.summary.errors, 0); + assert.strictEqual(result.summary.warnings, 0); + }); + + it('should return canProceed: true when no collisions', () => { + importer.discover([]); + + // Create mock components without collisions + const components: NamespacedComponent[] = [ + { + name: 'skill-a', + namespace: 'plugin-x', + canonicalName: 'plugin-x:skill-a', + type: 'skill', + filePath: '/x/skill-a.md', + source: 'plugin:plugin-x', + description: undefined, + metadata: {}, + }, + { + name: 'skill-b', + namespace: 'plugin-y', + canonicalName: 'plugin-y:skill-b', + type: 'skill', + filePath: '/y/skill-b.md', + source: 'plugin:plugin-y', + description: undefined, + metadata: {}, + }, + ]; + + const result = importer.validateImport(components); + + assert.strictEqual(result.canProceed, true); + }); + + it('should detect canonical collision and return canProceed: false (error blocks)', () => { + importer.discover([]); + + // Create components with same canonical name (collision) + const components: NamespacedComponent[] = [ + { + name: 'skill-a', + namespace: 'plugin-x', + canonicalName: 'plugin-x:skill-a', + type: 'skill', + filePath: '/first/skill-a.md', + source: 'plugin:plugin-x', + description: undefined, + metadata: {}, + }, + { + name: 'skill-a', + namespace: 'plugin-x', + canonicalName: 'plugin-x:skill-a', // Same canonical name! + type: 'skill', + filePath: '/second/skill-a.md', + source: 'plugin:plugin-x', + description: undefined, + metadata: {}, + }, + ]; + + const result = importer.validateImport(components); + + // Error severity should block + assert.strictEqual(result.canProceed, false); + assert.strictEqual(result.summary.errors, 1); + assert.ok(result.diagnostics.some((d) => d.severity === 'error')); + }); + + it('should detect shorthand overlap but return canProceed: true (warning passes)', () => { + importer.discover([]); + + // Create components with same bare name but different namespaces + const components: NamespacedComponent[] = [ + { + name: 'review', // Same bare name + namespace: 'plugin-a', + canonicalName: 'plugin-a:review', + type: 'skill', + filePath: '/a/review.md', + source: 'plugin:plugin-a', + description: undefined, + metadata: {}, + }, + { + name: 'review', // Same bare name + namespace: 'plugin-b', + canonicalName: 'plugin-b:review', + type: 'skill', + filePath: '/b/review.md', + source: 'plugin:plugin-b', + description: undefined, + metadata: {}, + }, + ]; + + const result = importer.validateImport(components); + + // Warning severity should NOT block + assert.strictEqual(result.canProceed, true); + assert.strictEqual(result.summary.errors, 0); + assert.strictEqual(result.summary.warnings, 1); + assert.ok(result.diagnostics.some((d) => d.severity === 'warning')); + }); + + it('should correctly classify severity: error for canonical conflict', () => { + importer.discover([]); + + const components: NamespacedComponent[] = [ + { + name: 'dup', + namespace: 'ns', + canonicalName: 'ns:dup', + type: 'skill', + filePath: '/1/dup.md', + source: 'first', + description: undefined, + metadata: {}, + }, + { + name: 'dup', + namespace: 'ns', + canonicalName: 'ns:dup', + type: 'skill', + filePath: '/2/dup.md', + source: 'second', + description: undefined, + metadata: {}, + }, + ]; + + const result = importer.validateImport(components); + + const error = result.diagnostics.find((d) => d.severity === 'error'); + assert.ok(error !== undefined); + assert.strictEqual(error!.class, 'canonical-conflict'); + assert.ok(error!.involvedCanonicalNames.includes('ns:dup')); + }); + + it('should correctly classify severity: warning for shorthand overlap', () => { + importer.discover([]); + + const components: NamespacedComponent[] = [ + { + name: 'common-skill', + namespace: 'plugin-a', + canonicalName: 'plugin-a:common-skill', + type: 'skill', + filePath: '/a/common.md', + source: 'plugin:plugin-a', + description: undefined, + metadata: {}, + }, + { + name: 'common-skill', + namespace: 'plugin-b', + canonicalName: 'plugin-b:common-skill', + type: 'skill', + filePath: '/b/common.md', + source: 'plugin:plugin-b', + description: undefined, + metadata: {}, + }, + ]; + + const result = importer.validateImport(components); + + const warning = result.diagnostics.find((d) => d.severity === 'warning'); + assert.ok(warning !== undefined); + assert.strictEqual(warning!.class, 'shorthand-overlap'); + assert.strictEqual(warning!.ambiguousBareName, 'common-skill'); + }); + }); + + describe('Stage 4: getImportManifest()', () => { + it('should produce valid manifest for empty selection', () => { + const manifest = importer.getImportManifest([]); + + assert.strictEqual(manifest.schemaVersion, '1.0'); + assert.strictEqual(typeof manifest.generatedAt, 'string'); + assert.deepStrictEqual(manifest.entries, []); + assert.strictEqual(manifest.summary.total, 0); + assert.strictEqual(manifest.summary.skills, 0); + assert.strictEqual(manifest.summary.agents, 0); + assert.deepStrictEqual(manifest.summary.namespaces, []); + }); + + it('should preserve canonical names in manifest (R013)', () => { + const components: NamespacedComponent[] = [ + { + name: 'code-review', + namespace: 'my-plugin', + canonicalName: 'my-plugin:code-review', + type: 'skill', + filePath: '/plugins/my-plugin/skills/code-review/SKILL.md', + source: 'plugin:my-plugin', + description: 'Reviews code', + metadata: { + pluginVersion: '1.0.0', + pluginAuthor: 'Test Author', + }, + }, + ]; + + const manifest = importer.getImportManifest(components); + + assert.strictEqual(manifest.entries.length, 1); + + // Verify canonical name preserved + const entry = manifest.entries[0]; + assert.strictEqual(entry!.canonicalName, 'my-plugin:code-review'); + assert.strictEqual(entry!.name, 'code-review'); + assert.strictEqual(entry!.namespace, 'my-plugin'); + }); + + it('should include all component metadata in manifest', () => { + const components: NamespacedComponent[] = [ + { + name: 'test-skill', + namespace: 'test-plugin', + canonicalName: 'test-plugin:test-skill', + type: 'skill', + filePath: '/test/skill.md', + source: 'plugin:test-plugin', + description: 'A test skill', + metadata: { + pluginVersion: '2.0.0', + pluginAuthor: 'Author Name', + pluginHomepage: 'https://example.com', + pluginCategory: 'testing', + }, + }, + ]; + + const manifest = importer.getImportManifest(components); + + const entry = manifest.entries[0]; + assert.ok(entry !== undefined); + assert.strictEqual(entry!.description, 'A test skill'); + assert.strictEqual(entry!.metadata.pluginVersion, '2.0.0'); + assert.strictEqual(entry!.metadata.pluginAuthor, 'Author Name'); + assert.strictEqual(entry!.metadata.pluginHomepage, 'https://example.com'); + assert.strictEqual(entry!.metadata.pluginCategory, 'testing'); + }); + + it('should count skills and agents separately in summary', () => { + const components: NamespacedComponent[] = [ + { + name: 'skill-a', + namespace: 'ns', + canonicalName: 'ns:skill-a', + type: 'skill', + filePath: '/a.md', + source: 'plugin:ns', + description: undefined, + metadata: {}, + }, + { + name: 'skill-b', + namespace: 'ns', + canonicalName: 'ns:skill-b', + type: 'skill', + filePath: '/b.md', + source: 'plugin:ns', + description: undefined, + metadata: {}, + }, + { + name: 'agent-x', + namespace: 'ns', + canonicalName: 'ns:agent-x', + type: 'agent', + filePath: '/x.md', + source: 'plugin:ns', + description: undefined, + metadata: {}, + }, + ]; + + const manifest = importer.getImportManifest(components); + + assert.strictEqual(manifest.summary.total, 3); + assert.strictEqual(manifest.summary.skills, 2); + assert.strictEqual(manifest.summary.agents, 1); + }); + + it('should list unique namespaces in summary', () => { + const components: NamespacedComponent[] = [ + { + name: 'skill', + namespace: 'plugin-a', + canonicalName: 'plugin-a:skill', + type: 'skill', + filePath: '/a.md', + source: 'plugin:plugin-a', + description: undefined, + metadata: {}, + }, + { + name: 'skill', + namespace: 'plugin-b', + canonicalName: 'plugin-b:skill', + type: 'skill', + filePath: '/b.md', + source: 'plugin:plugin-b', + description: undefined, + metadata: {}, + }, + { + name: 'skill', + namespace: 'plugin-a', // Duplicate namespace + canonicalName: 'plugin-a:skill-2', + type: 'skill', + filePath: '/a2.md', + source: 'plugin:plugin-a', + description: undefined, + metadata: {}, + }, + ]; + + const manifest = importer.getImportManifest(components); + + // Should have unique, sorted namespaces + assert.deepStrictEqual(manifest.summary.namespaces, ['plugin-a', 'plugin-b']); + }); + + it('should handle flat (non-namespaced) components', () => { + const components: NamespacedComponent[] = [ + { + name: 'flat-skill', + namespace: undefined, + canonicalName: 'flat-skill', + type: 'skill', + filePath: '/flat.md', + source: 'user', + description: undefined, + metadata: {}, + }, + ]; + + const manifest = importer.getImportManifest(components); + + assert.strictEqual(manifest.entries.length, 1); + assert.strictEqual(manifest.entries[0]!.namespace, undefined); + assert.strictEqual(manifest.entries[0]!.canonicalName, 'flat-skill'); + assert.deepStrictEqual(manifest.summary.namespaces, []); + }); + + it('should be serializable to JSON', () => { + const components: NamespacedComponent[] = [ + { + name: 'skill', + namespace: 'plugin', + canonicalName: 'plugin:skill', + type: 'skill', + filePath: '/skill.md', + source: 'plugin:plugin', + description: 'A skill', + metadata: { pluginVersion: '1.0.0' }, + }, + ]; + + const manifest = importer.getImportManifest(components); + + // Should be JSON serializable without errors + const json = JSON.stringify(manifest); + const parsed = JSON.parse(json); + + assert.strictEqual(parsed.schemaVersion, '1.0'); + assert.strictEqual(parsed.entries[0].canonicalName, 'plugin:skill'); + }); + }); + + describe('Full Pipeline: discover → select → validate → manifest', () => { + it('should execute full pipeline with mock components', () => { + // Stage 1: Discover (empty in this case) + const discovery = importer.discover([]); + assert.strictEqual(discovery.summary.totalComponents, 0); + + // Stage 2: Select all (empty) + const selected = importer.selectComponents(() => true); + assert.strictEqual(selected.length, 0); + + // Stage 3: Validate + const validation = importer.validateImport(selected); + assert.strictEqual(validation.canProceed, true); + + // Stage 4: Manifest + const manifest = importer.getImportManifest(selected); + assert.strictEqual(manifest.summary.total, 0); + }); + + it('should preserve canonical names through full pipeline (R013)', () => { + // Start with discovery + importer.discover([]); + + // Create mock components as if they were discovered + const components: NamespacedComponent[] = [ + { + name: 'code-review', + namespace: 'my-plugin', + canonicalName: 'my-plugin:code-review', + type: 'skill', + filePath: '/plugins/my-plugin/skills/code-review/SKILL.md', + source: 'plugin:my-plugin', + description: 'Reviews code', + metadata: { pluginVersion: '1.0.0' }, + }, + { + name: 'architect', + namespace: 'my-plugin', + canonicalName: 'my-plugin:architect', + type: 'agent', + filePath: '/plugins/my-plugin/agents/architect/AGENT.md', + source: 'plugin:my-plugin', + description: 'Designs architecture', + metadata: { pluginVersion: '1.0.0' }, + }, + ]; + + // Stage 3: Validate (no collisions) + const validation = importer.validateImport(components); + assert.strictEqual(validation.canProceed, true); + + // Stage 4: Manifest + const manifest = importer.getImportManifest(components); + + // Verify canonical names preserved + assert.strictEqual(manifest.entries.length, 2); + assert.strictEqual(manifest.entries[0]!.canonicalName, 'my-plugin:code-review'); + assert.strictEqual(manifest.entries[1]!.canonicalName, 'my-plugin:architect'); + + // Verify round-trip identity + const skill = manifest.entries.find((e) => e.type === 'skill'); + assert.ok(skill !== undefined); + assert.strictEqual(skill!.canonicalName, 'my-plugin:code-review'); + assert.strictEqual(skill!.name, 'code-review'); + assert.strictEqual(skill!.namespace, 'my-plugin'); + }); + + it('should block import on canonical collision', () => { + importer.discover([]); + + const components: NamespacedComponent[] = [ + { + name: 'skill', + namespace: 'ns', + canonicalName: 'ns:skill', + type: 'skill', + filePath: '/first.md', + source: 'first', + description: undefined, + metadata: {}, + }, + { + name: 'skill', + namespace: 'ns', + canonicalName: 'ns:skill', // Collision! + type: 'skill', + filePath: '/second.md', + source: 'second', + description: undefined, + metadata: {}, + }, + ]; + + const validation = importer.validateImport(components); + + // Should block + assert.strictEqual(validation.canProceed, false); + + // Diagnostic should explain why + assert.strictEqual(validation.summary.errors, 1); + assert.ok(validation.diagnostics[0]!.remediation.length > 0); + }); + + it('should allow import with warnings (shorthand overlap)', () => { + importer.discover([]); + + const components: NamespacedComponent[] = [ + { + name: 'review', + namespace: 'plugin-a', + canonicalName: 'plugin-a:review', + type: 'skill', + filePath: '/a.md', + source: 'plugin:plugin-a', + description: undefined, + metadata: {}, + }, + { + name: 'review', + namespace: 'plugin-b', + canonicalName: 'plugin-b:review', + type: 'skill', + filePath: '/b.md', + source: 'plugin:plugin-b', + description: undefined, + metadata: {}, + }, + ]; + + const validation = importer.validateImport(components); + + // Should NOT block (warning only) + assert.strictEqual(validation.canProceed, true); + assert.strictEqual(validation.summary.warnings, 1); + + // Manifest should still work + const manifest = importer.getImportManifest(components); + assert.strictEqual(manifest.entries.length, 2); + }); + }); + + describe('Inspection methods', () => { + it('should return null for getRegistry() before discover()', () => { + assert.strictEqual(importer.getRegistry(), null); + }); + + it('should return registry after discover()', () => { + importer.discover([]); + assert.ok(importer.getRegistry() !== null); + }); + + it('should return empty array for getDiscoveredPlugins() before discover()', () => { + const plugins = importer.getDiscoveredPlugins(); + assert.deepStrictEqual(plugins, []); + }); + + it('should return null for getLastValidation() before validateImport()', () => { + assert.strictEqual(importer.getLastValidation(), null); + }); + + it('should return last validation after validateImport()', () => { + importer.discover([]); + importer.validateImport([]); + assert.ok(importer.getLastValidation() !== null); + }); + + it('should return null for getLastDiscovery() before discover()', () => { + assert.strictEqual(importer.getLastDiscovery(), null); + }); + + it('should return last discovery after discover()', () => { + importer.discover([]); + assert.ok(importer.getLastDiscovery() !== null); + }); + }); + + describe('Diagnostic structure verification', () => { + it('should provide actionable remediation in diagnostics', () => { + importer.discover([]); + + const components: NamespacedComponent[] = [ + { + name: 'dup', + namespace: 'ns', + canonicalName: 'ns:dup', + type: 'skill', + filePath: '/first.md', + source: 'first', + description: undefined, + metadata: {}, + }, + { + name: 'dup', + namespace: 'ns', + canonicalName: 'ns:dup', + type: 'skill', + filePath: '/second.md', + source: 'second', + description: undefined, + metadata: {}, + }, + ]; + + const validation = importer.validateImport(components); + const diag = validation.diagnostics[0]; + + assert.ok(diag !== undefined); + assert.ok(diag!.remediation.length > 0); + assert.ok(diag!.remediation.includes('ns:dup')); + }); + + it('should include file paths in collision diagnostic', () => { + importer.discover([]); + + const components: NamespacedComponent[] = [ + { + name: 'dup', + namespace: 'ns', + canonicalName: 'ns:dup', + type: 'skill', + filePath: '/first/dup.md', + source: 'first', + description: undefined, + metadata: {}, + }, + { + name: 'dup', + namespace: 'ns', + canonicalName: 'ns:dup', + type: 'skill', + filePath: '/second/dup.md', + source: 'second', + description: undefined, + metadata: {}, + }, + ]; + + const validation = importer.validateImport(components); + const diag = validation.diagnostics[0]; + + assert.ok(diag!.filePaths.includes('/first/dup.md')); + assert.ok(diag!.filePaths.includes('/second/dup.md')); + }); + }); +}); + +describe('R012: Discover / select / import flow', () => { + it('should support staged discovery → selection → validation → manifest', () => { + const importer = new PluginImporter(); + + // Stage 1: Discover + const discovery = importer.discover([]); + assert.ok(discovery.registry !== undefined); + + // Stage 2: Select + const selected = importer.selectComponents(() => true); + assert.ok(Array.isArray(selected)); + + // Stage 3: Validate + const validation = importer.validateImport(selected); + assert.ok(typeof validation.canProceed === 'boolean'); + assert.ok(Array.isArray(validation.diagnostics)); + + // Stage 4: Manifest + const manifest = importer.getImportManifest(selected); + assert.ok(manifest.schemaVersion === '1.0'); + assert.ok(Array.isArray(manifest.entries)); + }); + + it('should allow independent testing of each stage', () => { + const importer = new PluginImporter(); + + // Each stage can be tested independently + importer.discover([]); + + // Selection can be called multiple times with different filters + const all = importer.selectComponents(() => true); + const skills = importer.selectComponents((c) => c.type === 'skill'); + const agents = importer.selectComponents((c) => c.type === 'agent'); + + // All should work without error + assert.ok(true); + + // Validation can be called with any component set + const validation1 = importer.validateImport(all); + const validation2 = importer.validateImport(skills); + const validation3 = importer.validateImport(agents); + + assert.ok(validation1.canProceed === true); + assert.ok(validation2.canProceed === true); + assert.ok(validation3.canProceed === true); + + // Manifest can be generated for any component set + const manifest1 = importer.getImportManifest(all); + const manifest2 = importer.getImportManifest(skills); + const manifest3 = importer.getImportManifest(agents); + + assert.ok(manifest1.schemaVersion === '1.0'); + assert.ok(manifest2.schemaVersion === '1.0'); + assert.ok(manifest3.schemaVersion === '1.0'); + }); +}); + +describe('R013: Canonical name preservation', () => { + it('should preserve plugin:component format in manifest entries', () => { + const importer = new PluginImporter(); + + const components: NamespacedComponent[] = [ + { + name: 'my-skill', + namespace: 'my-plugin', + canonicalName: 'my-plugin:my-skill', + type: 'skill', + filePath: '/skill.md', + source: 'plugin:my-plugin', + description: undefined, + metadata: {}, + }, + ]; + + const manifest = importer.getImportManifest(components); + + assert.strictEqual(manifest.entries[0]!.canonicalName, 'my-plugin:my-skill'); + }); + + it('should preserve flat names for non-namespaced components', () => { + const importer = new PluginImporter(); + + const components: NamespacedComponent[] = [ + { + name: 'flat-skill', + namespace: undefined, + canonicalName: 'flat-skill', + type: 'skill', + filePath: '/skill.md', + source: 'user', + description: undefined, + metadata: {}, + }, + ]; + + const manifest = importer.getImportManifest(components); + + assert.strictEqual(manifest.entries[0]!.canonicalName, 'flat-skill'); + assert.strictEqual(manifest.entries[0]!.namespace, undefined); + }); + + it('should support round-trip identity (name + namespace → canonical)', () => { + const importer = new PluginImporter(); + + const components: NamespacedComponent[] = [ + { + name: 'component', + namespace: 'namespace', + canonicalName: 'namespace:component', + type: 'skill', + filePath: '/path', + source: 'source', + description: undefined, + metadata: {}, + }, + ]; + + const manifest = importer.getImportManifest(components); + const entry = manifest.entries[0]!; + + // Round-trip: namespace:name should equal canonicalName + const reconstructed = entry.namespace + ? `${entry.namespace}:${entry.name}` + : entry.name; + + assert.strictEqual(reconstructed, entry.canonicalName); + assert.strictEqual(reconstructed, 'namespace:component'); + }); +}); + +// ============================================================================ +// T02: Command Flow Integration Tests +// ============================================================================ + +describe('T02: Command flow integration', () => { + describe('Marketplace detection', () => { + it('should categorize plugin roots into marketplaces vs flat paths', () => { + // Import the helper function (we'll need to export it for testing) + // For now, test the logic indirectly + const importer = new PluginImporter(); + + // Non-existent paths should still work + const result = importer.discover(['/nonexistent/marketplace']); + + // Should not crash and return valid structure + assert.ok(result.summary.marketplacesProcessed === 1); + }); + + it('should handle empty marketplace paths gracefully', () => { + const importer = new PluginImporter(); + + const result = importer.discover([]); + + assert.strictEqual(result.summary.marketplacesProcessed, 0); + assert.strictEqual(result.summary.totalPlugins, 0); + assert.strictEqual(result.summary.totalComponents, 0); + }); + }); + + describe('Component selection flow', () => { + it('should support filtering by plugin namespace', () => { + const importer = new PluginImporter(); + importer.discover([]); + + // Create mock components as if discovered + const components: NamespacedComponent[] = [ + { + name: 'skill-a', + namespace: 'plugin-x', + canonicalName: 'plugin-x:skill-a', + type: 'skill', + filePath: '/x/skill-a.md', + source: 'plugin:plugin-x', + description: undefined, + metadata: {}, + }, + { + name: 'skill-b', + namespace: 'plugin-y', + canonicalName: 'plugin-y:skill-b', + type: 'skill', + filePath: '/y/skill-b.md', + source: 'plugin:plugin-y', + description: undefined, + metadata: {}, + }, + ]; + + // Validate should work with any component set + const validation = importer.validateImport(components); + assert.strictEqual(validation.canProceed, true); + + // Manifest should preserve namespace info + const manifest = importer.getImportManifest(components); + assert.strictEqual(manifest.entries.length, 2); + assert.strictEqual(manifest.summary.namespaces.length, 2); + assert.ok(manifest.summary.namespaces.includes('plugin-x')); + assert.ok(manifest.summary.namespaces.includes('plugin-y')); + }); + + it('should support filtering by component type', () => { + const importer = new PluginImporter(); + importer.discover([]); + + const components: NamespacedComponent[] = [ + { + name: 'skill-a', + namespace: 'plugin', + canonicalName: 'plugin:skill-a', + type: 'skill', + filePath: '/skill-a.md', + source: 'plugin:plugin', + description: undefined, + metadata: {}, + }, + { + name: 'agent-x', + namespace: 'plugin', + canonicalName: 'plugin:agent-x', + type: 'agent', + filePath: '/agent-x.md', + source: 'plugin:plugin', + description: undefined, + metadata: {}, + }, + ]; + + const manifest = importer.getImportManifest(components); + + assert.strictEqual(manifest.summary.skills, 1); + assert.strictEqual(manifest.summary.agents, 1); + }); + }); + + describe('Pre-import diagnostics gating', () => { + it('should block import on canonical collision (error)', () => { + const importer = new PluginImporter(); + importer.discover([]); + + const components: NamespacedComponent[] = [ + { + name: 'skill', + namespace: 'ns', + canonicalName: 'ns:skill', + type: 'skill', + filePath: '/first.md', + source: 'first', + description: undefined, + metadata: {}, + }, + { + name: 'skill', + namespace: 'ns', + canonicalName: 'ns:skill', // Collision + type: 'skill', + filePath: '/second.md', + source: 'second', + description: undefined, + metadata: {}, + }, + ]; + + const validation = importer.validateImport(components); + + // Should block - error severity + assert.strictEqual(validation.canProceed, false); + assert.strictEqual(validation.summary.errors, 1); + }); + + it('should allow import with shorthand overlap (warning)', () => { + const importer = new PluginImporter(); + importer.discover([]); + + const components: NamespacedComponent[] = [ + { + name: 'review', + namespace: 'plugin-a', + canonicalName: 'plugin-a:review', + type: 'skill', + filePath: '/a/review.md', + source: 'plugin:plugin-a', + description: undefined, + metadata: {}, + }, + { + name: 'review', + namespace: 'plugin-b', + canonicalName: 'plugin-b:review', + type: 'skill', + filePath: '/b/review.md', + source: 'plugin:plugin-b', + description: undefined, + metadata: {}, + }, + ]; + + const validation = importer.validateImport(components); + + // Should NOT block - warning only + assert.strictEqual(validation.canProceed, true); + assert.strictEqual(validation.summary.warnings, 1); + assert.strictEqual(validation.summary.errors, 0); + }); + + it('should provide actionable diagnostics for blocking errors', () => { + const importer = new PluginImporter(); + importer.discover([]); + + const components: NamespacedComponent[] = [ + { + name: 'dup', + namespace: 'ns', + canonicalName: 'ns:dup', + type: 'skill', + filePath: '/first.md', + source: 'first', + description: undefined, + metadata: {}, + }, + { + name: 'dup', + namespace: 'ns', + canonicalName: 'ns:dup', + type: 'skill', + filePath: '/second.md', + source: 'second', + description: undefined, + metadata: {}, + }, + ]; + + const validation = importer.validateImport(components); + + // Should have diagnostic with remediation + assert.strictEqual(validation.diagnostics.length, 1); + assert.ok(validation.diagnostics[0]!.remediation.length > 0); + assert.ok(validation.diagnostics[0]!.remediation.includes('ns:dup')); + }); + }); + + describe('Config persistence with canonical names', () => { + it('should preserve canonical names in manifest for persistence', () => { + const importer = new PluginImporter(); + + const components: NamespacedComponent[] = [ + { + name: 'code-review', + namespace: 'my-plugin', + canonicalName: 'my-plugin:code-review', + type: 'skill', + filePath: '/plugins/my-plugin/skills/code-review/SKILL.md', + source: 'plugin:my-plugin', + description: 'Reviews code', + metadata: { pluginVersion: '1.0.0' }, + }, + { + name: 'architect', + namespace: 'my-plugin', + canonicalName: 'my-plugin:architect', + type: 'agent', + filePath: '/plugins/my-plugin/agents/architect/AGENT.md', + source: 'plugin:my-plugin', + description: 'Designs architecture', + metadata: { pluginVersion: '1.0.0' }, + }, + ]; + + const manifest = importer.getImportManifest(components); + + // Verify canonical names preserved + assert.strictEqual(manifest.entries.length, 2); + assert.strictEqual(manifest.entries[0]!.canonicalName, 'my-plugin:code-review'); + assert.strictEqual(manifest.entries[1]!.canonicalName, 'my-plugin:architect'); + + // Verify manifest is JSON-serializable for config persistence + const json = JSON.stringify(manifest); + const parsed = JSON.parse(json); + assert.strictEqual(parsed.entries[0].canonicalName, 'my-plugin:code-review'); + }); + + it('should include file paths for settings persistence', () => { + const importer = new PluginImporter(); + + const components: NamespacedComponent[] = [ + { + name: 'skill', + namespace: 'plugin', + canonicalName: 'plugin:skill', + type: 'skill', + filePath: '/absolute/path/to/skill.md', + source: 'plugin:plugin', + description: undefined, + metadata: {}, + }, + ]; + + const manifest = importer.getImportManifest(components); + + assert.strictEqual(manifest.entries[0]!.filePath, '/absolute/path/to/skill.md'); + }); + + it('should separate skills and agents for settings routing', () => { + const importer = new PluginImporter(); + + const components: NamespacedComponent[] = [ + { + name: 'skill-1', + namespace: 'p', + canonicalName: 'p:skill-1', + type: 'skill', + filePath: '/s1.md', + source: 'plugin:p', + description: undefined, + metadata: {}, + }, + { + name: 'skill-2', + namespace: 'p', + canonicalName: 'p:skill-2', + type: 'skill', + filePath: '/s2.md', + source: 'plugin:p', + description: undefined, + metadata: {}, + }, + { + name: 'agent-1', + namespace: 'p', + canonicalName: 'p:agent-1', + type: 'agent', + filePath: '/a1.md', + source: 'plugin:p', + description: undefined, + metadata: {}, + }, + ]; + + const manifest = importer.getImportManifest(components); + + const skills = manifest.entries.filter(e => e.type === 'skill'); + const agents = manifest.entries.filter(e => e.type === 'agent'); + + assert.strictEqual(skills.length, 2); + assert.strictEqual(agents.length, 1); + }); + }); + + describe('End-to-end command flow simulation', () => { + it('should execute full pipeline: discover → select → validate → manifest', () => { + const importer = new PluginImporter(); + + // Stage 1: Discover (empty in this test) + const discovery = importer.discover([]); + assert.strictEqual(discovery.summary.totalComponents, 0); + + // Stage 2: Simulate user selection (mock components) + const selected: NamespacedComponent[] = [ + { + name: 'code-review', + namespace: 'my-plugin', + canonicalName: 'my-plugin:code-review', + type: 'skill', + filePath: '/plugins/my-plugin/skills/code-review/SKILL.md', + source: 'plugin:my-plugin', + description: 'Reviews code', + metadata: { pluginVersion: '1.0.0' }, + }, + ]; + + // Stage 3: Validate + const validation = importer.validateImport(selected); + assert.strictEqual(validation.canProceed, true); + + // Stage 4: Generate manifest + const manifest = importer.getImportManifest(selected); + assert.strictEqual(manifest.entries.length, 1); + assert.strictEqual(manifest.entries[0]!.canonicalName, 'my-plugin:code-review'); + }); + + it('should block on validation failure before persistence', () => { + const importer = new PluginImporter(); + importer.discover([]); + + const selected: NamespacedComponent[] = [ + { + name: 'dup', + namespace: 'ns', + canonicalName: 'ns:dup', + type: 'skill', + filePath: '/first.md', + source: 'first', + description: undefined, + metadata: {}, + }, + { + name: 'dup', + namespace: 'ns', + canonicalName: 'ns:dup', + type: 'skill', + filePath: '/second.md', + source: 'second', + description: undefined, + metadata: {}, + }, + ]; + + const validation = importer.validateImport(selected); + + // Simulate command flow logic: should NOT proceed to persistence + if (validation.canProceed) { + // This should NOT be reached + assert.fail('Should not proceed to persistence with errors'); + } else { + // Correct: blocked before persistence + assert.strictEqual(validation.summary.errors, 1); + } + }); + + it('should allow proceeding after user confirms warnings', () => { + const importer = new PluginImporter(); + importer.discover([]); + + const selected: NamespacedComponent[] = [ + { + name: 'review', + namespace: 'plugin-a', + canonicalName: 'plugin-a:review', + type: 'skill', + filePath: '/a/review.md', + source: 'plugin:plugin-a', + description: undefined, + metadata: {}, + }, + { + name: 'review', + namespace: 'plugin-b', + canonicalName: 'plugin-b:review', + type: 'skill', + filePath: '/b/review.md', + source: 'plugin:plugin-b', + description: undefined, + metadata: {}, + }, + ]; + + const validation = importer.validateImport(selected); + + // Warnings should NOT block + assert.strictEqual(validation.canProceed, true); + assert.strictEqual(validation.summary.warnings, 1); + + // Simulate user confirmation and proceed to manifest + const manifest = importer.getImportManifest(selected); + assert.strictEqual(manifest.entries.length, 2); + }); + }); +}); diff --git a/src/resources/extensions/universal-config/discovery.ts b/src/resources/extensions/universal-config/discovery.ts index 12768111b..39516ecd0 100644 --- a/src/resources/extensions/universal-config/discovery.ts +++ b/src/resources/extensions/universal-config/discovery.ts @@ -58,6 +58,8 @@ export async function discoverAllConfigs( const rules = allItems.filter((i) => i.type === "rule").length; const contextFiles = allItems.filter((i) => i.type === "context-file").length; const settings = allItems.filter((i) => i.type === "settings").length; + const claudeSkills = allItems.filter((i) => i.type === "claude-skill").length; + const claudePlugins = allItems.filter((i) => i.type === "claude-plugin").length; const toolsWithConfig = toolResults.filter((r) => r.items.length > 0).length; return { @@ -68,6 +70,8 @@ export async function discoverAllConfigs( rules, contextFiles, settings, + claudeSkills, + claudePlugins, totalItems: allItems.length, toolsScanned: TOOLS.length, toolsWithConfig, diff --git a/src/resources/extensions/universal-config/format.ts b/src/resources/extensions/universal-config/format.ts index be6b69a54..41cdff663 100644 --- a/src/resources/extensions/universal-config/format.ts +++ b/src/resources/extensions/universal-config/format.ts @@ -23,7 +23,7 @@ export function formatDiscoveryForTool(result: DiscoveryResult): string { return lines.join("\n"); } - lines.push(`Found: ${summary.mcpServers} MCP server(s), ${summary.rules} rule(s), ${summary.contextFiles} context file(s), ${summary.settings} settings file(s)`); + lines.push(`Found: ${summary.mcpServers} MCP server(s), ${summary.rules} rule(s), ${summary.contextFiles} context file(s), ${summary.settings} settings file(s), ${summary.claudeSkills} Claude skill(s), ${summary.claudePlugins} Claude plugin(s)`); lines.push(""); for (const toolResult of result.tools) { @@ -70,9 +70,24 @@ export function formatDiscoveryForTool(result: DiscoveryResult): string { lines.push(` Settings (${byType.settings.length}):`); for (const item of byType.settings) { if (item.type !== "settings") continue; - const keys = Object.keys(item.data).slice(0, 5); - const suffix = Object.keys(item.data).length > 5 ? ` +${Object.keys(item.data).length - 5} more` : ""; - lines.push(` - ${item.source.path} (${item.source.level}): keys: ${keys.join(", ")}${suffix}`); + lines.push(` - ${item.source.path} (${item.source.level})`); + } + } + + if (byType["claude-skill"]?.length) { + lines.push(` Claude Skills (${byType["claude-skill"].length}):`); + for (const item of byType["claude-skill"]) { + if (item.type !== "claude-skill") continue; + lines.push(` - ${item.name} (${item.source.level}) ${item.path}`); + } + } + + if (byType["claude-plugin"]?.length) { + lines.push(` Claude Plugins (${byType["claude-plugin"].length}):`); + for (const item of byType["claude-plugin"]) { + if (item.type !== "claude-plugin") continue; + const label = item.packageName ? `${item.name} [${item.packageName}]` : item.name; + lines.push(` - ${label} (${item.source.level}) ${item.path}`); } } @@ -111,6 +126,8 @@ export function formatDiscoveryForCommand(result: DiscoveryResult): string[] { lines.push(` Rules: ${summary.rules}`); lines.push(` Context: ${summary.contextFiles}`); lines.push(` Settings: ${summary.settings}`); + lines.push(` Claude skills: ${summary.claudeSkills}`); + lines.push(` Claude plugins: ${summary.claudePlugins}`); lines.push(""); for (const toolResult of result.tools) { @@ -122,6 +139,8 @@ export function formatDiscoveryForCommand(result: DiscoveryResult): string[] { if (counts.rule) parts.push(`${counts.rule} rules`); if (counts["context-file"]) parts.push(`${counts["context-file"]} context`); if (counts.settings) parts.push(`${counts.settings} settings`); + if (counts["claude-skill"]) parts.push(`${counts["claude-skill"]} Claude skills`); + if (counts["claude-plugin"]) parts.push(`${counts["claude-plugin"]} Claude plugins`); lines.push(` ${toolResult.tool.name}: ${parts.join(", ")}`); @@ -131,6 +150,18 @@ export function formatDiscoveryForCommand(result: DiscoveryResult): string[] { if (server.type !== "mcp-server") continue; lines.push(` MCP: ${server.name} (${server.source.level})`); } + + const claudeSkills = toolResult.items.filter((i) => i.type === "claude-skill"); + for (const skill of claudeSkills) { + if (skill.type !== "claude-skill") continue; + lines.push(` Skill: ${skill.name} (${skill.source.level})`); + } + + const claudePlugins = toolResult.items.filter((i) => i.type === "claude-plugin"); + for (const plugin of claudePlugins) { + if (plugin.type !== "claude-plugin") continue; + lines.push(` Plugin: ${plugin.name} (${plugin.source.level})`); + } } if (result.warnings.length > 0) { diff --git a/src/resources/extensions/universal-config/index.ts b/src/resources/extensions/universal-config/index.ts index 9201c5e88..bd0492858 100644 --- a/src/resources/extensions/universal-config/index.ts +++ b/src/resources/extensions/universal-config/index.ts @@ -31,13 +31,13 @@ export default function universalConfig(pi: ExtensionAPI) { label: "Discover Configs", description: "Scan for existing AI coding tool configurations in this project and the user's home directory. " + - "Discovers MCP servers, rules, context files, and settings from Claude Code, Cursor, Windsurf, " + + "Discovers MCP servers, rules, context files, settings, Claude skills, and Claude plugins from Claude Code, Cursor, Windsurf, " + "Gemini CLI, Codex, Cline, GitHub Copilot, and VS Code. Read-only — never modifies config files.", - promptSnippet: "Discover existing AI tool configs (MCP servers, rules, context files) from 8 coding tools.", + promptSnippet: "Discover existing AI tool configs (MCP servers, rules, context files, Claude skills/plugins) from 8 coding tools.", promptGuidelines: [ "Use discover_configs when a user asks about their existing configuration, MCP servers, or when switching from another AI coding tool.", "The tool scans both user-level (~/) and project-level (./) config directories.", - "Results include MCP servers that could be reused, rules/instructions that could be adapted, and context files from other tools.", + "Results include MCP servers that could be reused, rules/instructions that could be adapted, context files from other tools, and Claude skills/plugins that could be imported.", ], parameters: Type.Object({ tool: Type.Optional( @@ -83,6 +83,8 @@ export default function universalConfig(pi: ExtensionAPI) { rules: allItems.filter((i) => i.type === "rule").length, contextFiles: allItems.filter((i) => i.type === "context-file").length, settings: allItems.filter((i) => i.type === "settings").length, + claudeSkills: allItems.filter((i) => i.type === "claude-skill").length, + claudePlugins: allItems.filter((i) => i.type === "claude-plugin").length, totalItems: allItems.length, toolsWithConfig: filtered.filter((t) => t.items.length > 0).length, }, diff --git a/src/resources/extensions/universal-config/scanners.ts b/src/resources/extensions/universal-config/scanners.ts index 874a7b0f4..3688a3ff8 100644 --- a/src/resources/extensions/universal-config/scanners.ts +++ b/src/resources/extensions/universal-config/scanners.ts @@ -8,6 +8,7 @@ */ import { readFile, readdir, stat } from "node:fs/promises"; +import { existsSync, readdirSync, readFileSync } from "node:fs"; import { join, basename, resolve } from "node:path"; import { homedir } from "node:os"; import type { @@ -29,6 +30,30 @@ function source(tool: ToolInfo, path: string, level: ConfigLevel): ConfigSource return { tool: tool.id, toolName: tool.name, path, level }; } +function walkDirectories(root: string, visit: (dir: string, depth: number) => void, maxDepth = 4): void { + const skip = new Set([".git", "node_modules", ".worktrees", "dist", "build", "cache", ".cache"]); + + function walk(dir: string, depth: number) { + visit(dir, depth); + if (depth >= maxDepth) return; + + let entries: Array<{ name: string; isDirectory: () => boolean }> = []; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + if (skip.has(entry.name)) continue; + walk(join(dir, entry.name), depth + 1); + } + } + + walk(root, 0); +} + async function readTextFile(path: string): Promise { try { return await readFile(path, "utf8"); @@ -208,6 +233,44 @@ async function scanClaude(projectRoot: string, home: string, tool: ToolInfo): Pr } } + // Claude skills: ~/.claude/skills/**/SKILL.md + const userSkillsRoot = join(home, ".claude/skills"); + if (existsSync(userSkillsRoot)) { + walkDirectories(userSkillsRoot, (dir) => { + const skillFile = join(dir, "SKILL.md"); + if (!existsSync(skillFile)) return; + items.push({ + type: "claude-skill", + name: basename(dir), + path: dir, + source: source(tool, skillFile, "user"), + }); + }, 5); + } + + // Claude plugins: ~/.claude/plugins/**/package.json + const userPluginsRoot = join(home, ".claude/plugins"); + if (existsSync(userPluginsRoot)) { + walkDirectories(userPluginsRoot, (dir) => { + const packageJsonPath = join(dir, "package.json"); + if (!existsSync(packageJsonPath)) return; + let packageName: string | undefined; + try { + const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { name?: string }; + packageName = pkg.name; + } catch { + packageName = undefined; + } + items.push({ + type: "claude-plugin", + name: packageName || basename(dir), + packageName, + path: dir, + source: source(tool, packageJsonPath, "user"), + }); + }, 4); + } + // User-level settings: ~/.claude/settings.json const userSettings = join(home, ".claude/settings.json"); const settingsContent = await readTextFile(userSettings); diff --git a/src/resources/extensions/universal-config/tests/discovery.test.ts b/src/resources/extensions/universal-config/tests/discovery.test.ts index 7d3706881..dd1eef96f 100644 --- a/src/resources/extensions/universal-config/tests/discovery.test.ts +++ b/src/resources/extensions/universal-config/tests/discovery.test.ts @@ -41,6 +41,8 @@ describe("discoverAllConfigs", () => { assert.equal(result.summary.totalItems, 0); assert.equal(result.summary.toolsScanned, 8); assert.equal(result.summary.toolsWithConfig, 0); + assert.equal(result.summary.claudeSkills, 0); + assert.equal(result.summary.claudePlugins, 0); assert.ok(result.durationMs >= 0); } finally { cleanup(); @@ -53,6 +55,8 @@ describe("discoverAllConfigs", () => { writeJson(join(testHome, ".claude.json"), { mcpServers: { "claude-mcp": { command: "node", args: ["server.js"] } }, }); + writeText(join(testHome, ".claude/skills/test-skill/SKILL.md"), "# Test skill"); + writeJson(join(testHome, ".claude/plugins/test-plugin/package.json"), { name: "test-plugin" }); writeText(join(testRoot, ".cursorrules"), "Use semicolons."); writeText(join(testRoot, ".github/copilot-instructions.md"), "Be helpful."); @@ -61,7 +65,9 @@ describe("discoverAllConfigs", () => { assert.equal(result.summary.mcpServers, 1); assert.equal(result.summary.rules, 1); assert.equal(result.summary.contextFiles, 1); - assert.equal(result.allItems.length, 3); + assert.equal(result.summary.claudeSkills, 1); + assert.equal(result.summary.claudePlugins, 1); + assert.equal(result.allItems.length, 5); } finally { cleanup(); } @@ -103,6 +109,8 @@ describe("discoverAllConfigs", () => { assert.equal(result.summary.rules, 2); assert.equal(result.summary.contextFiles, 1); assert.equal(result.summary.settings, 1); + assert.equal(result.summary.claudeSkills, 0); + assert.equal(result.summary.claudePlugins, 0); assert.equal(result.summary.totalItems, 5); } finally { cleanup(); diff --git a/src/resources/extensions/universal-config/tests/format.test.ts b/src/resources/extensions/universal-config/tests/format.test.ts index 0d0f48370..aecd29f12 100644 --- a/src/resources/extensions/universal-config/tests/format.test.ts +++ b/src/resources/extensions/universal-config/tests/format.test.ts @@ -16,6 +16,8 @@ const emptyResult: DiscoveryResult = { rules: 0, contextFiles: 0, settings: 0, + claudeSkills: 0, + claudePlugins: 0, totalItems: 0, toolsScanned: 8, toolsWithConfig: 0, @@ -38,11 +40,17 @@ const populatedResult: DiscoveryResult = { source: { tool: "cursor", toolName: "Cursor", path: "/project/.cursor/mcp.json", level: "project" }, }, { - type: "rule", - name: "style", - content: "Use semicolons and strict TypeScript.", - alwaysApply: true, - source: { tool: "cursor", toolName: "Cursor", path: "/project/.cursor/rules/style.mdc", level: "project" }, + type: "claude-skill", + name: "cursor-mdc-editor", + path: "/home/user/.claude/skills/cursor-mdc-editor", + source: { tool: "claude", toolName: "Claude Code", path: "/home/user/.claude/skills/cursor-mdc-editor/SKILL.md", level: "user" }, + }, + { + type: "claude-plugin", + name: "context-mode", + packageName: "context-mode", + path: "/home/user/.claude/plugins/marketplaces/context-mode", + source: { tool: "claude", toolName: "Claude Code", path: "/home/user/.claude/plugins/marketplaces/context-mode/package.json", level: "user" }, }, ], warnings: [], @@ -66,7 +74,9 @@ const populatedResult: DiscoveryResult = { rules: 1, contextFiles: 1, settings: 0, - totalItems: 3, + claudeSkills: 1, + claudePlugins: 1, + totalItems: 5, toolsScanned: 8, toolsWithConfig: 2, }, @@ -86,10 +96,14 @@ describe("formatDiscoveryForTool", () => { const text = formatDiscoveryForTool(populatedResult); assert.ok(text.includes("2/8 tools with config")); assert.ok(text.includes("1 MCP server(s)")); + assert.ok(text.includes("1 Claude skill(s)")); + assert.ok(text.includes("1 Claude plugin(s)")); assert.ok(text.includes("Cursor")); assert.ok(text.includes("test-mcp")); assert.ok(text.includes("GitHub Copilot")); assert.ok(text.includes("copilot-instructions.md")); + assert.ok(text.includes("cursor-mdc-editor")); + assert.ok(text.includes("context-mode")); }); }); @@ -107,5 +121,7 @@ describe("formatDiscoveryForCommand", () => { assert.ok(text.includes("2 of 8")); assert.ok(text.includes("Cursor")); assert.ok(text.includes("MCP: test-mcp")); + assert.ok(text.includes("Skill: cursor-mdc-editor")); + assert.ok(text.includes("Plugin: context-mode")); }); }); diff --git a/src/resources/extensions/universal-config/tests/scanners.test.ts b/src/resources/extensions/universal-config/tests/scanners.test.ts index f31c8452a..c63e7c4b6 100644 --- a/src/resources/extensions/universal-config/tests/scanners.test.ts +++ b/src/resources/extensions/universal-config/tests/scanners.test.ts @@ -107,6 +107,24 @@ describe("Claude Code scanner", () => { } }); + test("discovers Claude Code skills and plugins", async () => { + const { testRoot, testHome, cleanup } = makeTempDirs(); + try { + writeText(join(testHome, ".claude/skills/test-skill/SKILL.md"), "# test skill"); + writeJson(join(testHome, ".claude/plugins/test-plugin/package.json"), { name: "test-plugin" }); + + const { items } = await SCANNERS.claude(testRoot, testHome, getTool("claude")); + const skills = items.filter((i) => i.type === "claude-skill"); + const plugins = items.filter((i) => i.type === "claude-plugin"); + assert.equal(skills.length, 1); + assert.equal(plugins.length, 1); + if (skills[0]?.type === "claude-skill") assert.equal(skills[0].name, "test-skill"); + if (plugins[0]?.type === "claude-plugin") assert.equal(plugins[0].name, "test-plugin"); + } finally { + cleanup(); + } + }); + test("discovers settings.json", async () => { const { testRoot, testHome, cleanup } = makeTempDirs(); try { diff --git a/src/resources/extensions/universal-config/types.ts b/src/resources/extensions/universal-config/types.ts index 3f3eeae37..1c353d590 100644 --- a/src/resources/extensions/universal-config/types.ts +++ b/src/resources/extensions/universal-config/types.ts @@ -80,11 +80,28 @@ export interface DiscoveredSettings { source: ConfigSource; } +export interface DiscoveredClaudeSkill { + type: "claude-skill"; + name: string; + path: string; + source: ConfigSource; +} + +export interface DiscoveredClaudePlugin { + type: "claude-plugin"; + name: string; + path: string; + packageName?: string; + source: ConfigSource; +} + export type DiscoveredItem = | DiscoveredMCPServer | DiscoveredRule | DiscoveredContextFile - | DiscoveredSettings; + | DiscoveredSettings + | DiscoveredClaudeSkill + | DiscoveredClaudePlugin; // ── Discovery result ────────────────────────────────────────────────────────── @@ -105,6 +122,8 @@ export interface DiscoveryResult { rules: number; contextFiles: number; settings: number; + claudeSkills: number; + claudePlugins: number; totalItems: number; toolsScanned: number; toolsWithConfig: number; diff --git a/src/tests/marketplace-discovery.test.ts b/src/tests/marketplace-discovery.test.ts new file mode 100644 index 000000000..9bb981f09 --- /dev/null +++ b/src/tests/marketplace-discovery.test.ts @@ -0,0 +1,409 @@ +/** + * Marketplace Discovery Contract Tests + * + * Contract tests that exercise discoverMarketplace against real marketplace repos + * (../claude_skills and ../claude-plugins-official). These tests validate: + * - R001: marketplace parsing + * - R002: path resolution + * - R003: manifest inspection + * + * Tests run against real data, not synthetic fixtures. + */ + +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import * as path from 'node:path'; +import * as fs from 'node:fs'; +import { + parseMarketplaceJson, + inspectPlugin, + discoverMarketplace, + resolvePluginRoot +} from '../resources/extensions/gsd/marketplace-discovery.js'; +import { getMarketplaceFixtures } from '../resources/extensions/gsd/tests/marketplace-test-fixtures.js'; + +const fixtureSetup = getMarketplaceFixtures(import.meta.dirname); +const fixtures = fixtureSetup.fixtures; +const CLAUDE_SKILLS_PATH = fixtures?.claudeSkillsPath; +const CLAUDE_PLUGINS_OFFICIAL_PATH = fixtures?.claudePluginsOfficialPath; + +const skipReason = !fixtureSetup.available + ? fixtureSetup.skipReason ?? 'Marketplace repos not found' + : undefined; + +describe('Marketplace Discovery Contract Tests', { skip: skipReason }, () => { + describe('claude_skills marketplace (jamie-style)', () => { + it('should discover at least 15 plugins', () => { + const result = discoverMarketplace(CLAUDE_SKILLS_PATH!); + + assert.strictEqual(result.status, 'ok', `Expected ok status, got error: ${result.error}`); + assert.ok(result.plugins.length >= 15, + `Expected at least 15 plugins, found ${result.plugins.length}`); + }); + + it('should detect jamie-style format', () => { + const result = discoverMarketplace(CLAUDE_SKILLS_PATH!); + + assert.strictEqual(result.pluginFormat, 'jamie-style'); + }); + + it('should verify python3-development has skills and agents', () => { + const result = discoverMarketplace(CLAUDE_SKILLS_PATH!); + const pythonPlugin = result.plugins.find(p => p.name === 'python3-development'); + + assert.ok(pythonPlugin, 'python3-development plugin should exist'); + assert.strictEqual(pythonPlugin.status, 'ok', + `Plugin should have ok status, got error: ${pythonPlugin.error}`); + + // Verify skills inventory + assert.ok(pythonPlugin.inventory.skills.length > 0, + `python3-development should have skills, found: ${pythonPlugin.inventory.skills.length}`); + assert.ok(pythonPlugin.inventory.skills.length >= 10, + `python3-development should have at least 10 skills, found ${pythonPlugin.inventory.skills.length}`); + + // Verify agents inventory + assert.ok(pythonPlugin.inventory.agents.length > 0, + `python3-development should have agents, found: ${pythonPlugin.inventory.agents.length}`); + assert.ok(pythonPlugin.inventory.agents.length >= 5, + `python3-development should have at least 5 agents, found ${pythonPlugin.inventory.agents.length}`); + }); + + it('should verify all resolved paths exist on disk', () => { + const result = discoverMarketplace(CLAUDE_SKILLS_PATH!); + + // Filter plugins with resolved paths (local plugins, not external) + const localPlugins = result.plugins.filter(p => p.resolvedPath !== null); + + assert.ok(localPlugins.length > 0, 'Should have at least one local plugin'); + + for (const plugin of localPlugins) { + assert.ok(fs.existsSync(plugin.resolvedPath!), + `Plugin ${plugin.name} resolved path should exist: ${plugin.resolvedPath}`); + } + }); + + it('should preserve canonical names for known plugins', () => { + const result = discoverMarketplace(CLAUDE_SKILLS_PATH!); + const knownPluginNames = [ + 'python3-development', + 'bash-development', + 'gitlab-skill', + 'commitlint', + 'conventional-commits', + 'fastmcp-creator' + ]; + + for (const expectedName of knownPluginNames) { + const plugin = result.plugins.find(p => p.name === expectedName); + assert.ok(plugin, `Plugin ${expectedName} should exist`); + assert.strictEqual(plugin.canonicalName, expectedName, + `Canonical name should match for ${expectedName}`); + } + }); + + it('should have consistent summary counts', () => { + const result = discoverMarketplace(CLAUDE_SKILLS_PATH!); + + assert.strictEqual(result.summary.total, result.plugins.length, + 'Total count should match plugins array length'); + assert.strictEqual(result.summary.ok, + result.plugins.filter(p => p.status === 'ok').length, + 'Ok count should match plugins with ok status'); + assert.strictEqual(result.summary.error, + result.plugins.filter(p => p.status === 'error').length, + 'Error count should match plugins with error status'); + }); + }); + + describe('claude-plugins-official marketplace (official-style)', () => { + it('should discover at least 10 plugins', () => { + const result = discoverMarketplace(CLAUDE_PLUGINS_OFFICIAL_PATH!); + + assert.strictEqual(result.status, 'ok', `Expected ok status, got error: ${result.error}`); + assert.ok(result.plugins.length >= 10, + `Expected at least 10 plugins, found ${result.plugins.length}`); + }); + + it('should detect official-style format', () => { + const result = discoverMarketplace(CLAUDE_PLUGINS_OFFICIAL_PATH!); + + assert.strictEqual(result.pluginFormat, 'official-style'); + }); + + it('should extract LSP servers from inline marketplace metadata', () => { + const result = discoverMarketplace(CLAUDE_PLUGINS_OFFICIAL_PATH!); + + // TypeScript LSP plugin should have lspServers from marketplace.json + const tsPlugin = result.plugins.find(p => p.name === 'typescript-lsp'); + assert.ok(tsPlugin, 'typescript-lsp plugin should exist'); + assert.ok(Object.keys(tsPlugin.inventory.lspServers).length > 0, + 'typescript-lsp should have LSP servers from inline metadata'); + assert.ok('typescript' in tsPlugin.inventory.lspServers, + 'typescript-lsp should have typescript LSP server'); + + // Verify LSP server config structure + const tsLspConfig = tsPlugin.inventory.lspServers.typescript as { command?: string }; + assert.strictEqual(tsLspConfig.command, 'typescript-language-server', + 'TypeScript LSP should use typescript-language-server command'); + }); + + it('should have description from inline metadata', () => { + const result = discoverMarketplace(CLAUDE_PLUGINS_OFFICIAL_PATH!); + + const tsPlugin = result.plugins.find(p => p.name === 'typescript-lsp'); + assert.ok(tsPlugin, 'typescript-lsp plugin should exist'); + assert.ok(tsPlugin.description, 'typescript-lsp should have description'); + assert.ok(tsPlugin.description.includes('TypeScript'), + 'Description should mention TypeScript'); + }); + + it('should handle external plugins (URL sources) correctly', () => { + const result = discoverMarketplace(CLAUDE_PLUGINS_OFFICIAL_PATH!); + + // Find plugins with URL sources (external) + const externalPlugins = result.plugins.filter(p => p.resolvedPath === null); + + assert.ok(externalPlugins.length > 0, + 'Should have at least one external plugin with null resolvedPath'); + + // External plugins should still have ok status (they're valid, just not local) + const atlassian = externalPlugins.find(p => p.name === 'atlassian'); + assert.ok(atlassian, 'atlassian plugin should exist as external'); + assert.strictEqual(atlassian.status, 'ok', + 'External plugins should have ok status'); + }); + + it('should preserve canonical names for known official plugins', () => { + const result = discoverMarketplace(CLAUDE_PLUGINS_OFFICIAL_PATH!); + const knownPluginNames = [ + 'typescript-lsp', + 'pyright-lsp', + 'gopls-lsp', + 'rust-analyzer-lsp', + 'feature-dev', + 'pr-review-toolkit' + ]; + + for (const expectedName of knownPluginNames) { + const plugin = result.plugins.find(p => p.name === expectedName); + assert.ok(plugin, `Plugin ${expectedName} should exist in official marketplace`); + assert.strictEqual(plugin.canonicalName, expectedName, + `Canonical name should match for ${expectedName}`); + } + }); + + it('should extract multiple LSP server types', () => { + const result = discoverMarketplace(CLAUDE_PLUGINS_OFFICIAL_PATH!); + + // Check that multiple LSP plugins have their servers extracted + const lspPlugins = [ + { name: 'pyright-lsp', server: 'pyright' }, + { name: 'gopls-lsp', server: 'gopls' }, + { name: 'rust-analyzer-lsp', server: 'rust-analyzer' }, + { name: 'clangd-lsp', server: 'clangd' } + ]; + + for (const { name, server } of lspPlugins) { + const plugin = result.plugins.find(p => p.name === name); + assert.ok(plugin, `${name} plugin should exist`); + assert.ok(server in plugin.inventory.lspServers, + `${name} should have ${server} LSP server`); + } + }); + }); + + describe('Error handling', () => { + it('should return structured error for non-existent repo path', () => { + const result = discoverMarketplace('/tmp/nonexistent-marketplace-' + Date.now()); + + assert.strictEqual(result.status, 'error'); + assert.ok(result.error, 'Error message should be present'); + assert.ok(result.error.includes('not found'), + `Error should mention 'not found', got: ${result.error}`); + assert.deepStrictEqual(result.plugins, []); + assert.strictEqual(result.summary.total, 0); + assert.strictEqual(result.summary.ok, 0); + assert.strictEqual(result.summary.error, 0); + }); + + it('should return error for directory without marketplace.json', () => { + // Create a temp directory without marketplace.json + const tmpDir = '/tmp/test-no-marketplace-' + Date.now(); + fs.mkdirSync(tmpDir, { recursive: true }); + + try { + const result = discoverMarketplace(tmpDir); + + assert.strictEqual(result.status, 'error'); + assert.ok(result.error, 'Error message should be present'); + assert.ok(result.error.includes('not found'), + `Error should mention 'not found', got: ${result.error}`); + } finally { + fs.rmSync(tmpDir, { recursive: true }); + } + }); + + it('should return error for malformed marketplace.json', () => { + const tmpDir = '/tmp/test-malformed-marketplace-' + Date.now(); + fs.mkdirSync(tmpDir + '/.claude-plugin', { recursive: true }); + fs.writeFileSync(tmpDir + '/.claude-plugin/marketplace.json', '{ this is not valid json }'); + + try { + const result = discoverMarketplace(tmpDir); + + assert.strictEqual(result.status, 'error'); + assert.ok(result.error, 'Error message should be present'); + assert.ok(result.error.includes('Failed to parse'), + `Error should mention 'Failed to parse', got: ${result.error}`); + } finally { + fs.rmSync(tmpDir, { recursive: true }); + } + }); + + it('should return error for marketplace.json missing required fields', () => { + const tmpDir = '/tmp/test-invalid-marketplace-' + Date.now(); + fs.mkdirSync(tmpDir + '/.claude-plugin', { recursive: true }); + // Valid JSON but missing required 'name' and 'plugins' fields + fs.writeFileSync(tmpDir + '/.claude-plugin/marketplace.json', JSON.stringify({ description: 'test' })); + + try { + const parseResult = parseMarketplaceJson(tmpDir); + + assert.strictEqual(parseResult.success, false); + if (!parseResult.success) { + assert.ok(parseResult.error.includes('missing'), + `Error should mention missing field, got: ${parseResult.error}`); + } + } finally { + fs.rmSync(tmpDir, { recursive: true }); + } + }); + + it('should handle missing plugin directory gracefully', () => { + const tmpDir = '/tmp/test-missing-plugin-' + Date.now(); + fs.mkdirSync(tmpDir + '/.claude-plugin', { recursive: true }); + fs.writeFileSync(tmpDir + '/.claude-plugin/marketplace.json', JSON.stringify({ + name: 'test-marketplace', + plugins: [ + { name: 'missing-plugin', source: './plugins/nonexistent' } + ] + })); + + try { + const result = discoverMarketplace(tmpDir); + + // Marketplace should parse ok, but the missing plugin should have error status + assert.strictEqual(result.status, 'error'); // Because one plugin has error + + const missingPlugin = result.plugins.find(p => p.name === 'missing-plugin'); + assert.ok(missingPlugin, 'Missing plugin should be in results'); + assert.strictEqual(missingPlugin.status, 'error'); + assert.ok(missingPlugin.error, 'Missing plugin should have error message'); + assert.ok(missingPlugin.error.includes('not found'), + `Error should mention 'not found', got: ${missingPlugin.error}`); + } finally { + fs.rmSync(tmpDir, { recursive: true }); + } + }); + }); + + describe('Component inventory accuracy', () => { + it('should accurately count skills in python3-development', () => { + const result = discoverMarketplace(CLAUDE_SKILLS_PATH!); + const pythonPlugin = result.plugins.find(p => p.name === 'python3-development'); + + assert.ok(pythonPlugin, 'python3-development should exist'); + + // Verify by directly counting the skills directory + const skillsDir = path.join(pythonPlugin.resolvedPath!, 'skills'); + if (fs.existsSync(skillsDir)) { + const actualSkills = fs.readdirSync(skillsDir) + .filter(item => { + const itemPath = path.join(skillsDir, item); + return fs.statSync(itemPath).isDirectory() || item.endsWith('.md'); + }); + + // Allow for some variance due to filtering differences + assert.ok(Math.abs(pythonPlugin.inventory.skills.length - actualSkills.length) <= 2, + `Skills count should be close to actual: reported ${pythonPlugin.inventory.skills.length}, actual ${actualSkills.length}`); + } + }); + + it('should discover MCP servers from plugin.json', () => { + const result = discoverMarketplace(CLAUDE_SKILLS_PATH!); + const pythonPlugin = result.plugins.find(p => p.name === 'python3-development'); + + assert.ok(pythonPlugin, 'python3-development should exist'); + assert.ok(Object.keys(pythonPlugin.inventory.mcpServers).length > 0, + 'python3-development should have MCP servers from plugin.json'); + }); + + it('should include commands in inventory when present', () => { + const result = discoverMarketplace(CLAUDE_SKILLS_PATH!); + const pythonPlugin = result.plugins.find(p => p.name === 'python3-development'); + + assert.ok(pythonPlugin, 'python3-development should exist'); + assert.ok(pythonPlugin.inventory.commands.length > 0, + 'python3-development should have commands'); + }); + + it('should detect hooks when present', () => { + const result = discoverMarketplace(CLAUDE_SKILLS_PATH!); + + // Find any plugin with hooks + const pluginWithHooks = result.plugins.find(p => + p.inventory.hooks && p.inventory.hooks.length > 0 + ); + + // At least some plugins should have hooks + assert.ok(pluginWithHooks !== undefined, + 'At least one plugin should have hooks'); + }); + }); + + describe('Cross-marketplace consistency', () => { + it('should return consistent type structure for both marketplaces', () => { + const jamie = discoverMarketplace(CLAUDE_SKILLS_PATH!); + const official = discoverMarketplace(CLAUDE_PLUGINS_OFFICIAL_PATH!); + + // Both should have the same top-level structure + const requiredKeys = ['status', 'marketplacePath', 'marketplaceName', + 'pluginFormat', 'plugins', 'summary']; + + for (const key of requiredKeys) { + assert.ok(key in jamie, `jamie result should have ${key}`); + assert.ok(key in official, `official result should have ${key}`); + } + + // Both summaries should have same structure + const summaryKeys = ['total', 'ok', 'error']; + for (const key of summaryKeys) { + assert.ok(key in jamie.summary, `jamie summary should have ${key}`); + assert.ok(key in official.summary, `official summary should have ${key}`); + } + }); + + it('should return consistent plugin structure', () => { + const jamie = discoverMarketplace(CLAUDE_SKILLS_PATH!); + const official = discoverMarketplace(CLAUDE_PLUGINS_OFFICIAL_PATH!); + + const jamiePlugin = jamie.plugins[0]; + const officialPlugin = official.plugins[0]; + + const requiredKeys = ['name', 'canonicalName', 'source', 'resolvedPath', + 'status', 'manifestSource', 'inventory']; + + for (const key of requiredKeys) { + assert.ok(key in jamiePlugin, `jamie plugin should have ${key}`); + assert.ok(key in officialPlugin, `official plugin should have ${key}`); + } + + // Inventory structure should be consistent + const inventoryKeys = ['skills', 'agents', 'commands', 'mcpServers', 'lspServers']; + for (const key of inventoryKeys) { + assert.ok(key in jamiePlugin.inventory, `jamie inventory should have ${key}`); + assert.ok(key in officialPlugin.inventory, `official inventory should have ${key}`); + } + }); + }); +});