@pikku/core 0.12.92 → 0.12.93

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/errors/error-handler.d.ts +11 -0
  3. package/dist/errors/error-handler.js +8 -0
  4. package/dist/errors/serialized-error.d.ts +3 -0
  5. package/dist/function/function-runner.d.ts +1 -1
  6. package/dist/function/function-runner.js +3 -3
  7. package/dist/function/functions.types.d.ts +33 -21
  8. package/dist/middleware/auth-apikey.d.ts +7 -0
  9. package/dist/middleware/auth-apikey.js +6 -0
  10. package/dist/middleware/auth-bearer.d.ts +3 -0
  11. package/dist/middleware/auth-bearer.js +2 -0
  12. package/dist/middleware/auth-cookie.d.ts +9 -0
  13. package/dist/middleware/auth-cookie.js +6 -4
  14. package/dist/middleware/cors.d.ts +8 -0
  15. package/dist/middleware/cors.js +2 -0
  16. package/dist/middleware/middleware-factories.d.ts +14 -0
  17. package/dist/middleware/middleware-factories.js +14 -0
  18. package/dist/services/local-secrets.js +1 -1
  19. package/dist/services/personas-service.d.ts +4 -0
  20. package/dist/services/personas-service.js +4 -0
  21. package/dist/services/secret-service.d.ts +0 -1
  22. package/dist/testing/service-tests/secret-service-tests.js +1 -1
  23. package/dist/types/core.types.d.ts +5 -0
  24. package/dist/wirings/addon/wire-addon.d.ts +17 -15
  25. package/dist/wirings/addon/wire-addon.js +6 -0
  26. package/dist/wirings/addon/wire-remote-addon.d.ts +4 -0
  27. package/dist/wirings/addon/wire-remote-addon.js +4 -0
  28. package/dist/wirings/agent/agent-prepare.js +2 -2
  29. package/dist/wirings/agent/agent.types.d.ts +31 -27
  30. package/dist/wirings/agent-scorer/agent-scorer.d.ts +4 -0
  31. package/dist/wirings/agent-scorer/agent-scorer.js +4 -0
  32. package/dist/wirings/channel/channel.types.d.ts +11 -0
  33. package/dist/wirings/cli/cli.types.d.ts +43 -1
  34. package/dist/wirings/cli/define-cli-commands.d.ts +6 -1
  35. package/dist/wirings/cli/define-cli-commands.js +6 -1
  36. package/dist/wirings/credential/credential.types.d.ts +7 -0
  37. package/dist/wirings/gateway/gateway.types.d.ts +23 -0
  38. package/dist/wirings/http/http.types.d.ts +51 -37
  39. package/dist/wirings/mcp/mcp.types.d.ts +27 -0
  40. package/dist/wirings/queue/queue.types.d.ts +6 -0
  41. package/dist/wirings/scheduler/scheduler.types.d.ts +5 -0
  42. package/dist/wirings/secret/secret.types.d.ts +9 -14
  43. package/dist/wirings/secret/secret.types.js +7 -0
  44. package/dist/wirings/trigger/trigger.types.d.ts +4 -1
  45. package/dist/wirings/variable/variable.types.d.ts +14 -13
  46. package/dist/wirings/variable/variable.types.js +7 -0
  47. package/dist/wirings/workflow/graph/wire-workflow-graph.d.ts +7 -0
  48. package/dist/wirings/workflow/scenario-cookie-jar.d.ts +6 -0
  49. package/dist/wirings/workflow/scenario-cookie-jar.js +6 -0
  50. package/dist/wirings/workflow/scenario-poll.d.ts +6 -0
  51. package/dist/wirings/workflow/scenario-poll.js +6 -0
  52. package/dist/wirings/workflow/scenario-step-guards.d.ts +6 -0
  53. package/dist/wirings/workflow/scenario-step-guards.js +6 -0
  54. package/dist/wirings/workflow/workflow-errors.d.ts +4 -0
  55. package/dist/wirings/workflow/workflow-errors.js +4 -0
  56. package/package.json +1 -1
  57. package/src/app-leaf-surface.test.ts +2 -2
  58. package/src/errors/error-handler.ts +11 -0
  59. package/src/errors/serialized-error.ts +3 -0
  60. package/src/function/function-runner.ts +4 -4
  61. package/src/function/functions.types.ts +33 -21
  62. package/src/middleware/auth-apikey.ts +7 -0
  63. package/src/middleware/auth-bearer.ts +3 -0
  64. package/src/middleware/auth-cookie.ts +9 -4
  65. package/src/middleware/cors.ts +8 -0
  66. package/src/middleware/middleware-factories.ts +14 -0
  67. package/src/services/local-secrets.test.ts +2 -2
  68. package/src/services/local-secrets.ts +1 -1
  69. package/src/services/personas-service.ts +4 -0
  70. package/src/services/secret-service.ts +0 -1
  71. package/src/testing/service-tests/secret-service-tests.ts +1 -1
  72. package/src/types/core.types.ts +5 -0
  73. package/src/wirings/addon/addon-auth-tags.test.ts +4 -4
  74. package/src/wirings/addon/wire-addon.ts +17 -15
  75. package/src/wirings/addon/wire-remote-addon.ts +4 -0
  76. package/src/wirings/agent/agent-authorization.test.ts +7 -3
  77. package/src/wirings/agent/agent-prepare.ts +2 -2
  78. package/src/wirings/agent/agent.types.ts +31 -27
  79. package/src/wirings/agent-scorer/agent-scorer.ts +4 -0
  80. package/src/wirings/channel/channel.types.ts +11 -0
  81. package/src/wirings/cli/cli.types.ts +44 -1
  82. package/src/wirings/cli/define-cli-commands.ts +6 -1
  83. package/src/wirings/credential/credential.types.ts +7 -0
  84. package/src/wirings/gateway/gateway-authorization.test.ts +3 -3
  85. package/src/wirings/gateway/gateway.types.ts +23 -0
  86. package/src/wirings/http/http-runner-addon-ref.test.ts +1 -1
  87. package/src/wirings/http/http.types.ts +94 -69
  88. package/src/wirings/mcp/mcp.types.ts +27 -0
  89. package/src/wirings/queue/queue.types.ts +6 -0
  90. package/src/wirings/scheduler/scheduler.types.ts +5 -0
  91. package/src/wirings/secret/secret.types.ts +9 -14
  92. package/src/wirings/trigger/trigger.types.ts +4 -1
  93. package/src/wirings/variable/variable.types.ts +14 -13
  94. package/src/wirings/workflow/graph/wire-workflow-graph.ts +7 -0
  95. package/src/wirings/workflow/scenario-cookie-jar.ts +6 -0
  96. package/src/wirings/workflow/scenario-poll.ts +6 -0
  97. package/src/wirings/workflow/scenario-step-guards.ts +6 -0
  98. package/src/wirings/workflow/workflow-errors.ts +4 -0
  99. package/tsconfig.tsbuildinfo +1 -1
