@jaypie/mcp 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,15 @@
1
1
  ---
2
- description: DynamoDB patterns and queries
3
- related: aws, cdk, models
2
+ description: DynamoDB code patterns, key design, and queries
3
+ related: aws, cdk, models, tools-dynamodb
4
4
  ---
5
5
 
6
6
  # DynamoDB Patterns
7
7
 
8
8
  Best practices for DynamoDB with Jaypie applications.
9
9
 
10
- ## MCP DynamoDB Tools
10
+ ## MCP Tools
11
11
 
12
- ```
13
- aws_dynamodb_describe_table - Get table schema and indexes
14
- aws_dynamodb_query - Query by partition key (efficient)
15
- aws_dynamodb_scan - Full table scan (use sparingly)
16
- aws_dynamodb_get_item - Get single item by key
17
- ```
12
+ For interactive DynamoDB tools (query, scan, get-item, describe-table), see **tools-dynamodb**.
18
13
 
19
14
  ## Key Design
20
15
 
@@ -48,21 +43,7 @@ Use prefixed keys for multiple entity types:
48
43
  | List user orders | pk = USER#123, sk begins_with ORDER# |
49
44
  | Get specific order | pk = USER#123, sk = ORDER#2024-01-15#abc |
50
45
 
51
- ## Query Examples
52
-
53
- ### Query via MCP
54
-
55
- ```
56
- # Get user profile
57
- aws_dynamodb_get_item --tableName "MyTable" --key '{"pk":{"S":"USER#123"},"sk":{"S":"PROFILE"}}'
58
-
59
- # List user orders
60
- aws_dynamodb_query --tableName "MyTable" \
61
- --keyConditionExpression "pk = :pk AND begins_with(sk, :prefix)" \
62
- --expressionAttributeValues '{":pk":{"S":"USER#123"},":prefix":{"S":"ORDER#"}}'
63
- ```
64
-
65
- ### Query in Code
46
+ ## Query in Code
66
47
 
67
48
  ```typescript
68
49
  import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
@@ -98,10 +79,15 @@ const result = await client.send(new QueryCommand({
98
79
 
99
80
  Query pending orders:
100
81
 
101
- ```
102
- aws_dynamodb_query --tableName "MyTable" --indexName "gsi1" \
103
- --keyConditionExpression "gsi1pk = :status" \
104
- --expressionAttributeValues '{":status":{"S":"ORDER#pending"}}'
82
+ ```typescript
83
+ const result = await client.send(new QueryCommand({
84
+ TableName: process.env.CDK_ENV_TABLE,
85
+ IndexName: "gsi1",
86
+ KeyConditionExpression: "gsi1pk = :status",
87
+ ExpressionAttributeValues: {
88
+ ":status": "ORDER#pending",
89
+ },
90
+ }));
105
91
  ```
106
92
 
107
93
  ## CDK Table Definition
@@ -138,3 +124,26 @@ AWS_ACCESS_KEY_ID=local AWS_SECRET_ACCESS_KEY=local \
138
124
  --endpoint-url http://127.0.0.1:8000
