@pikku/skills 0.12.9 → 0.12.10

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.
@@ -28,25 +28,29 @@ Use this skill as an execution checklist, not reference material.
28
28
 
29
29
  ## Writing Queries — the Kysely query builder
30
30
 
31
- In a Pikku function body the injected `kysely` IS the `Kysely<DB>` instance — query it directly. Every connection factory below wires the **CamelCasePlugin** by default, so you write **camelCase everywhere in TS** (columns, aliases) and raw **snake_case ONLY inside a `` sql`` `` literal**. If a project opted out (`createNodeSqliteKysely({ camelCase: false })`), that inverts — check how the instance was built before assuming. Kysely is a query builder, NOT an ORM — there are no relations; shape nested data with the JSON helpers below. Never hand-roll SQL strings; never annotate the return type (in Pikku the output zod schema IS the type).
31
+ In a Pikku function body the injected `kysely` IS the `Kysely<DB>` instance — query it directly. Every connection factory below wires the **CamelCasePlugin** by default, so you write **camelCase everywhere in TS** (columns, aliases) and raw **snake_case ONLY inside a ` sql` `` literal**. If a project opted out (`createNodeSqliteKysely({ camelCase: false })`), that inverts — check how the instance was built before assuming. Kysely is a query builder, NOT an ORM — there are no relations; shape nested data with the JSON helpers below. Never hand-roll SQL strings; never annotate the return type (in Pikku the output zod schema IS the type).
32
32
 
33
33
  ```typescript
34
34
  import { sql } from 'kysely'
35
35
  // Relation helpers are ENGINE-SPECIFIC — import the matching path:
36
- import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/sqlite' // SQLite / libSQL
36
+ import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/sqlite' // SQLite / libSQL
37
37
  // import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres' // Postgres
38
38
  ```
39
39
 
