@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.
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +3 -3
- package/skills/{pikku-ai-agent → pikku-agent}/SKILL.md +21 -21
- package/skills/pikku-ai-vercel/SKILL.md +18 -18
- package/skills/pikku-ai-voice/SKILL.md +15 -15
- package/skills/pikku-concepts/SKILL.md +5 -5
- package/skills/pikku-deploy-cloudflare/SKILL.md +9 -9
- package/skills/pikku-deploy-uws/SKILL.md +5 -2
- package/skills/pikku-deps/SKILL.md +42 -3
- package/skills/pikku-kysely/SKILL.md +68 -41
- package/skills/pikku-machine-auth/SKILL.md +10 -10
- package/skills/pikku-mcp/SKILL.md +19 -16
- package/skills/pikku-middleware/SKILL.md +14 -7
- package/skills/pikku-mongodb/SKILL.md +11 -11
- package/skills/pikku-n8n-import/references/addon-mapping.md +14 -8
- package/skills/pikku-product-second-opinion/SKILL.md +83 -73
- package/skills/pikku-rpc/SKILL.md +1 -1
- package/skills/pikku-scenario/SKILL.md +39 -31
- package/skills/pikku-software-archaeology/SKILL.md +27 -23
- package/skills/pikku-software-archaeology/references/blueprint.schema.json +580 -102
- package/skills/pikku-versioning/SKILL.md +87 -3
- package/skills/pikku-ws/SKILL.md +5 -2
|
@@ -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
|
|
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'
|
|
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
|
|
43
|
+
const rows = await kysely
|
|
44
|
+
.selectFrom('item')
|
|
44
45
|
.select(['id', 'name', 'quantity'])
|
|
45
46
|
.where('warehouseId', '=', warehouseId)
|
|
46
|
-
.orderBy('name')
|
|
47
|
+
.orderBy('name')
|
|
48
|
+
.limit(50)
|
|
49
|
+
.execute()
|
|
47
50
|
|
|
48
51
|
// JOINS + aliased selects (qualify columns once a join exists)
|
|
49
|
-
await kysely
|
|
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
|
|
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)
|
|
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
|
|
69
|
+
const created = await kysely
|
|
70
|
+
.insertInto('item')
|
|
65
71
|
.values({ name: input.name, warehouseId })
|
|
66
|
-
.returning(['id', 'name'])
|
|
72
|
+
.returning(['id', 'name'])
|
|
73
|
+
.executeTakeFirstOrThrow()
|
|
67
74
|
|
|
68
75
|
// UPDATE + RETURNING, DELETE
|
|
69
|
-
await kysely
|
|
70
|
-
.
|
|
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
|
|
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'))
|
|
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
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
91
|
-
|
|
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',
|
|
181
|
+
filename: 'app.db', // or ':memory:'
|
|
155
182
|
camelCase: true,
|
|
156
|
-
plugins: [],
|
|
157
|
-
functions: {},
|
|
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
|
|
179
|
-
|
|
|
180
|
-
| `*ChannelStore`
|
|
181
|
-
| `*EventHubStore`
|
|
182
|
-
| `*WorkflowService`
|
|
183
|
-
| `*WorkflowRunService`
|
|
184
|
-
| `*DeploymentService`
|
|
185
|
-
| `*
|
|
186
|
-
| `*AgentRunService`
|
|
187
|
-
| `*SecretService`
|
|
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
|
|
193
|
-
|
|
|
194
|
-
| `KyselySessionStore`
|
|
195
|
-
| `KyselyScopeService`
|
|
196
|
-
| `KyselyWebhookService`
|
|
197
|
-
| `KyselyCredentialService`
|
|
198
|
-
| `
|
|
199
|
-
| `KyselyWorkflowMirror`
|
|
200
|
-
| `KyselyAuditService`
|
|
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
|
|
20
|
-
|
|
21
|
-
| **Human** (CLI, dev)
|
|
22
|
-
| **Machine** (agent, sandbox, worker) | scoped API key
|
|
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,
|
|
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,
|
|
107
|
+
userId: sandboxRuntimeUserId, // a stable service user
|
|
108
108
|
name: `sandbox:${sandboxId}`,
|
|
109
|
-
expiresIn: 60 * 60,
|
|
110
|
-
metadata: { sandboxId },
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
|
41
|
-
|
|
|
42
|
-
| **Tool**
|
|
43
|
-
| **Resource** | `pikkuMCPResourceFunc`
|
|
44
|
-
| **Prompt**
|
|
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',
|
|
62
|
-
input: CreateTodoInput,
|
|
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', {
|
|
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
|
|
246
|
-
|
|
|
247
|
-
| `wireMCPTool` is not exported
|
|
248
|
-
| `uri`/`title` rejected on `pikkuMCPResourceFunc` | Those belong on `wireMCPResource`
|
|
249
|
-
| Resource returning `{ uri, blob, mimeType }`
|
|
250
|
-
| Client sees a tool with no description
|
|
251
|
-
| stdio client disconnects on the first log line
|
|
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])
|
|
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 })])
|
|
92
|
-
addGlobalMiddleware([telemetryInner({ environmentId: env.STAGE_ID })])
|
|
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('*', [
|
|
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
|
|
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) => {
|
|
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 =
|
|
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
|
|
53
|
-
|
|
|
54
|
-
| `MongoDBChannelStore`
|
|
55
|
-
| `MongoDBEventHubStore`
|
|
56
|
-
| `MongoDBWorkflowService`
|
|
57
|
-
| `MongoDBWorkflowRunService`
|
|
58
|
-
| `MongoDBDeploymentService`
|
|
59
|
-
| `
|
|
60
|
-
| `MongoDBAgentRunService`
|
|
61
|
-
| `MongoDBSecretService`
|
|
62
|
-
| `MongoDBSessionStore`
|
|
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": {
|
|
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
|
|
39
|
-
|
|
40
|
-
| `n8n-nodes-base.gmail` / `gmailTool`
|
|
41
|
-
| `n8n-nodes-base.slack` / `slackTool`
|
|
42
|
-
| `n8n-nodes-base.googleSheets` / `…Tool`
|
|
43
|
-
| `n8n-nodes-base.notion` / `notionTool`
|
|
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'`).
|