@@ -373,10 +373,15 @@ export type CommonWireMeta = {
373
373
  pikkuFuncId: string
374
374
  packageName?: string
375
375
 
376
+ /** A human name for this wiring, shown wherever it is listed rather than called. */
376
377
  title?: string
378
+ /** Filters this wiring in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
377
379
  tags?: string[]
380
+ /** A one-line description for listings, where the full `description` is too long. */
378
381
  summary?: string
382
+ /** What this does, for whoever is reading the wiring rather than writing it. */
379
383
  description?: string
384
+ /** Names of error classes this may throw, so each one's registered status is used instead of a 500. */
380
385
  errors?: string[]
381
386
 
382
387
  middleware?: MiddlewareMetadata[]
@@ -7,7 +7,7 @@ import {
7
7
  addTagMiddleware,
8
8
  clearMiddlewareCache,
9
9
  } from '../../middleware-runner.js'
10
- import { ForbiddenError } from '../../errors/errors.js'
10
+ import { ForbiddenError, MissingSessionError } from '../../errors/errors.js'
11
11
  import { resolveAddonAuth, resolveAddonTags, wireAddon } from './wire-addon.js'
12
12
 
13
13
  const ADDON_PACKAGE = '@addon/console'
@@ -114,7 +114,7 @@ describe('wireAddon auth on direct wirings', () => {
114
114
 
115
115
  await assert.rejects(
116
116
  () => callOverDirectWiring('credentialGet'),
117
- ForbiddenError
117
+ MissingSessionError
118
118
  )
119
119
  })
120
120
 
@@ -141,7 +141,7 @@ describe('wireAddon auth on direct wirings', () => {
141
141
 
142
142
  await assert.rejects(
143
143
  () => callOverDirectWiring('credentialGet', { auth: true }),
144
- ForbiddenError
144
+ MissingSessionError
145
145
  )
146
146
  })
147
147
 
@@ -161,7 +161,7 @@ describe('wireAddon auth on direct wirings', () => {
161
161
  ]) {
162
162
  await assert.rejects(
163
163
  () => callOverDirectWiring('credentialGet', { wireType }),
164
- ForbiddenError,
164
+ MissingSessionError,
165
165
  `${wireType} should have been gated`
166
166
  )
167
167
  }
@@ -2,35 +2,31 @@ import { pikkuState } from '../../pikku-state.js'
2
2
  import { getTagGroups } from '../../utils.js'
3
3
  import type { CorePikkuMiddleware } from '../../middleware/middleware.types.js'
4
4
  export type WireAddonConfig = {
5
+ /** How this instance is addressed. One package may be wired more than once, and the name is what tells the instances apart. */
5
6
  name: string
7
+ /** The npm package the addon ships in. */
6
8
  package: string
9
+ /** Where to reach the addon when it runs as its own service rather than in-process. */
7
10
  rpcEndpoint?: string
11
+ /** Requires a session for every function in the addon, whatever each one declares. Gates an addon whose functions are individually open. */
8
12
  auth?: boolean
13
+ /** Offers the addon's functions to MCP clients as tools, without wiring each one. */
9
14
  mcp?: boolean
15
+ /** Filters this addon in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
10
16
  tags?: string[]
11
17
  /** Required of every function in the addon, on top of the function's own. */
12
18
  scopes?: string[]
19
+ /** Points a secret the addon reads at a different key in this deployment, so two instances can hold different credentials. */
13
20
  secretOverrides?: Record<string, string>
21
+ /** Points a variable the addon reads at a different key in this deployment. */
14
22
  variableOverrides?: Record<string, string>
23
+ /** Points a credential the addon reads at a different key in this deployment. */
15
24
  credentialOverrides?: Record<string, string>
16
- /**
17
- * Secrets this instance may read on top of the ones it declared, named as the
18
- * addon reads them — the scope check runs before `secretOverrides` renames
19
- * them, so an overridden secret is named here by its addon-side key. Listing
20
- * one in `secretOverrides` grants it too.
21
- *
22
- * For an addon whose secret names come off its input rather than its own
23
- * source, this is how the host lends names the addon could not declare.
24
- */
25
+ /** Extra secrets this instance may read, named as the addon reads them — the scope check runs before `secretOverrides` renames them. */
25
26
  secretGrants?: string[]
26
27
  /** Credentials this instance may read on top of the ones it declared. */
27
28
  credentialGrants?: string[]
28
- /**
29
- * Hands this instance the whole `SecretService` instead of one scoped to the
30
- * secrets it declared. The value is the reason, recorded in the deploy
31
- * manifest — an addon that names secrets at runtime cannot be scoped, and
32
- * only the consuming app, never the addon, can grant it.
33
- */
29
+ /** Hands over the whole `SecretService` unscoped. The value is the reason, recorded in the deploy manifest. */
34
30
  globalSecrets?: string
35
31
  /**
36
32
  * Hands this instance the whole `CredentialService` instead of one narrowed
@@ -40,6 +36,12 @@ export type WireAddonConfig = {
40
36
  globalCredentials?: string
41
37
  }
42
38
 
39
+ /**
40
+ * Installs an addon into this project: its functions, wirings and scopes become
41
+ * part of the app, under the namespace and options given here.
42
+ *
43
+ * @example snippet: addonWiring
44
+ */
43
45
  export const wireAddon = (config: WireAddonConfig): void => {
44
46
  pikkuState(null, 'addons', 'packages').set(config.name, {
45
47
  package: config.package,
@@ -25,6 +25,10 @@ export type WireRemoteAddonConfig = {
25
25
  tags?: string[]
26
26
  }
27
27
 
28
+ /**
29
+ * Installs an addon that runs as its own deployed service: the contract is
30
+ * local, the calls go over the wire to the addon's own host.
31
+ */
28
32
  export const wireRemoteAddon = (config: WireRemoteAddonConfig): void => {
29
33
  pikkuState(null, 'addons', 'packages').set(config.name, {
30
34
  package: config.package,
@@ -8,7 +8,11 @@ import {
8
8
  } from '../../pikku-state.js'
9
9
  import { assertAgentAuthorized } from './agent-prepare.js'
10
10
  import { clearPermissionsCache } from '../../permissions.js'
11
- import { ForbiddenError, MissingScopeError } from '../../errors/errors.js'
11
+ import {
12
+ ForbiddenError,
13
+ MissingScopeError,
14
+ MissingSessionError,
15
+ } from '../../errors/errors.js'
12
16
  import type { CoreAgent } from './agent.types.js'
13
17
  import type { CoreUserSession } from '../../types/core.types.js'
14
18
 
@@ -73,7 +77,7 @@ describe('assertAgentAuthorized', () => {
73
77
  await assertAgentAuthorized(agent, {}, null)
74
78
  })
75
79
 
76
- test('throws ForbiddenError when auth is explicitly true and no session exists', async () => {
80
+ test('throws MissingSessionError when auth is explicitly true and no session exists', async () => {
77
81
  const agent = addAgent('needs-session', { auth: true })
78
82
  await assert.rejects(
79
83
  () =>
@@ -82,7 +86,7 @@ describe('assertAgentAuthorized', () => {
82
86
  { sessionService: sessionService(undefined) },
83
87
  null
84
88
  ),
85
- ForbiddenError
89
+ MissingSessionError
86
90
  )
87
91
  })
88
92
 
@@ -22,7 +22,7 @@ import {
22
22
  type PermissionWire,
23
23
  } from '../../permissions.js'
24
24
  import { AIProviderNotConfiguredError } from '../../errors/errors.js'
25
- import { ForbiddenError } from '../../errors/errors.js'
25
+ import { ForbiddenError, MissingSessionError } from '../../errors/errors.js'
26
26
  import { verifyScopes } from '../../scopes.js'
27
27
  import { pikkuState, getSingletonServices } from '../../pikku-state.js'
28
28
  import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js'
@@ -315,7 +315,7 @@ export async function assertAgentAuthorized(
315
315
  : undefined
316
316
 
317
317
  if (agent.auth === true && !session) {
318
- throw new ForbiddenError('Authentication required')
318
+ throw new MissingSessionError('Authentication required')
319
319
  }
320
320
 
321
321
  verifyScopes(agent.scopes, session)
@@ -183,6 +183,7 @@ export interface PikkuAgentMiddlewareHooks<
183
183
  State extends Record<string, unknown> = Record<string, unknown>,
184
184
  SingletonServices extends CoreSingletonServices = CoreSingletonServices,
185
185
  > {
186
+ /** Rewrites what the model is about to see — messages and instructions — before each turn. */
186
187
  modifyInput?: (
187
188
  services: SingletonServices,
188
189
  ctx: {
@@ -210,6 +211,7 @@ export interface PikkuAgentMiddlewareHooks<
210
211
  | Promise<{ messages: AgentMessage[]; instructions: string }>
211
212
  | { messages: AgentMessage[]; instructions: string }
212
213
 
214
+ /** Sees the model's output as it streams, for redaction or live inspection. Keeps its own `state` across chunks, unlike the shared run notes. */
213
215
  modifyOutputStream?: (
214
216
  services: SingletonServices,
215
217
  ctx: {
@@ -262,6 +264,7 @@ export interface PikkuAgentMiddlewareHooks<
262
264
  * and handed to anything that grades the run, and scrubbing the reply alone
263
265
  * leaves them untouched.
264
266
  */
267
+ /** Rewrites the finished output of a turn, after streaming has completed. */
265
268
  modifyOutput?: (
266
269
  services: SingletonServices,
267
270
  ctx: {
@@ -282,6 +285,7 @@ export interface PikkuAgentMiddlewareHooks<
282
285
  toolCalls?: NonNullable<AgentStep['toolCalls']>
283
286
  }
284
287
 
288
+ /** Runs before a tool executes, and may rewrite the arguments the model chose. Returning nothing leaves them as they are. */
285
289
  beforeToolCall?: (
286
290
  services: SingletonServices,
287
291
  ctx: {
@@ -294,6 +298,7 @@ export interface PikkuAgentMiddlewareHooks<
294
298
  | { args: Record<string, unknown> }
295
299
  | void
296
300
 
301
+ /** Runs once a tool has returned, and may replace its result before the model sees it. */
297
302
  afterToolCall?: (
298
303
  services: SingletonServices,
299
304
  ctx: {
@@ -305,6 +310,7 @@ export interface PikkuAgentMiddlewareHooks<
305
310
  }
306
311
  ) => Promise<{ result: unknown } | void> | { result: unknown } | void
307
312
 
313
+ /** Runs at the end of each model turn. For observation — it cannot change what happened. */
308
314
  afterStep?: (
309
315
  services: SingletonServices,
310
316
  ctx: {
@@ -323,6 +329,7 @@ export interface PikkuAgentMiddlewareHooks<
323
329
  }
324
330
  ) => Promise<void> | void
325
331
 
332
+ /** Runs when a turn throws, with the step it failed on. For logging and cleanup; it does not swallow the error. */
326
333
  onError?: (
327
334
  services: SingletonServices,
328
335
  ctx: {
@@ -347,58 +354,51 @@ export type CoreAgent<
347
354
  Scope extends string = string,
348
355
  Scorer extends string = string,
349
356
  > = {
357
+ /** Unique across the project. It is how the agent is invoked and how its runs are grouped. */
350
358
  name: string
359
+ /** What the agent is for. Another agent choosing whether to delegate to this one reads it, so write it for that reader. */
351
360
  description: string
361
+ /** A one-line description for listings, where the full `description` is too long. */
352
362
  summary?: string
363
+ /** Names of error classes this may throw, so each one's registered status is used instead of a 500. */
353
364
  errors?: string[]
354
- /**
355
- * The three fields below are the system prompt. `buildInstructions` joins
356
- * whichever are set with a blank line, always in this order — role, then
357
- * personality, then goal — and appends the tool-usage rules if the agent has
358
- * tools. Nothing checks them against each other: the split is there to keep a
359
- * prompt legible, and text put in the wrong one still reaches the model.
360
- *
361
- * Who the agent is. 'You are a support engineer triaging inbound bugs.'
362
- */
365
+ /** Who the agent is. Joined with `personality` and `goal`, in that order, to form the system prompt. */
363
366
  role?: string
364
367
  /** How it should sound: tone, vocabulary, how much it says at a time. */
365
368
  personality?: string
366
369
  /** What it is for — the only one of the three that is required. */
367
370
  goal: string
371
+ /** Which model to run on, as the provider names it. */
368
372
  model: string
373
+ /** How much the model is allowed to vary its answer. Lower is more repeatable, which is what a tool-driving agent usually wants. */
369
374
  temperature?: number
370
375
  /** Ownership/partitioning of this agent's threads and runs. Defaults to `'user'`. */
371
376
  sessionScope?: SessionScope
377
+ /** Functions the model may call, given to it as tools. A `readonly` one may be called without asking first. */
372
378
  tools?: unknown[]
379
+ /** Other agents this one may hand work to. `agentMode` decides whether it delegates or supervises. */
373
380
  agents?: unknown[]
381
+ /** Workflows the model may start, for work too long to sit inside one agent run. */
374
382
  workflows?: unknown[]
375
- /**
376
- * Grades this agent's finished runs on live traffic, named by the generated
377
- * `ScorerName` union rather than by `ref()` — a scorer is not a function, so
378
- * there is nothing in the function map for a ref to resolve against.
379
- *
380
- * A reference-based judge listed here is never sampled: live traffic has no
381
- * answer key. Scenarios name scorers directly and may grade with scorers an
382
- * agent does not ship with.
383
- */
383
+ /** Grades finished runs on live traffic. A reference-based judge is never sampled here — live traffic has no answer key. */
384
384
  scorers?: Scorer[]
385
+ /** `delegate` hands a sub-agent the task and takes its answer; `supervise` keeps this agent in the loop over each step. */
385
386
  agentMode?: 'delegate' | 'supervise'
387
+ /** What the agent carries between runs, and how much of it. */
386
388
  memory?: AgentMemoryConfig
389
+ /** How many model turns a single run may take before it is stopped. The guard against a tool loop that never converges. */
387
390
  maxSteps?: number
391
+ /** Whether the model may answer without calling a tool (`auto`), must call one (`required`), or may not (`none`). */
388
392
  toolChoice?: 'auto' | 'required' | 'none'
389
- /**
390
- * Per-provider model settings, keyed by provider id and passed through
391
- * untouched — for anything only one vendor offers, which the fields above
392
- * deliberately do not try to unify.
393
- *
394
- * `{ openai: { reasoningEffort: 'minimal' } }` is the one that matters for
395
- * voice: on gpt-5-mini it measured 0.9s to first token against 2.5s at the
396
- * default, and a spoken reply is waited through rather than skimmed.
397
- */
393
+ /** Per-provider settings passed through untouched, for what only one vendor offers. */
398
394
  providerOptions?: AIProviderOptions
395
+ /** The schema of what starts a run, which is also its input type. */
399
396
  input?: unknown
397
+ /** The schema the final answer must match, which is also its return type. Without one the agent returns free text. */
400
398
  output?: unknown
399
+ /** Filters this agent in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
401
400
  tags?: string[]
401
+ /** Runs before each model turn, to change what that turn sees or to stop the run. This is where a step budget or a tool narrowing goes. */
402
402
  prepareStep?: (ctx: {
403
403
  stepNumber: number
404
404
  messages: AgentMessage[]
@@ -407,8 +407,11 @@ export type CoreAgent<
407
407
  model: string
408
408
  stop: () => void
409
409
  }) => void | Promise<void>
410
+ /** Wraps the whole run: auth, tracing, spend limits. */
410
411
  middleware?: PikkuMiddleware[]
412
+ /** Wraps each message when the agent is driven over a channel. */
411
413
  channelMiddleware?: CorePikkuChannelMiddleware<any, any>[]
414
+ /** Hooks around each step and each tool call — the place to inspect, rewrite or veto what the model is about to do. */
412
415
  agentMiddleware?: PikkuAgentMiddlewareHooks<any, any>[]
413
416
  /**
414
417
  * Whether a session is required to run this agent. Defaults to `false`, since
@@ -427,6 +430,7 @@ export type CoreAgent<
427
430
  * `#pikku/scopes`, so an undeclared scope is a compile error.
428
431
  */
429
432
  scopes?: Scope[]
433
+ /** Checks that run before the agent starts. Grouped names OR together, so any one passing admits the caller. */
430
434
  permissions?: CorePermissionGroup<PikkuPermission>
431
435
  }
432
436
 
@@ -18,6 +18,8 @@ const assertSampleRate = (name: string, sampleRate: number | undefined) => {
18
18
  /**
19
19
  * A heuristic scorer: pure code over the finished run, no model call, so it
20
20
  * grades on the fast lane.
21
+ *
22
+ * @example snippet: agentScorer
21
23
  */
22
24
  export const pikkuAgentScorer = <Services = any>(config: {
23
25
  name: string
@@ -49,6 +51,8 @@ export const pikkuAgentScorer = <Services = any>(config: {
49
51
  * The rubric field is `goal`, matching `pikkuAgent`'s prompt vocabulary — a
50
52
  * judge is a degenerate agent, and should use the same word for the same thing.
51
53
  * `prompt` is the escape hatch for non-standard framing.
54
+ *
55
+ * @example snippet: agentJudge
52
56
  */
53
57
  export const pikkuAgentJudge = <Services = any>(config: {
54
58
  name: string
@@ -98,8 +98,11 @@ export type CoreChannel<
98
98
  PikkuPermission = CorePikkuPermission<ChannelData>,
99
99
  PikkuMiddleware = CorePikkuMiddleware,
100
100
  > = {
101
+ /** Unique across the project. It is how the channel is addressed in `pikku meta` and by the generated client. */
101
102
  name: string
103
+ /** The path a client opens the socket on. */
102
104
  route: Channel
105
+ /** Runs once when a client connects, before any message. Its return value is sent as the first message. */
103
106
  onConnect?:
104
107
  | ChannelConnect
105
108
  | {
@@ -112,7 +115,9 @@ export type CoreChannel<
112
115
  func?: ChannelDisconnect
113
116
  middleware?: PikkuMiddleware[]
114
117
  }
118
+ /** Handles any message that no `onMessageWiring` entry claimed. Without one, an unrouted message is dropped. */
115
119
  onMessage?: ChannelFunctionMessage
120
+ /** Routes messages by a key in their payload: the outer key is the field to switch on, the inner key its value. This is how one socket carries many operations. */
116
121
  onMessageWiring?: Record<
117
122
  string,
118
123
  Record<
@@ -125,17 +130,23 @@ export type CoreChannel<
125
130
  }
126
131
  >
127
132
  >
133
+ /** Wraps the connection: it runs on connect, where the session is established, not on every message. */
128
134
  middleware?: PikkuMiddleware[]
135
+ /** Wraps every message, which is where per-message concerns like rate limiting belong. */
129
136
  channelMiddleware?: Array<
130
137
  CorePikkuChannelMiddleware | CorePikkuChannelMiddlewareFactory
131
138
  >
139
+ /** Whether opening the channel requires a session. Defaults to true — a channel is closed unless it says otherwise. */
132
140
  auth?: boolean
141
+ /** Whether this channel carries binary frames. Text frames are still parsed as JSON. */
133
142
  binary?: boolean | null
143
+ /** Handles binary frames, which never reach `onMessage` because they are not JSON. Return a value to reply in kind. */
134
144
  onBinaryMessage?: (
135
145
  services: any,
136
146
  data: BinaryData,
137
147
  channel: PikkuChannel<ChannelData, any>
138
148
  ) => Promise<BinaryData | void> | BinaryData | void
149
+ /** Filters this channel in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
139
150
  tags?: string[]
140
151
  }
141
152
 
@@ -174,6 +174,12 @@ type SplitBySpace<S extends string> = S extends `${infer First} ${infer Rest}`
174
174
  ? []
175
175
  : [S]
176
176
 
177
+ /**
178
+ * Strip the `...` a variadic positional carries, so `[files...]` validates
179
+ * against the `files` key the runtime parser fills.
180
+ */
181
+ type StripVariadic<S extends string> = S extends `${infer Name}...` ? Name : S
182
+
177
183
  /**
178
184
  * Recursively build a tuple by stripping brackets from each element
179
185
  */
@@ -182,7 +188,7 @@ type BuildParamsTuple<Parts extends readonly string[]> =
182
188
  infer First extends string,
183
189
  ...infer Rest extends string[],
184
190
  ]
185
- ? [StripBrackets<First>, ...BuildParamsTuple<Rest>]
191
+ ? [StripVariadic<StripBrackets<First>>, ...BuildParamsTuple<Rest>]
186
192
  : []
187
193
 
188
194
  /**
@@ -227,16 +233,39 @@ export type CoreCLICommandConfig<
227
233
  >,
228
234
  Params extends string = string,
229
235
  > = {
236
+ /**
237
+ * Positional arguments, written the way `--help` prints them: `<env>` is
238
+ * required, `[region]` optional. Each name must be a key of the function's
239
+ * input schema, so a typo is a compile error rather than an argument that
240
+ * silently never arrives.
241
+ */
230
242
  parameters?: ValidateParameters<Params, ExtractFunctionInput<FuncConfig>>
243
+ /** The function to run. Omit it on a command that exists only to hold `subcommands`. */
231
244
  func?: FuncConfig
245
+ /** The heading shown above this command's own help. */
232
246
  title?: string
247
+ /** The one-line summary listed beside the command name. */
233
248
  description?: string
249
+ /**
250
+ * Prints the result. The function returns data and this is the only thing
251
+ * that writes to stdout, which is what lets one command serve both a human
252
+ * reading it and a script parsing it.
253
+ */
234
254
  render?: PikkuCLIRender
255
+ /**
256
+ * Flags, keyed by the input field each one fills. A field with no entry is
257
+ * still accepted as `--field`; an entry is how it gets a short form, a
258
+ * default, or help text.
259
+ */
235
260
  options?: {
236
261
  [K in keyof ExtractFunctionInput<FuncConfig>]?: {
262
+ /** What this flag is for, as `--help` prints it. */
237
263
  description?: string
264
+ /** A single-letter alias, so `--verbose` can also be `-v`. */
238
265
  short?: string
266
+ /** How the argument is parsed. Inferred from the input schema when omitted. */
239
267
  type?: CLIOptionType
268
+ /** Used when the flag is absent, and shown in `--help` so nobody has to guess it. */
240
269
  default?: ExtractFunctionInput<FuncConfig>[K]
241
270
  /**
242
271
  * The values this option accepts. Rejected by the parser and listed in
@@ -246,13 +275,18 @@ export type CoreCLICommandConfig<
246
275
  choices?: ReadonlyArray<ExtractFunctionInput<FuncConfig>[K]>
247
276
  }
248
277
  }
278
+ /** Wraps every run of this command. */
249
279
  middleware?: PikkuMiddleware[]
280
+ /** Nested commands, keyed by the word that selects them: `deploy plan` is `deploy` with a `plan` subcommand. */
250
281
  subcommands?: Record<
251
282
  string,
252
283
  CoreCLICommandConfig<any, PikkuMiddleware, PikkuCLIRender, any>
253
284
  >
285
+ /** Whether running this requires a logged-in session. Defaults to true — a command is closed unless it says otherwise. */
254
286
  auth?: boolean
287
+ /** Permission checks run before the function, for a command that not every logged-in user may run. */
255
288
  permissions?: any[]
289
+ /** Runs when the parent is invoked with no subcommand named. */
256
290
  isDefault?: boolean
257
291
  }
258
292
 
@@ -308,14 +342,23 @@ export interface CoreCLI<
308
342
  PikkuMiddleware,
309
343
  PikkuCLIRender,
310
344
  > {
345
+ /** The command name a user types. It is also what `--help` prints as the program. */
311
346
  program: string
347
+ /** What the program is for, shown at the top of `--help`. */
312
348
  description?: string
349
+ /** Top-level commands, keyed by the word that selects them. */
313
350
  commands: Commands
351
+ /** Flags accepted before any command, for things that apply to the whole program. */
314
352
  options?: CLIOptions<Options>
353
+ /** Wraps every command. */
315
354
  middleware?: PikkuMiddleware[]
355
+ /** The fallback printer for commands that declare none of their own. */
316
356
  render?: PikkuCLIRender
357
+ /** A one-line description for listings, where the full `description` is too long. */
317
358
  summary?: string
359
+ /** Names of error classes any command may throw, so each one's registered exit behaviour is used. */
318
360
  errors?: string[]
361
+ /** Filters this program in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
319
362
  tags?: string[]
320
363
  /**
321
364
  * Requires a session on the websocket serving this program remotely. Has no
@@ -1,6 +1,11 @@
1
1
  import type { CoreCLICommandConfig } from './cli.types.js'
2
2
 
3
- /** Identity at runtime; exists so a commands record can be typed and composed before `wireCLI`. */
3
+ /**
4
+ * Identity at runtime; exists so a commands record can be typed and composed
5
+ * before `wireCLI`.
6
+ *
7
+ * @example snippet: cliCommandContract
8
+ */
4
9
  export function defineCLICommands<
5
10
  T extends Record<string, CoreCLICommandConfig<any, any, any, any>>,
6
11
  >(commands: T): T {
@@ -1,12 +1,19 @@
1
1
  import type { OAuth2CredentialConfig } from '../secret/secret.types.js'
2
2
 
3
3
  export type CoreCredential<T = unknown> = {
4
+ /** How the credential is asked for in code. Generated into `CredentialsMap`, so it is what `credentials.get` autocompletes. */
4
5
  name: string
6
+ /** The name shown to whoever has to supply the value, who is often not the person who wrote this. */
5
7
  displayName: string
8
+ /** What the credential is for and where to obtain one. */
6
9
  description?: string
10
+ /** `singleton` is one value for the whole deployment; `wire` is one per user, supplied by them and stored against their account. */
7
11
  type: 'singleton' | 'wire'
12
+ /** The shape of the value, validated when it is supplied rather than when it is first used. */
8
13
  schema: T
14
+ /** Where to go to create one, shown next to the field asking for it. */
9
15
  docsUrl?: string
16
+ /** Makes this an OAuth connection rather than a value pasted in: the user is sent to the provider and the tokens are stored for them. */
10
17
  oauth2?: OAuth2CredentialConfig & {
11
18
  appCredentialSecretId: string
12
19
  }
@@ -280,7 +280,7 @@ describe('gateway handler authorization', () => {
280
280
 
281
281
  const response = await postMessage('/webhooks/declared-session')
282
282
 
283
- assert.equal(response.status, 403)
283
+ assert.equal(response.status, 401)
284
284
  assert.deepEqual(
285
285
  calls,
286
286
  [],
@@ -339,7 +339,7 @@ describe('gateway handler authorization', () => {
339
339
 
340
340
  const response = await postMessage('/webhooks/gw-auth')
341
341
 
342
- assert.equal(response.status, 403)
342
+ assert.equal(response.status, 401)
343
343
  assert.deepEqual(calls, [], 'auth: true must require a session')
344
344
  })
345
345
 
@@ -365,7 +365,7 @@ describe('gateway handler authorization', () => {
365
365
 
366
366
  const response = await postMessage('/webhooks/handler-auth')
367
367
 
368
- assert.equal(response.status, 403)
368
+ assert.equal(response.status, 401)
369
369
  assert.deepEqual(calls, [], 'auth: true must require a session')
370
370
  })
371
371
 
@@ -17,6 +17,10 @@ export interface GatewayAttachment {
17
17
  filename?: string
18
18
  }
19
19
 
20
+ /**
21
+ * One message arriving from a gateway, normalised: who sent it, in which
22
+ * conversation, and what they said.
23
+ */
20
24
  export interface GatewayInboundMessage {
21
25
  /** Platform-specific: a phone number, a Slack user id, and so on. */
22
26
  senderId: string
@@ -26,15 +30,27 @@ export interface GatewayInboundMessage {
26
30
  metadata?: Record<string, unknown>
27
31
  }
28
32
 
33
+ /**
34
+ * One message to send back through a gateway — plain text, or the provider's
35
+ * own rich content.
36
+ */
29
37
  export interface GatewayOutboundMessage {
30
38
  text?: string
31
39
  richContent?: Record<string, unknown>
32
40
  attachments?: GatewayAttachment[]
33
41
  }
34
42
 
43
+ /**
44
+ * What a gateway's `verifyWebhook` returns — verified, with the response the
45
+ * provider expects back, or not.
46
+ */
35
47
  export type WebhookVerificationResult =
36
48
  { verified: true; response: unknown } | { verified: false }
37
49
 
50
+ /**
51
+ * What a gateway integration implements: parse an incoming event into a
52
+ * message, send one back, and open and close the connection.
53
+ */
38
54
  export interface GatewayAdapter {
39
55
  name: string
40
56
  /** Return null to ignore the event, e.g. a delivery receipt. */
@@ -70,14 +86,21 @@ export type CoreGateway<
70
86
  > = Partial<
71
87
  Pick<CommonWireMeta, 'title' | 'summary' | 'description' | 'errors'>
72
88
  > & {
89
+ /** Unique across the project. It is how the gateway is addressed in `pikku meta` and in logs. */
73
90
  name: string
91
+ /** How the platform reaches us: a `webhook` it posts to, a `websocket` it holds open, or a `listener` we open outward. */
74
92
  type: GatewayTransportType
75
93
  /** Required for 'webhook' and 'websocket'; unused for 'listener'. */
76
94
  route?: string
95
+ /** Which service this speaks to — slack, whatsapp, discord. It selects the adapter's dialect, not the transport. */
77
96
  platform?: string
97
+ /** Translates between the platform's message format and pikku's. A factory is called with services, for an adapter that needs a token or a client. */
78
98
  adapter: GatewayAdapter | GatewayAdapterFactory
99
+ /** The function to run per inbound message. It receives the normalised message, not the platform's raw payload. */
79
100
  func: PikkuFunctionConfig
101
+ /** Wraps every inbound message: signature verification, tracing, rate limiting. */
80
102
  middleware?: CorePikkuMiddlewareGroup<any, any>
103
+ /** Filters this gateway in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
81
104
  tags?: string[]
82
105
  /** Unset lets the handler's own `auth` govern; gateway handlers are sessionless by default. */
83
106
  auth?: boolean
@@ -233,6 +233,6 @@ describe('http routes wired with ref() to an addon function', () => {
233
233
 
234
234
  const response = await fetch(new ParamsRequest('/addon/greet', 'get'))
235
235
 
236
- assert.strictEqual(response.status, 403)
236
+ assert.strictEqual(response.status, 401)
237
237
  })
238
238
  })