40
40
  ```typescript
41
41
  // SELECT + where/orderBy/limit/offset. Terminals: .execute() | .executeTakeFirst()
42
42
  // | .executeTakeFirstOrThrow(() => new NotFoundError()) — pass an error factory.
43
- const rows = await kysely.selectFrom('item')
43
+ const rows = await kysely
44
+ .selectFrom('item')
44
45
  .select(['id', 'name', 'quantity'])
45
46
  .where('warehouseId', '=', warehouseId)
46
- .orderBy('name').limit(50).execute()
47
+ .orderBy('name')
48
+ .limit(50)
49
+ .execute()
47
50
 
48
51
  // JOINS + aliased selects (qualify columns once a join exists)
49
- await kysely.selectFrom('stock')
52
+ await kysely
53
+ .selectFrom('stock')
50
54
  .innerJoin('item', 'item.id', 'stock.itemId')
51
55
  .leftJoin('bin as b', 'b.id', 'stock.binId')
52
56
  .select(['stock.id', 'item.name as itemName', 'b.code as binCode'])
@@ -54,41 +58,64 @@ await kysely.selectFrom('stock')
54
58
 
55
59
  // AGGREGATES via the fn helper + groupBy/having. eb.fn.count returns string|number —
56
60
  // cast if you need a JS number (SQLite: CAST(... AS INTEGER)).
57
- await kysely.selectFrom('stock')
61
+ await kysely
62
+ .selectFrom('stock')
58
63
  .select((eb) => ['itemId', eb.fn.sum<number>('quantity').as('onHand')])
59
64
  .groupBy('itemId')
60
- .having((eb) => eb.fn.sum('quantity'), '<', 10) // low-stock
65
+ .having((eb) => eb.fn.sum('quantity'), '<', 10) // low-stock
61
66
  .execute()
62
67
 
63
68
  // INSERT + RETURNING (one round-trip; works on SQLite & Postgres)
64
- const created = await kysely.insertInto('item')
69
+ const created = await kysely
70
+ .insertInto('item')
65
71
  .values({ name: input.name, warehouseId })
66
- .returning(['id', 'name']).executeTakeFirstOrThrow()
72
+ .returning(['id', 'name'])
73
+ .executeTakeFirstOrThrow()
67
74
 
68
75
  // UPDATE + RETURNING, DELETE
69
- await kysely.updateTable('item').set({ quantity: input.quantity })
70
- .where('id', '=', input.id).returning(['id', 'quantity']).executeTakeFirstOrThrow()
76
+ await kysely
77
+ .updateTable('item')
78
+ .set({ quantity: input.quantity })
79
+ .where('id', '=', input.id)
80
+ .returning(['id', 'quantity'])
81
+ .executeTakeFirstOrThrow()
71
82
  await kysely.deleteFrom('item').where('id', '=', input.id).execute()
72
83
 
73
84
  // EXPRESSION BUILDER for and/or; $if for conditional building; sql for raw fragments
74
- await kysely.selectFrom('item')
85
+ await kysely
86
+ .selectFrom('item')
75
87
  .selectAll()
76
88
  .where((eb) => eb.or([eb('quantity', '=', 0), eb('discontinued', '=', true)]))
77
89
  .$if(!!input.search, (qb) => qb.where('name', 'like', `%${input.search}%`))
78
- .select(sql<number>`quantity * unit_cost`.as('value')) // snake_case ok inside sql``
90
+ .select(sql<number>`quantity * unit_cost`.as('value')) // snake_case ok inside sql``
79
91
  .execute()
80
92
 
81
93
  // NESTED DATA (no relations) — jsonObjectFrom (one) / jsonArrayFrom (many)
82
- await kysely.selectFrom('warehouse')
83
- .select((eb) => ['warehouse.id', 'warehouse.name',
84
- jsonArrayFrom(eb.selectFrom('bin').select(['bin.id', 'bin.code'])
85
- .whereRef('bin.warehouseId', '=', 'warehouse.id')).as('bins')])
94
+ await kysely
95
+ .selectFrom('warehouse')
96
+ .select((eb) => [
97
+ 'warehouse.id',
98
+ 'warehouse.name',
99
+ jsonArrayFrom(
100
+ eb
101
+ .selectFrom('bin')
102
+ .select(['bin.id', 'bin.code'])
103
+ .whereRef('bin.warehouseId', '=', 'warehouse.id')
104
+ ).as('bins'),
105
+ ])
86
106
  .execute()
87
107
 
88
108
  // TRANSACTION — multi-write atomicity. Use trx (not kysely) inside.
89
109
  await kysely.transaction().execute(async (trx) => {
90
- await trx.updateTable('stock').set({ quantity: 0 }).where('itemId', '=', id).execute()
91
- await trx.insertInto('stockMove').values({ itemId: id, delta: -qty }).execute()
110
+ await trx
111
+ .updateTable('stock')
112
+ .set({ quantity: 0 })
113
+ .where('itemId', '=', id)
114
+ .execute()
115
+ await trx
116
+ .insertInto('stockMove')
117
+ .values({ itemId: id, delta: -qty })
118
+ .execute()
92
119
  })
93
120
  ```
94
121
 
@@ -151,10 +178,10 @@ import { createNodeSqliteKysely } from '@pikku/kysely-node-sqlite'
151
178
 
152
179
  // Your application DB — CamelCasePlugin on by default
153
180
  const kysely = createNodeSqliteKysely<DB>({
154
- filename: 'app.db', // or ':memory:'
181
+ filename: 'app.db', // or ':memory:'
155
182
  camelCase: true,
156
- plugins: [], // layered on top
157
- functions: {}, // scalar UDFs, registered as deterministic (Node only)
183
+ plugins: [], // layered on top
184
+ functions: {}, // scalar UDFs, registered as deterministic (Node only)
158
185
  })