139
125
  ```
140
126
 
127
+ ## Testing
128
+
129
+ Mock DynamoDB in tests:
130
+
131
+ ```typescript
132
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
133
+ import { vi } from "vitest";
134
+
135
+ vi.mock("@aws-sdk/client-dynamodb");
136
+
137
+ describe("OrderService", () => {
138
+ it("queries user orders", async () => {
139
+ vi.mocked(DynamoDBClient.prototype.send).mockResolvedValue({
140
+ Items: [{ pk: "USER#123", sk: "ORDER#abc" }],
141
+ });
142
+
143
+ const orders = await getOrders("123");
144
+
145
+ expect(orders).toHaveLength(1);
146
+ });
147
+ });
148
+ ```
149
+
@@ -0,0 +1,55 @@
1
+ ---
2
+ description: Submitting issues to Jaypie repositories
3
+ related: jaypie, agents
4
+ ---
5
+
6
+ # Jaypie Issues
7
+
8
+ ## Repository
9
+
10
+ Submit issues to: **github.com/finlaysonstudio/jaypie**
11
+
12
+ ## Agent Guidelines
13
+
14
+ **CRITICAL: Never submit issues without explicit user approval.**
15
+
16
+ Before creating an issue:
17
+ 1. Draft the issue title and body
18
+ 2. Present the draft to the user
19
+ 3. Wait for explicit approval ("yes", "submit it", etc.)
20
+ 4. Only then use `mcp__github__issue_write` to create the issue
21
+
22
+ ## Issue Format
23
+
24
+ ```markdown
25
+ **Title**: [Brief, descriptive title]
26
+
27
+ **Description**:
28
+ [What is the issue or feature request]
29
+
30
+ **Context**:
31
+ - Package: [affected package, e.g., `jaypie`, `@jaypie/express`]
32
+ - Version: [if known]
33
+ - Environment: [if relevant]
34
+
35
+ **Steps to Reproduce** (for bugs):
36
+ 1. ...
37
+ 2. ...
38
+
39
+ **Expected Behavior**:
40
+ [What should happen]
41
+
42
+ **Actual Behavior**:
43
+ [What actually happens]
44
+ ```
45
+
46
+ ## Labels
47
+
48
+ Common labels: `bug`, `enhancement`, `documentation`, `question`
49
+
50
+ ## When to Suggest Issues
51
+
52
+ - Bugs in Jaypie packages discovered during development
53
+ - Missing features that would benefit multiple projects
54
+ - Documentation gaps or errors
55
+ - API inconsistencies
package/skills/llm.md ADDED
@@ -0,0 +1,381 @@
1
+ ---
2
+ description: Using @jaypie/llm for unified LLM access
3
+ related: fabric, tools, tests
4
+ ---
5
+
6
+ # @jaypie/llm
7
+
8
+ Unified interface for calling LLM providers with multi-turn conversations, tool calling, streaming, and structured output.
9
+
10
+ ## Quick Start
11
+
12
+ ```typescript
13
+ import Llm from "@jaypie/llm";
14
+
15
+ // Auto-detect provider from model name
16
+ const response = await Llm.operate("What is 2+2?", { model: "claude-sonnet-4" });
17
+ console.log(response.content); // "4"
18
+ ```
19
+
20
+ ## Providers and Models
21
+
22
+ | Provider | Match Keywords | Default Model |
23
+ |----------|----------------|---------------|
24
+ | OpenAI | "openai", "gpt", /^o\d/ | gpt-5.1 |
25
+ | Anthropic | "anthropic", "claude", "haiku", "opus", "sonnet" | claude-sonnet-4-5 |
26
+ | Gemini | "gemini", "google" | gemini-3-pro-preview |
27
+ | OpenRouter | "openrouter" | z-ai/glm-4.7 |
28
+
29
+ ```typescript
30
+ // Provider auto-detected from model
31
+ await Llm.operate(input, { model: "gpt-5.1" }); // OpenAI
32
+ await Llm.operate(input, { model: "claude-opus-4" }); // Anthropic
33
+ await Llm.operate(input, { model: "gemini-3" }); // Gemini
34
+ ```
35
+
36
+ ## Core Methods
37
+
38
+ ### operate() - Multi-turn with Tools
39
+
40
+ The primary method for complex interactions:
41
+
42
+ ```typescript
43
+ const response = await Llm.operate(input, {
44
+ model: "gpt-5.1",
45
+ system: "You are a helpful assistant",
46
+ tools: toolkit,
47
+ turns: 5, // Allow up to 5 conversation turns
48
+ });
49
+
50
+ // Response structure
51
+ response.content; // string | object (structured output)
52
+ response.history; // LlmHistory - for follow-up calls
53
+ response.usage; // Token usage per turn
54
+ response.reasoning; // Extended thinking (if available)
55
+ response.status; // "completed" | "incomplete" | "in_progress"
56
+ ```
57
+
58
+ ### send() - Simple Completions
59
+
60
+ For single-shot text completions:
61
+
62
+ ```typescript
63
+ const response = await Llm.send("Explain REST APIs", {
64
+ model: "claude-sonnet-4",
65
+ system: "You are a technical writer",
66
+ });
67
+ ```
68
+
69
+ ### stream() - Streaming Responses
70
+
71
+ For real-time output:
72
+
73
+ ```typescript
74
+ for await (const chunk of Llm.stream("Tell me a story", { model: "gpt-5.1" })) {
75
+ switch (chunk.type) {
76
+ case "text":
77
+ process.stdout.write(chunk.content);
78
+ break;
79
+ case "tool_call":
80
+ console.log(`Tool: ${chunk.toolCall.name}`);
81
+ break;
82
+ case "done":
83
+ console.log("Usage:", chunk.usage);
84
+ break;
85
+ }
86
+ }
87
+ ```
88
+
89
+ ## Tool Calling
90
+
91
+ ### Define Tools with Toolkit
92
+
93
+ ```typescript
94
+ import Llm, { Toolkit } from "@jaypie/llm";
95
+
96
+ const toolkit = new Toolkit([
97
+ {
98
+ name: "get_weather",
99
+ description: "Get current weather for a city",
100
+ type: "function",
101
+ parameters: {
102
+ type: "object",
103
+ properties: {
104
+ city: { type: "string", description: "City name" },
105
+ },
106
+ required: ["city"],
107
+ },
108
+ call: async ({ city }) => {
109
+ // Actual implementation
110
+ return { temp: 72, conditions: "sunny" };
111
+ },
112
+ },
113
+ ]);
114
+
115
+ const response = await Llm.operate("What's the weather in NYC?", {
116
+ model: "gpt-5.1",
117
+ tools: toolkit,
118
+ });
119
+ ```
120
+
121
+ ### Built-in Tools
122
+
123
+ ```typescript
124
+ import Llm, { tools, JaypieToolkit } from "@jaypie/llm";
125
+
126
+ // Use individual tools
127
+ const response = await Llm.operate("Roll 2d6", {
128
+ model: "gpt-5.1",
129
+ tools, // Includes: random, roll, time, weather
130
+ });
131
+
132
+ // Or use the pre-configured toolkit
133
+ const toolkit = new JaypieToolkit();
134
+ ```
135
+
136
+ ### Fabric Services as Tools
137
+
138
+ ```typescript
139
+ import { fabricLlmTool } from "@jaypie/fabric";
140
+
141
+ const greetTool = fabricLlmTool(greetService);
142
+ const toolkit = new Toolkit([greetTool]);
143
+ ```
144
+
145
+ ## Structured Output
146
+
147
+ ### Natural Schema
148
+
149
+ ```typescript
150
+ const result = await Llm.operate("Extract contact info from: John Doe, john@example.com, 555-1234", {
151
+ model: "gpt-5.1",
152
+ format: {
153
+ name: String,
154
+ email: String,
155
+ phone: String,
156
+ },
157
+ });
158
+ // result.content = { name: "John Doe", email: "john@example.com", phone: "555-1234" }
159
+ ```
160
+
161
+ ### Zod Schema
162
+
163
+ ```typescript
164
+ import { z } from "zod";
165
+
166
+ const PersonSchema = z.object({
167
+ name: z.string(),
168
+ age: z.number(),
169
+ hobbies: z.array(z.string()),
170
+ });
171
+
172
+ const result = await Llm.operate("Parse: Alice is 30 and likes hiking and reading", {
173
+ model: "gpt-5.1",
174
+ format: PersonSchema,
175
+ });
176
+ ```
177
+
178
+ ## Conversation History
179
+
180
+ Continue conversations across calls:
181
+
182
+ ```typescript
183
+ const first = await Llm.operate("My name is Alice", { model: "gpt-5.1" });
184
+
185
+ const second = await Llm.operate("What's my name?", {
186
+ model: "gpt-5.1",
187
+ history: first.history,
188
+ });
189
+ // second.content = "Your name is Alice"
190
+ ```
191
+
192
+ ## Input with Files and Images
193
+
194
+ ```typescript
195
+ const response = await Llm.operate([
196
+ "Analyze these documents",
197
+ { file: "report.pdf", bucket: "my-bucket" },
198
+ { image: "chart.png" },
199
+ ], {
200
+ model: "claude-sonnet-4",
201
+ });
202
+ ```
203
+
204
+ ## Placeholders and Data
205
+
206
+ Substitute data into prompts:
207
+
208
+ ```typescript
209
+ const response = await Llm.operate("Summarize the article about {{topic}}", {
210
+ model: "gpt-5.1",
211
+ data: { topic: "climate change" },
212
+ placeholders: { input: true },
213
+ });
214
+ ```
215
+
216
+ ## Lifecycle Hooks
217
+
218
+ Monitor and react to LLM operations:
219
+
220
+ ```typescript
221
+ const response = await Llm.operate(input, {
222
+ model: "gpt-5.1",
223
+ hooks: {
224
+ beforeEachModelRequest: ({ providerRequest }) => {
225
+ console.log("Sending request...");
226
+ },
227
+ afterEachModelResponse: ({ content, usage }) => {
228
+ console.log(`Tokens: ${usage.total}`);
229
+ },
230
+ beforeEachTool: ({ toolName, args }) => {
231
+ console.log(`Calling ${toolName} with`, args);
232
+ },
233
+ afterEachTool: ({ result, toolName }) => {
234
+ console.log(`${toolName} returned:`, result);
235
+ },
236
+ onToolError: ({ error, toolName }) => {
237
+ console.error(`${toolName} failed:`, error);
238
+ },
239
+ },
240
+ });
241
+ ```
242
+
243
+ ## Fallback Providers
244
+
245
+ Configure a chain of fallback providers that automatically retry failed calls when the primary provider fails:
246
+
247
+ ```typescript
248
+ // Instance-level configuration
249
+ const llm = new Llm("anthropic", {
250
+ model: "claude-sonnet-4",
251
+ fallback: [
252
+ { provider: "openai", model: "gpt-4o" },
253
+ { provider: "gemini", model: "gemini-2.0-flash" },
254
+ ],
255
+ });
256
+
257
+ // Per-call override
258
+ const response = await llm.operate(input, {
259
+ fallback: [{ provider: "openai", model: "gpt-4o" }],
260
+ });
261
+
262
+ // Disable fallback for specific call
263
+ const response = await llm.operate(input, { fallback: false });
264
+
265
+ // Static method with fallback
266
+ const response = await Llm.operate(input, {
267
+ model: "claude-sonnet-4",
268
+ fallback: [{ provider: "openai", model: "gpt-4o" }],
269
+ });
270
+ ```
271
+
272
+ ### Fallback Response Metadata
273
+
274
+ ```typescript
275
+ response.provider; // Which provider handled the request
276
+ response.fallbackUsed; // true if a fallback was used
277
+ response.fallbackAttempts; // Number of providers tried (1 = primary only)
278
+ ```
279
+
280
+ ## Instance Usage
281
+
282
+ For repeated calls with same configuration:
283
+
284
+ ```typescript
285
+ const llm = new Llm("anthropic", {
286
+ model: "claude-sonnet-4",
287
+ system: "You are a code reviewer",
288
+ });
289
+
290
+ const review1 = await llm.operate(code1);
291
+ const review2 = await llm.operate(code2);
292
+ ```
293
+
294
+ ## Environment Variables
295
+
296
+ ```bash
297
+ OPENAI_API_KEY # Required for OpenAI
298
+ ANTHROPIC_API_KEY # Required for Anthropic
299
+ GOOGLE_API_KEY # Required for Gemini
300
+ OPENROUTER_API_KEY # Required for OpenRouter
301
+ ```
302
+
303
+ Keys are resolved via `getEnvSecret()` which supports AWS Secrets Manager.
304
+
305
+ ## Error Handling
306
+
307
+ The package auto-retries rate limits and transient errors:
308
+
309
+ ```typescript
310
+ try {
311
+ const response = await Llm.operate(input, { model: "gpt-5.1" });
312
+ if (response.status !== "completed") {
313
+ console.log("Incomplete:", response.error);
314
+ }
315
+ } catch (error) {
316
+ // Unrecoverable errors (auth, validation) throw
317
+ }
318
+ ```
319
+
320
+ ## Testing Pattern
321
+
322
+ ```typescript
323
+ import { describe, expect, it, vi } from "vitest";
324
+ import Llm, { Toolkit } from "@jaypie/llm";
325
+
326
+ describe("LLM Integration", () => {
327
+ it("calls tool and returns response", async () => {
328
+ const mockTool = {
329
+ name: "calculate",
330
+ description: "Perform calculation",
331
+ type: "function",
332
+ parameters: { type: "object", properties: { expr: { type: "string" } } },
333
+ call: vi.fn().mockResolvedValue("42"),
334
+ };
335
+
336
+ const toolkit = new Toolkit([mockTool]);
337
+ const response = await Llm.operate("Calculate 6*7", {
338
+ model: "gpt-5.1",
339
+ tools: toolkit,
340
+ });
341
+
342
+ expect(mockTool.call).toHaveBeenCalled();
343
+ expect(response.status).toBe("completed");
344
+ });
345
+ });
346
+ ```
347
+
348
+ ## Best Practices
349
+
350
+ 1. **Use operate() over send()** - More flexible, handles tools and structure
351
+ 2. **Set turns appropriately** - Default is 1; increase for tool-heavy workflows
352
+ 3. **Provide clear tool descriptions** - LLMs use these to decide which tool to call
353
+ 4. **Use structured output** - More reliable than parsing free text
354
+ 5. **Track usage** - Monitor `response.usage` for cost management
355
+ 6. **Handle incomplete status** - Check `response.status` for tool errors or limits
356
+ 7. **Configure fallbacks** - Set up fallback providers for resilience against outages
357
+
358
+ ## Common Options
359
+
360
+ ```typescript
361
+ interface LlmOperateOptions {
362
+ data?: Record<string, any>; // Placeholder substitution data
363
+ fallback?: LlmFallbackConfig[] | false; // Fallback provider chain
364
+ format?: JsonObject | ZodType; // Structured output schema
365
+ history?: LlmHistory; // Previous conversation
366
+ hooks?: LlmHooks; // Lifecycle callbacks
367
+ instructions?: string; // Additional instructions
368
+ model?: string; // Model override
369
+ system?: string; // System prompt
370
+ tools?: LlmTool[] | Toolkit; // Available tools
371
+ turns?: boolean | number; // Max conversation turns
372
+ user?: string; // User ID for logging
373
+ }
374
+
375
+ interface LlmFallbackConfig {
376
+ provider: string; // Provider name (e.g., "openai", "anthropic", "gemini")
377
+ model?: string; // Model to use (optional, uses provider default)
378
+ apiKey?: string; // API key (optional, uses environment variable)
379
+ }
380
+ ```
381
+
package/skills/secrets.md CHANGED
@@ -5,26 +5,52 @@ related: aws, cdk, variables
5
5
 
6
6
  # Secret Management
7
7
 
8
- Jaypie uses AWS Secrets Manager for secure credential storage.
8
+ Jaypie uses AWS Secrets Manager for secure credential storage, with environment-aware resolution that works seamlessly in both local development and Lambda.
9
9
 
10
10
  ## Basic Usage
11
11
 
12
+ Use `getEnvSecret` for environment-aware secret resolution:
13
+
12
14
  ```typescript