159
186
  ```
160
187
 
@@ -175,29 +202,29 @@ functions query.
175
202
 
176
203
  Each database variant exports these services with a prefix (`Pg`, `MySQL`, `SQLite`, or base `Kysely`):
177
204
 
178
- | Service | Interface | Purpose |
179
- | --------------------- | ------------------------------------- | ---------------------------------------------- |
180
- | `*ChannelStore` | `ChannelStore` | WebSocket channel state persistence |
181
- | `*EventHubStore` | `EventHubStore` | Event hub state persistence |
182
- | `*WorkflowService` | `PikkuWorkflowService` | Workflow definition storage |
183
- | `*WorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |
184
- | `*DeploymentService` | `DeploymentService` | Deployment state management |
185
- | `*AIStorageService` | `AIStorageService, AIRunStateService` | AI conversation/run storage |
186
- | `*AgentRunService` | `AgentRunService` | Agent execution tracking |
187
- | `*SecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
205
+ | Service | Interface | Purpose |
206
+ | ---------------------- | ------------------------------------------- | ---------------------------------------------- |
207
+ | `*ChannelStore` | `ChannelStore` | WebSocket channel state persistence |
208
+ | `*EventHubStore` | `EventHubStore` | Event hub state persistence |
209
+ | `*WorkflowService` | `PikkuWorkflowService` | Workflow definition storage |
210
+ | `*WorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |
211
+ | `*DeploymentService` | `DeploymentService` | Deployment state management |
212
+ | `*AgentStorageService` | `AgentStorageService, AgentRunStateService` | AI conversation/run storage |
213
+ | `*AgentRunService` | `AgentRunService` | Agent execution tracking |
214
+ | `*SecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
188
215
 
189
216
  A handful more live only on the base package — there is no `Pg`/`MySQL`/`SQLite`
190
217
  variant to reach for, you import them from `@pikku/kysely` whatever the engine:
191
218
 
192
- | Service | Purpose |
193
- | -------------------------- | --------------------------------------------- |
194
- | `KyselySessionStore` | Persisted user sessions |
195
- | `KyselyScopeService` | Scope and role storage |
196
- | `KyselyWebhookService` | Webhook registrations and deliveries |
197
- | `KyselyCredentialService` | Encrypted third-party credentials |
198
- | `KyselyAIRunStateService` | AI run state (also implemented by AIStorage) |
199
- | `KyselyWorkflowMirror` | Mirrors workflow runs into queryable tables |
200
- | `KyselyAuditService` | Durable audit sink (see `pikku-audit`) |
219
+ | Service | Purpose |
220
+ | ---------------------------- | -------------------------------------------- |
221
+ | `KyselySessionStore` | Persisted user sessions |
222
+ | `KyselyScopeService` | Scope and role storage |
223
+ | `KyselyWebhookService` | Webhook registrations and deliveries |
224
+ | `KyselyCredentialService` | Encrypted third-party credentials |
225
+ | `KyselyAgentRunStateService` | AI run state (also implemented by AIStorage) |
226
+ | `KyselyWorkflowMirror` | Mirrors workflow runs into queryable tables |
227
+ | `KyselyAuditService` | Durable audit sink (see `pikku-audit`) |
201
228
 
202
229
  All services take a `Kysely<KyselyPikkuDB>` instance in their constructor and have an `init()` method that creates tables if needed.
203
230
 
@@ -16,10 +16,10 @@ description: >-
16
16
  Unified authentication for humans **and** machines against a Pikku + better-auth
17
17
  server. Two paths, two headers, one resolver:
18
18
 
19
- | Caller | Credential | Header | Obtained by |
20
- |---|---|---|---|
21
- | **Human** (CLI, dev) | better-auth session token | `Authorization: Bearer <token>` | `pikku login` (device flow) → `~/.pikku/session.json` |
22
- | **Machine** (agent, sandbox, worker) | scoped API key | `x-api-key: <key>` | `createApiKey` (server-side, at provision/spawn) |
19
+ | Caller | Credential | Header | Obtained by |
20
+ | ------------------------------------ | ------------------------- | ------------------------------- | ----------------------------------------------------- |
21
+ | **Human** (CLI, dev) | better-auth session token | `Authorization: Bearer <token>` | `pikku login` (device flow) → `~/.pikku/session.json` |
22
+ | **Machine** (agent, sandbox, worker) | scoped API key | `x-api-key: <key>` | `createApiKey` (server-side, at provision/spawn) |
23
23
 
24
24
  Both resolve to a Pikku `UserSession` through one middleware:
25
25
  `betterAuthSession({ mapSession, apiKey: { mapKey } })`.
@@ -83,7 +83,7 @@ import { apiKey } from '@better-auth/api-key'
83
83
  betterAuth({
84
84
  plugins: [
85
85
  apiKey({
86
- enableMetadata: true, // REQUIRED to store scope on the key
86
+ enableMetadata: true, // REQUIRED to store scope on the key
87
87
  enableSessionForAPIKeys: true, // lets a key resolve via getSession too
88
88
  }),
89
89
  ],
@@ -104,10 +104,10 @@ one for a non-existent `userId` is created but will not resolve.
104
104
  // `auth` is the better-auth instance (injected service)
105
105
  const { key } = await auth.api.createApiKey({
106
106
  body: {
107
- userId: sandboxRuntimeUserId, // a stable service user
107
+ userId: sandboxRuntimeUserId, // a stable service user
108
108
  name: `sandbox:${sandboxId}`,
109
- expiresIn: 60 * 60, // seconds
110
- metadata: { sandboxId }, // keep only STABLE ids here
109
+ expiresIn: 60 * 60, // seconds
110
+ metadata: { sandboxId }, // keep only STABLE ids here
111
111
  permissions: { sandbox: ['read', 'write'] },
112
112
  },
113
113
  })
@@ -159,7 +159,7 @@ When the api-key header is present it is authoritative — the middleware never
159
159
  falls through to `getSession` (a bare mock session would shadow the scoped one).
160
160
  When it is absent, the human `getSession` path runs as normal. Either way the
161
161
  middleware bails out entirely if a session is already set, and it checks the
162
- *live* session rather than the wire's construction-time snapshot, so it can't
162
+ _live_ session rather than the wire's construction-time snapshot, so it can't
163
163
  clobber one an earlier middleware resolved.
164
164
 
165
165
  ### Restricting a key below its owner
@@ -182,7 +182,7 @@ power — the restriction lives on the key, not on a proliferation of identities
182
182
  ### Failure handling is deliberately split
183
183
 
184
184
  A key that fails to verify is logged and treated as an ordinary "not
185
- authenticated" — an unusable credential is not an outage. A failure *inside*
185
+ authenticated" — an unusable credential is not an outage. A failure _inside_
186
186
  `mapKey` (your scope store is down) propagates as a real error instead. That
187
187
  asymmetry is on purpose: a scope lookup that silently failed would serve the
188
188
  request anonymously, which is exactly the wrong direction to fail in.
@@ -6,7 +6,7 @@ description: >-
6
6
  wireMCPPrompt, the MCP wire object and PikkuMCPServer. TRIGGER when: code uses mcp: true or any
7
7
  pikkuMCP*Func/wireMCP* helper, user asks about MCP, Model Context Protocol, AI tool integration,
8
8
  or exposing functions to Claude/ChatGPT. DO NOT TRIGGER when: user asks about AI agents (use
9
- pikku-ai-agent) or general function definitions (use pikku-concepts).
9
+ pikku-agent) or general function definitions (use pikku-concepts).
10
10
  installGroups: [core]
11
11
  ---
12
12
 
@@ -37,11 +37,11 @@ See `pikku-concepts` for the core mental model.
37
37
 
38
38
  MCP has three surfaces, and Pikku wires them differently:
39
39
 
40
- | Surface | Function factory | Wiring | Return type |
41
- | --- | --- | --- | --- |
42
- | **Tool** | `mcp: true` on a `pikkuFunc`, or `pikkuMCPToolFunc` | none — the function *is* the registration | the func's own output, or MCP content blocks |
43
- | **Resource** | `pikkuMCPResourceFunc` | `wireMCPResource({ uri, title, … })` | `Array<{ uri, text }>` |
44
- | **Prompt** | `pikkuMCPPromptFunc` | `wireMCPPrompt({ name, description, … })` | `Array<MCPPromptMessage>` |
40
+ | Surface | Function factory | Wiring | Return type |
41
+ | ------------ | --------------------------------------------------- | ----------------------------------------- | -------------------------------------------- |
42
+ | **Tool** | `mcp: true` on a `pikkuFunc`, or `pikkuMCPToolFunc` | none — the function _is_ the registration | the func's own output, or MCP content blocks |
43
+ | **Resource** | `pikkuMCPResourceFunc` | `wireMCPResource({ uri, title, … })` | `Array<{ uri, text }>` |
44
+ | **Prompt** | `pikkuMCPPromptFunc` | `wireMCPPrompt({ name, description, … })` | `Array<MCPPromptMessage>` |
45
45
 
46
46
  Tools are the odd one out — there is no `wireMCPTool`. Resources and prompts
47
47
  carry protocol metadata (a URI template, a prompt name) that belongs to the
@@ -58,8 +58,8 @@ Add `mcp: true` to any existing function:
58
58
 
59
59
  ```typescript
60
60
  export const createTodo = pikkuFunc({
61
- description: 'Create a new todo item', // becomes the MCP tool description
62
- input: CreateTodoInput, // becomes the MCP tool input schema
61
+ description: 'Create a new todo item', // becomes the MCP tool description
62
+ input: CreateTodoInput, // becomes the MCP tool input schema
63
63
  output: CreateTodoOutput,
64
64
  mcp: true,
65
65
  func: async ({ db }, { text, priority }) => db.createTodo({ text, priority }),
@@ -141,7 +141,10 @@ import { pikkuMCPPromptFunc, wireMCPPrompt } from '#pikku'
141
141
  export const planDayPrompt = pikkuMCPPromptFunc({
142
142
  input: UserIdInputSchema,
143
143
  func: async (_services, { userId }, { rpc }) => {
144
- const { todos } = await rpc.invoke('listTodos', { userId, completed: false })
144
+ const { todos } = await rpc.invoke('listTodos', {
145
+ userId,
146
+ completed: false,
147
+ })
145
148
  return [
146
149
  {
147
150
  role: 'user' as const,
@@ -242,10 +245,10 @@ anything logs.
242
245
 
243
246
  ## Red flags
244
247
 
245
- | Symptom | Cause |
246
- | --- | --- |
247
- | `wireMCPTool` is not exported | There is no tool wiring — use `mcp: true` or `pikkuMCPToolFunc` |
248
- | `uri`/`title` rejected on `pikkuMCPResourceFunc` | Those belong on `wireMCPResource` |
249
- | Resource returning `{ uri, blob, mimeType }` | Resources are text only: `{ uri, text }` |
250
- | Client sees a tool with no description | `mcp: true` without a `description` — check the codegen warning |
251
- | stdio client disconnects on the first log line | Logger still writing to stdout; use `createMCPLogger()` |
248
+ | Symptom | Cause |
249
+ | ------------------------------------------------ | --------------------------------------------------------------- |
250
+ | `wireMCPTool` is not exported | There is no tool wiring — use `mcp: true` or `pikkuMCPToolFunc` |
251
+ | `uri`/`title` rejected on `pikkuMCPResourceFunc` | Those belong on `wireMCPResource` |
252
+ | Resource returning `{ uri, blob, mimeType }` | Resources are text only: `{ uri, text }` |
253
+ | Client sees a tool with no description | `mcp: true` without a `description` — check the codegen warning |
254
+ | stdio client disconnects on the first log line | Logger still writing to stdout; use `createMCPLogger()` |
@@ -48,6 +48,7 @@ const telemetryMiddleware = pikkuMiddleware({
48
48
  ```
49
49
 
50
50
  The `wire` object gives you:
51
+
51
52
  - `wire.http` — inbound HTTP context (headers, URL, cookies)
52
53
  - `wire.setSession(session)` — set the session for this request
53
54
  - `wire.getSession()` — read the current session
@@ -70,7 +71,7 @@ addHTTPMiddleware('*', [cors(), authBearer()])
70
71
  addHTTPMiddleware('/admin/*', [auditLog])
71
72
 
72
73
  // 4. Tag-based: any wiring with matching tag
73
- addTagMiddleware('machine-agent', [bearerAuth]) // tag on function or wire
74
+ addTagMiddleware('machine-agent', [bearerAuth]) // tag on function or wire
74
75
 
75
76
  // 5. Inline: per-wiring
76
77
  wireHTTP({
@@ -88,8 +89,8 @@ Runs before everything else, across every wire type: HTTP, Queue, Channel, Trigg
88
89
  import { addGlobalMiddleware } from '@pikku/core'
89
90
  import { telemetryOuter, telemetryInner } from '@pikku/core/middleware'
90
91
 
91
- addGlobalMiddleware([telemetryOuter({ environmentId: env.STAGE_ID })]) // wraps the full call
92
- addGlobalMiddleware([telemetryInner({ environmentId: env.STAGE_ID })]) // closest to the function body
92
+ addGlobalMiddleware([telemetryOuter({ environmentId: env.STAGE_ID })]) // wraps the full call
93
+ addGlobalMiddleware([telemetryInner({ environmentId: env.STAGE_ID })]) // closest to the function body
93
94
  ```
94
95
 
95
96
  `telemetryOuter` ships with `priority: 'highest'`, `telemetryInner` with `priority: 'lowest'` — so priority sorting places outer first regardless of array/call order.
@@ -101,7 +102,9 @@ import { addHTTPMiddleware } from '@pikku/core/http'
101
102
  import { cors, authBearer } from '@pikku/core/middleware'
102
103
 
103
104
  // All routes
104
- addHTTPMiddleware('*', [cors({ origin: 'https://app.example.com', credentials: true })])
105
+ addHTTPMiddleware('*', [
106
+ cors({ origin: 'https://app.example.com', credentials: true }),
107
+ ])
105
108
 
106
109
  // Scoped to /api/* prefix
107
110
  addHTTPMiddleware('/api/*', [rateLimit({ maxRequests: 100, windowMs: 60_000 })])
@@ -161,7 +164,7 @@ highest → high → medium (default) → low → lowest
161
164
 
162
165
  **Priority is the primary key across every scope, not within one.** The collected
163
166
  list is flattened first and sorted once, so a `priority: 'lowest'` global
164
- middleware runs *after* an inline per-route middleware of default priority — the
167
+ middleware runs _after_ an inline per-route middleware of default priority — the
165
168
  narrower scope does not win. Scope order survives only as the tiebreaker between
166
169
  middleware of equal priority, because the sort is stable.
167
170
 
@@ -190,7 +193,9 @@ A server that exposes RPCs only to a trusted caller (e.g. an API calling a machi
190
193
  ```typescript
191
194
  // lib/host-token.ts
192
195
  let _token: string | null = null
193
- export const setToken = (t: string) => { _token = t }
196
+ export const setToken = (t: string) => {
197
+ _token = t
198
+ }
194
199
  export const getToken = () => _token
195
200
  ```
196
201
 
@@ -202,7 +207,9 @@ import { UnauthorizedError } from '@pikku/core/errors'
202
207
  import { getToken } from '../lib/host-token.js'
203
208
 
204
209
  const bearerAuth = pikkuMiddleware(async (_services, { http }, next) => {
205
- const authHeader = http?.request?.header?.('authorization') || http?.request?.header?.('Authorization')
210
+ const authHeader =
211
+ http?.request?.header?.('authorization') ||
212
+ http?.request?.header?.('Authorization')
206
213
  const token = getToken()
207
214
  const expected = token ? `Bearer ${token}` : null
208
215
  if (
@@ -49,17 +49,17 @@ await mongo.close()
49
49
 
50
50
  ### Available Services
51
51
 
52
- | Service | Interface | Purpose |
53
- | --------------------------- | ------------------------------------- | ---------------------------------------------- |
54
- | `MongoDBChannelStore` | `ChannelStore` | WebSocket channel state persistence |
55
- | `MongoDBEventHubStore` | `EventHubStore` | Event hub state persistence |
56
- | `MongoDBWorkflowService` | `PikkuWorkflowService` | Workflow definition storage |
57
- | `MongoDBWorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |
58
- | `MongoDBDeploymentService` | `DeploymentService` | Deployment state management |
59
- | `MongoDBAIStorageService` | `AIStorageService, AIRunStateService` | AI conversation/run storage |
60
- | `MongoDBAgentRunService` | `AgentRunService` | Agent execution tracking |
61
- | `MongoDBSecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
62
- | `MongoDBSessionStore` | `SessionStore` | Persisted user sessions |
52
+ | Service | Interface | Purpose |
53
+ | ---------------------------- | ------------------------------------------- | ---------------------------------------------- |
54
+ | `MongoDBChannelStore` | `ChannelStore` | WebSocket channel state persistence |
55
+ | `MongoDBEventHubStore` | `EventHubStore` | Event hub state persistence |
56
+ | `MongoDBWorkflowService` | `PikkuWorkflowService` | Workflow definition storage |
57
+ | `MongoDBWorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |
58
+ | `MongoDBDeploymentService` | `DeploymentService` | Deployment state management |
59
+ | `MongoDBAgentStorageService` | `AgentStorageService, AgentRunStateService` | AI conversation/run storage |
60
+ | `MongoDBAgentRunService` | `AgentRunService` | Agent execution tracking |
61
+ | `MongoDBSecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
62
+ | `MongoDBSessionStore` | `SessionStore` | Persisted user sessions |
63
63
 
64
64
  All services take a `Db` instance in their constructor and have an `init()` method that creates collections/indexes.
65
65
 
@@ -18,9 +18,11 @@ rewrite the stub.
18
18
  "n8nType": "n8n-nodes-base.gmailTool",
19
19
  "n8nName": "Send a message in Gmail",
20
20
  "parameters": { "sendTo": "...", "message": "...", "subject": "..." },
21
- "credentials": { "gmailOAuth2": { "id": "...", "name": "Personal Gmail" } },
21
+ "credentials": {
22
+ "gmailOAuth2": { "id": "...", "name": "Personal Gmail" },
23
+ },
22
24
  "isAgentTool": true,
23
- "agentName": "Inbox Assistant"
25
+ "agentName": "Inbox Assistant",
24
26
  }
25
27
  ```
26
28
  2. **Installed addons** — `@pikku/addon-*` in the project's `package.json`
@@ -35,12 +37,12 @@ rewrite the stub.
35
37
  Map `n8nType` to a package by reading its source. Common shapes (**guesses, not
36
38
  authoritative** — always verify against installed source):
37
39
 
38
- | n8n type prefix | typical addon candidate |
39
- |---|---|
40
- | `n8n-nodes-base.gmail` / `gmailTool` | `@pikku/addon-email-gmail` |
41
- | `n8n-nodes-base.slack` / `slackTool` | `@pikku/addon-chat-slack` |
42
- | `n8n-nodes-base.googleSheets` / `…Tool` | `@pikku/addon-sheets-google` |
43
- | `n8n-nodes-base.notion` / `notionTool` | `@pikku/addon-docs-notion` |
40
+ | n8n type prefix | typical addon candidate |
41
+ | ------------------------------------------ | ---------------------------- |
42
+ | `n8n-nodes-base.gmail` / `gmailTool` | `@pikku/addon-email-gmail` |
43
+ | `n8n-nodes-base.slack` / `slackTool` | `@pikku/addon-chat-slack` |
44
+ | `n8n-nodes-base.googleSheets` / `…Tool` | `@pikku/addon-sheets-google` |
45
+ | `n8n-nodes-base.notion` / `notionTool` | `@pikku/addon-docs-notion` |
44
46
  | `n8n-nodes-base.telegram` / `telegramTool` | `@pikku/addon-chat-telegram` |
45
47
 
46
48
  If no installed addon plausibly covers the n8n type, stop and report it — do not
@@ -74,6 +76,7 @@ over guessing.
74
76
  Two outcomes, by `isAgentTool`:
75
77
 
76
78
  **A) `isAgentTool: true`** — the stub is an agent tool referenced via `ref()`:
79
+
77
80
  1. Delete the stub file.
78
81
  2. In the agent file, replace `ref('agentGmailtool__sendAMessageInGmail')` in
79
82
  `tools: [...]` with `ref('messageSend')` (the resolved addon function).
@@ -81,13 +84,16 @@ Two outcomes, by `isAgentTool`:
81
84
  `node_modules/@pikku/addon-*`).
82
85
 
83
86
  If you can't delete safely, leave a one-line re-export instead of a stub:
87
+
84
88
  ```ts
85
89
  import { messageSend } from '@pikku/addon-email-gmail'
86
90
  export const agentGmailtool__sendAMessageInGmail = messageSend
87
91
  ```
92
+
88
93
  Default is delete + retarget; wrappers add maintenance burden.
89
94
 
90
95
  **B) `isAgentTool: false`** — the stub is a graph node:
96
+
91
97
  1. Open `<workflow>.graph.ts`.
92
98
  2. In `nodes: { … }` find the entry whose value is the stub rpc name.
93
99
  3. Replace it with the addon function name (`'messageSend'`).