13
- import { getSecret } from "jaypie";
15
+ import { getEnvSecret } from "jaypie";
14
16
 
15
- const apiKey = await getSecret("my-api-key");
16
- const dbUri = await getSecret("mongodb-connection-string");
17
+ const apiKey = await getEnvSecret("ANTHROPIC_API_KEY");
18
+ const dbUri = await getEnvSecret("MONGODB_URI");
17
19
  ```
18
20
 
19
- ## Environment Variables
21
+ ### How getEnvSecret Works
22
+
23
+ `getEnvSecret` checks environment variables in order:
24
+
25
+ 1. `SECRET_{name}` - If found, fetches from AWS Secrets Manager
26
+ 2. `{name}_SECRET` - If found, fetches from AWS Secrets Manager
27
+ 3. `{name}` - Returns direct value without AWS call
28
+
29
+ This allows the same code to work locally (with direct env values) and in Lambda (with secret references).
30
+
31
+ ## Loading Multiple Secrets
32
+
33
+ Use `loadEnvSecrets` during handler initialization:
34
+
35
+ ```typescript
36
+ import { loadEnvSecrets } from "jaypie";
37
+
38
+ // Load secrets and set in process.env
39
+ await loadEnvSecrets("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "MONGODB_URI");
40
+
41
+ // Now available as process.env.ANTHROPIC_API_KEY, etc.
42
+ ```
43
+
44
+ ## CDK Configuration
20
45
 
21
46
  Reference secrets via environment variables in CDK:
22
47
 
23
48
  ```typescript
24
49
  const handler = new JaypieLambda(this, "Handler", {
25
50
  environment: {
26
- SECRET_MONGODB_URI: "mongodb-connection-string",
27
- SECRET_API_KEY: "third-party-api-key",
51
+ // SECRET_ prefix triggers AWS Secrets Manager fetch
52
+ SECRET_MONGODB_URI: "my-project/mongodb-uri",
53
+ SECRET_API_KEY: "my-project/third-party-api-key",
28
54
  },
29
55
  });
30
56
  ```
@@ -32,10 +58,23 @@ const handler = new JaypieLambda(this, "Handler", {
32
58
  In code:
33
59
 
34
60
  ```typescript
35
- const secretName = process.env.SECRET_MONGODB_URI;
36
- const mongoUri = await getSecret(secretName);
61
+ // getEnvSecret sees SECRET_MONGODB_URI and fetches from Secrets Manager
62
+ const mongoUri = await getEnvSecret("MONGODB_URI");
37
63
  ```
38
64
 
65
+ ## Direct Secret Access
66
+
67
+ Use `getSecret` when you need to fetch by exact AWS secret name:
68
+
69
+ ```typescript
70
+ import { getSecret } from "jaypie";
71
+
72
+ // Fetch by exact AWS secret name
73
+ const secret = await getSecret("my-project/production/api-key");
74
+ ```
75
+
76
+ Note: `getSecret` requires `AWS_SESSION_TOKEN` and always calls Secrets Manager. Prefer `getEnvSecret` for typical use cases.
77
+
39
78
  ## Creating Secrets
40
79
 
41
80
  ### Via CDK
@@ -86,7 +125,7 @@ aws secretsmanager create-secret \
86
125
  Retrieve in code:
87
126
 
88
127
  ```typescript
89
- const credentialsJson = await getSecret("my-project/db-credentials");
128
+ const credentialsJson = await getEnvSecret("DB_CREDENTIALS");
90
129
  const credentials = JSON.parse(credentialsJson);
91
130
  ```
92
131
 
@@ -96,10 +135,10 @@ Secrets are cached by default to reduce API calls:
96
135
 
97
136
  ```typescript
98
137
  // First call: fetches from Secrets Manager
99
- const key1 = await getSecret("api-key");
138
+ const key1 = await getEnvSecret("API_KEY");
100
139
 
101
140
  // Second call: returns cached value
102
- const key2 = await getSecret("api-key");
141
+ const key2 = await getEnvSecret("API_KEY");
103
142
  ```
104
143
 
105
144
  Cache is scoped to Lambda execution context (warm starts reuse cache).
@@ -125,19 +164,16 @@ secret.addRotationSchedule("Rotation", {
125
164
 
126
165
  ## Local Development
127
166
 
128
- For local development, use environment variables:
167
+ For local development, set environment variables directly:
129
168
 
130
169
  ```bash
131
170
  # .env.local (not committed)
171
+ ANTHROPIC_API_KEY=sk-ant-test123
132
172
  MONGODB_URI=mongodb://localhost:27017/dev
133
173
  API_KEY=test_key_123
134
174
  ```
135
175
 
136
- In code, check for direct value first:
137
-
138
- ```typescript
139
- const mongoUri = process.env.MONGODB_URI || await getSecret(process.env.SECRET_MONGODB_URI);
140
- ```
176
+ `getEnvSecret` automatically returns these values without AWS calls since there's no `SECRET_` prefix.
141
177
 
142
178
  ## IAM Permissions
143
179
 
@@ -153,3 +189,23 @@ lambdaFunction.addToRolePolicy(new PolicyStatement({
153
189
  }));
154
190
  ```
155
191
 
192
+ ## Testing
193
+
194
+ Mock secret functions in tests:
195
+
196
+ ```typescript
197
+ import { getEnvSecret } from "@jaypie/testkit/mock";
198
+ import { vi } from "vitest";
199
+
200
+ vi.mock("@jaypie/aws");
201
+
202
+ describe("Handler", () => {
203
+ it("uses API key from secrets", async () => {
204
+ vi.mocked(getEnvSecret).mockResolvedValue("test-api-key");
205
+
206
+ const result = await handler();
207
+
208
+ expect(getEnvSecret).toHaveBeenCalledWith("API_KEY");
209
+ });
210
+ });
211
+ ```
@@ -0,0 +1,23 @@
1
+ ---
2
+ description:
3
+ related:
4
+ agents: Prompts to improve agent adoption in AGENTS.md, CLAUDE.md, etc.
5
+ index: All available skills
6
+ ---
7
+
8
+ # Jaypie Skills
9
+
10
+ - Instruct Jaypie styles, techniques, and traditions
11
+ - Written for agents
12
+ - Balance brevity and rigor
13
+
14
+ Look up skills by their "alias:"
15
+ `mcp__jaypie__skills(index)`
16
+
17
+ ## Essentials
18
+
19
+ Contents: index, releasenotes
20
+ Development: documentation, errors, logs, mocks, style, tests
21
+ Infrastructure: aws, cdk, cicd, datadog, dns, dynamodb, secrets, variables
22
+ Patterns: fabric, models, services, vocabulary
23
+ Meta: issues, jaypie, skills, tools