@pikku/core 0.12.20 → 0.12.22

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 (141) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/dev/hot-reload.js +20 -6
  3. package/dist/errors/error-handler.d.ts +1 -1
  4. package/dist/errors/error-handler.js +2 -2
  5. package/dist/function/functions.types.d.ts +3 -2
  6. package/dist/handle-error.js +3 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +2 -2
  9. package/dist/middleware/index.d.ts +2 -2
  10. package/dist/middleware/index.js +2 -2
  11. package/dist/middleware/telemetry.d.ts +2 -2
  12. package/dist/middleware/telemetry.js +2 -2
  13. package/dist/middleware-runner.d.ts +15 -27
  14. package/dist/middleware-runner.js +25 -30
  15. package/dist/permissions.d.ts +15 -22
  16. package/dist/permissions.js +30 -25
  17. package/dist/pikku-request.js +1 -1
  18. package/dist/pikku-state.js +2 -3
  19. package/dist/services/ai-agent-runner-service.d.ts +1 -0
  20. package/dist/services/in-memory-queue-service.js +1 -1
  21. package/dist/services/in-memory-workflow-service.d.ts +15 -13
  22. package/dist/services/in-memory-workflow-service.js +31 -13
  23. package/dist/services/local-content.js +8 -1
  24. package/dist/services/user-session-service.d.ts +1 -1
  25. package/dist/testing/service-tests.js +24 -0
  26. package/dist/types/core.types.d.ts +4 -0
  27. package/dist/types/state.types.d.ts +2 -18
  28. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
  29. package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
  30. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
  31. package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
  32. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
  33. package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
  34. package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
  35. package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
  36. package/dist/wirings/channel/channel-handler.js +1 -1
  37. package/dist/wirings/channel/channel-store.d.ts +13 -0
  38. package/dist/wirings/channel/channel.types.d.ts +3 -0
  39. package/dist/wirings/channel/local/local-channel-runner.js +23 -5
  40. package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
  41. package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
  42. package/dist/wirings/cli/cli-runner.js +24 -5
  43. package/dist/wirings/cli/command-parser.js +19 -4
  44. package/dist/wirings/http/http-runner.js +72 -36
  45. package/dist/wirings/http/http.types.d.ts +1 -0
  46. package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
  47. package/dist/wirings/http/web-request.js +32 -0
  48. package/dist/wirings/rpc/rpc-runner.js +13 -3
  49. package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
  50. package/dist/wirings/workflow/graph/graph-node.js +4 -0
  51. package/dist/wirings/workflow/graph/graph-runner.js +12 -10
  52. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
  53. package/dist/wirings/workflow/index.d.ts +1 -1
  54. package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
  55. package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
  56. package/dist/wirings/workflow/workflow.types.d.ts +34 -0
  57. package/package.json +1 -1
  58. package/run-tests.sh +4 -1
  59. package/src/dev/hot-reload.test.ts +62 -11
  60. package/src/dev/hot-reload.ts +28 -7
  61. package/src/errors/error-handler.ts +8 -2
  62. package/src/errors/error.test.ts +2 -0
  63. package/src/function/function-runner.test.ts +500 -10
  64. package/src/function/functions.types.ts +3 -2
  65. package/src/handle-error.test.ts +1 -1
  66. package/src/handle-error.ts +4 -1
  67. package/src/index.ts +12 -2
  68. package/src/middleware/index.ts +11 -2
  69. package/src/middleware/telemetry.ts +2 -2
  70. package/src/middleware-runner.test.ts +16 -16
  71. package/src/middleware-runner.ts +42 -30
  72. package/src/permissions.test.ts +27 -24
  73. package/src/permissions.ts +41 -25
  74. package/src/pikku-request.test.ts +35 -0
  75. package/src/pikku-request.ts +1 -1
  76. package/src/pikku-state.ts +2 -3
  77. package/src/run-tests-script.test.ts +18 -0
  78. package/src/services/ai-agent-runner-service.ts +1 -0
  79. package/src/services/in-memory-queue-service.ts +1 -1
  80. package/src/services/in-memory-session-store.ts +1 -2
  81. package/src/services/in-memory-workflow-service.test.ts +33 -11
  82. package/src/services/in-memory-workflow-service.ts +49 -13
  83. package/src/services/local-content.test.ts +54 -0
  84. package/src/services/local-content.ts +12 -1
  85. package/src/services/typed-credential-service.ts +3 -3
  86. package/src/services/typed-secret-service.ts +3 -3
  87. package/src/services/typed-variables-service.ts +3 -3
  88. package/src/services/user-session-service.ts +4 -4
  89. package/src/testing/service-tests.ts +30 -0
  90. package/src/types/core.types.ts +6 -2
  91. package/src/types/state.types.ts +2 -13
  92. package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
  93. package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
  94. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
  95. package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
  96. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
  97. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
  98. package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
  99. package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
  100. package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
  101. package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
  102. package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
  103. package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
  104. package/src/wirings/channel/channel-handler.test.ts +272 -0
  105. package/src/wirings/channel/channel-handler.ts +1 -1
  106. package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
  107. package/src/wirings/channel/channel-store.ts +19 -0
  108. package/src/wirings/channel/channel.types.ts +4 -0
  109. package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
  110. package/src/wirings/channel/local/local-channel-runner.ts +41 -5
  111. package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
  112. package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
  113. package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
  114. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
  115. package/src/wirings/cli/cli-runner.test.ts +255 -2
  116. package/src/wirings/cli/cli-runner.ts +31 -10
  117. package/src/wirings/cli/cli.types.ts +4 -8
  118. package/src/wirings/cli/command-parser.test.ts +83 -0
  119. package/src/wirings/cli/command-parser.ts +23 -4
  120. package/src/wirings/http/http-runner.test.ts +296 -1
  121. package/src/wirings/http/http-runner.ts +87 -57
  122. package/src/wirings/http/http.types.ts +11 -26
  123. package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
  124. package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
  125. package/src/wirings/http/web-request.test.ts +115 -0
  126. package/src/wirings/http/web-request.ts +43 -5
  127. package/src/wirings/mcp/mcp-runner.test.ts +367 -0
  128. package/src/wirings/queue/queue.types.ts +2 -5
  129. package/src/wirings/rpc/rpc-runner.test.ts +511 -0
  130. package/src/wirings/rpc/rpc-runner.ts +15 -3
  131. package/src/wirings/rpc/wire-addon.test.ts +57 -0
  132. package/src/wirings/workflow/graph/graph-node.ts +12 -0
  133. package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
  134. package/src/wirings/workflow/graph/graph-runner.ts +28 -10
  135. package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
  136. package/src/wirings/workflow/index.ts +1 -0
  137. package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
  138. package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
  139. package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
  140. package/src/wirings/workflow/workflow.types.ts +68 -5
  141. package/tsconfig.tsbuildinfo +1 -1
@@ -1,6 +1,9 @@
1
1
  import { watch, type FSWatcher } from 'node:fs'
2
2
  import { stat, readFile } from 'node:fs/promises'
3
3
  import { join, resolve, relative } from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
5
+
6
+ import { register } from 'tsx/esm/api'
4
7
 
5
8
  import { pikkuState } from '../pikku-state.js'
6
9
  import { addFunction } from '../function/function-runner.js'
@@ -54,10 +57,26 @@ const findCompiledFile = async (
54
57
  // Use data: URLs to import modules. This bypasses TypeScript loaders
55
58
  // (e.g. tsx) that intercept file:// imports and break dynamic ESM loading.
56
59
  // Each import gets unique content so there's no module cache to worry about.
60
+ let tsxRegistered = false
61
+
62
+ const ensureTsxRegistered = () => {
63
+ if (tsxRegistered) return
64
+ register()
65
+ tsxRegistered = true
66
+ }
67
+
57
68
  const reimportModule = async (
58
- filePath: string
69
+ filePath: string,
70
+ useTsx = false
59
71
  ): Promise<Record<string, unknown> | null> => {
60
72
  try {
73
+ if (useTsx) {
74
+ ensureTsxRegistered()
75
+ return await import(
76
+ `${pathToFileURL(resolve(filePath)).href}?t=${Date.now()}`
77
+ )
78
+ }
79
+
61
80
  const content = await readFile(resolve(filePath), 'utf-8')
62
81
  const dataUrl =
63
82
  'data:text/javascript;base64,' + Buffer.from(content).toString('base64')
@@ -98,17 +117,19 @@ export async function pikkuDevReloader(
98
117
  srcDir,
99
118
  absPikkuDir
100
119
  )
101
- if (!compiledFile) {
102
- logger.warn(
103
- `Could not find compiled JS for: ${relative(process.cwd(), changedTsFile)}`
120
+ const importPath = compiledFile ?? changedTsFile
121
+ const usedTsxFallback = !compiledFile
122
+
123
+ if (usedTsxFallback) {
124
+ logger.debug(
125
+ `Hot-reload using tsx fallback for: ${relative(process.cwd(), changedTsFile)}`
104
126
  )
105
- return
106
127
  }
107
128
 
108
- const mod = await reimportModule(compiledFile)
129
+ const mod = await reimportModule(importPath, usedTsxFallback)
109
130
  if (!mod) {
110
131
  logger.error(
111
- `Failed to import: ${relative(process.cwd(), compiledFile)} (keeping old code)`
132
+ `Failed to import: ${relative(process.cwd(), importPath)} (keeping old code)`
112
133
  )
113
134
  return
114
135
  }
@@ -29,8 +29,14 @@ export interface ErrorDetails {
29
29
  * @param error - The error to add.
30
30
  * @param details - The details of the error.
31
31
  */
32
- export const addError = (error: any, { status, message }: ErrorDetails) => {
33
- pikkuState(null, 'misc', 'errors').set(error, { status, message })
32
+ export const addError = (
33
+ error: any,
34
+ { status, message, mcpCode }: ErrorDetails
35
+ ) => {
36
+ pikkuState(null, 'misc', 'errors').set(
37
+ error,
38
+ mcpCode === undefined ? { status, message } : { status, message, mcpCode }
39
+ )
34
40
  }
35
41
 
36
42
  /**
@@ -77,6 +77,7 @@ describe('getErrorResponse', () => {
77
77
  const response = getErrorResponse(error)
78
78
  assert.deepStrictEqual(response, {
79
79
  status: 400,
80
+ mcpCode: -32600,
80
81
  message:
81
82
  'The server cannot or will not process the request due to client error (e.g., malformed request syntax).',
82
83
  })
@@ -87,6 +88,7 @@ describe('getErrorResponse', () => {
87
88
  const response = getErrorResponse(error)
88
89
  assert.deepStrictEqual(response, {
89
90
  status: 404,
91
+ mcpCode: -32601,
90
92
  message: 'The server cannot find the requested resource.',
91
93
  })
92
94
  })
@@ -1,10 +1,18 @@
1
1
  import { describe, test, beforeEach } from 'node:test'
2
2
  import * as assert from 'node:assert'
3
- import { addFunction, runPikkuFunc } from './function-runner.js'
4
- import { addMiddleware, addPermission } from '../index.js'
3
+ import {
4
+ addFunction,
5
+ getAllFunctionNames,
6
+ getFunctionNames,
7
+ runPikkuFunc,
8
+ runPikkuFuncDirectly,
9
+ } from './function-runner.js'
10
+ import { addTagMiddleware, addTagPermission } from '../index.js'
5
11
  import { resetPikkuState, pikkuState } from '../pikku-state.js'
6
12
  import type { CoreServices, CorePikkuMiddleware } from '../types/core.types.js'
7
13
  import type { CorePermissionGroup } from './functions.types.js'
14
+ import { PikkuSessionService } from '../services/user-session-service.js'
15
+ import { ReadonlySessionError } from '../errors/errors.js'
8
16
 
9
17
  beforeEach(() => {
10
18
  resetPikkuState()
@@ -53,8 +61,8 @@ describe('runPikkuFunc - Integration Tests', () => {
53
61
  }
54
62
  }
55
63
 
56
- addMiddleware('wiringTag', [createMiddleware('wiringTag')])
57
- addMiddleware('funcTag', [createMiddleware('funcTag')])
64
+ addTagMiddleware('wiringTag', [createMiddleware('wiringTag')])
65
+ addTagMiddleware('funcTag', [createMiddleware('funcTag')])
58
66
 
59
67
  // Register function with middleware and tags
60
68
  addTestFunction('testFunc', {
@@ -105,8 +113,8 @@ describe('runPikkuFunc - Integration Tests', () => {
105
113
  return true
106
114
  }
107
115
 
108
- addPermission('wiringTag', [wiringTagPermission])
109
- addPermission('funcTag', [funcTagPermission])
116
+ addTagPermission('wiringTag', [wiringTagPermission])
117
+ addTagPermission('funcTag', [funcTagPermission])
110
118
 
111
119
  // Setup direct permissions
112
120
  const wiringPermissions: CorePermissionGroup = {
@@ -159,7 +167,7 @@ describe('runPikkuFunc - Integration Tests', () => {
159
167
  test('should throw specific error for wiring tag permission failures', async () => {
160
168
  const failingWiringTagPermission = async () => false
161
169
 
162
- addPermission('wiringTag', [failingWiringTagPermission])
170
+ addTagPermission('wiringTag', [failingWiringTagPermission])
163
171
 
164
172
  addTestFunction('testFunc', {
165
173
  func: async () => 'success',
@@ -207,7 +215,7 @@ describe('runPikkuFunc - Integration Tests', () => {
207
215
  test('should throw specific error for function tag permission failures', async () => {
208
216
  const failingFuncTagPermission = async () => false
209
217
 
210
- addPermission('funcTag', [failingFuncTagPermission])
218
+ addTagPermission('funcTag', [failingFuncTagPermission])
211
219
 
212
220
  addTestFunction('testFunc', {
213
221
  func: async () => 'success',
@@ -265,7 +273,7 @@ describe('runPikkuFunc - Integration Tests', () => {
265
273
  }
266
274
 
267
275
  // Add same middleware to tag and directly
268
- addMiddleware('testTag', [duplicatedMiddleware])
276
+ addTagMiddleware('testTag', [duplicatedMiddleware])
269
277
 
270
278
  addTestFunction('testFunc', {
271
279
  func: async () => 'success',
@@ -309,7 +317,7 @@ describe('runPikkuFunc - Integration Tests', () => {
309
317
  ],
310
318
  }
311
319
 
312
- addPermission('mixedTag', arrayPermission)
320
+ addTagPermission('mixedTag', arrayPermission)
313
321
 
314
322
  addTestFunction('testFunc', {
315
323
  func: async () => {
@@ -533,4 +541,486 @@ describe('runPikkuFunc - Integration Tests', () => {
533
541
  ...wireServices,
534
542
  })
535
543
  })
544
+
545
+ test('should resolve versioned function ids to the base function and warn once', async () => {
546
+ const warnings: string[] = []
547
+ const singletonServices = {
548
+ ...mockSingletonServices,
549
+ logger: {
550
+ ...mockSingletonServices.logger,
551
+ warn: (message: string) => {
552
+ warnings.push(message)
553
+ },
554
+ },
555
+ } as any
556
+
557
+ addTestFunction('versionedBase', {
558
+ func: async () => 'resolved',
559
+ })
560
+
561
+ const result = await runPikkuFunc(
562
+ 'rpc',
563
+ Math.random().toString(),
564
+ 'versionedBase@v2',
565
+ {
566
+ singletonServices,
567
+ data: () => ({}),
568
+ auth: false,
569
+ wire: {},
570
+ }
571
+ )
572
+
573
+ assert.equal(result, 'resolved')
574
+ assert.equal(warnings.length, 1)
575
+ assert.match(
576
+ warnings[0]!,
577
+ /Version 'versionedBase@v2' not registered, resolved to 'versionedBase'/
578
+ )
579
+ })
580
+
581
+ test('should throw when function metadata is missing', async () => {
582
+ addFunction('metaMissing', {
583
+ func: async () => 'ok',
584
+ } as any)
585
+
586
+ await assert.rejects(
587
+ () =>
588
+ runPikkuFunc('rpc', Math.random().toString(), 'metaMissing', {
589
+ singletonServices: mockSingletonServices,
590
+ data: () => ({}),
591
+ auth: false,
592
+ wire: {},
593
+ }),
594
+ {
595
+ message: 'Function meta not found: metaMissing',
596
+ }
597
+ )
598
+ })
599
+
600
+ test('should throw when function config is missing', async () => {
601
+ pikkuState(null, 'function', 'meta').missingConfig = {
602
+ pikkuFuncId: 'missingConfig',
603
+ sessionless: true,
604
+ permissions: [],
605
+ } as any
606
+
607
+ await assert.rejects(
608
+ () =>
609
+ runPikkuFunc('rpc', Math.random().toString(), 'missingConfig', {
610
+ singletonServices: mockSingletonServices,
611
+ data: () => ({}),
612
+ auth: false,
613
+ wire: {},
614
+ }),
615
+ {
616
+ message: 'Function not found: missingConfig',
617
+ }
618
+ )
619
+ })
620
+
621
+ test('should require a session when sessionless metadata is false', async () => {
622
+ addTestFunction('sessionRequired', {
623
+ func: async () => 'ok',
624
+ })
625
+ pikkuState(null, 'function', 'meta').sessionRequired.sessionless = false
626
+
627
+ await assert.rejects(
628
+ () =>
629
+ runPikkuFunc('rpc', Math.random().toString(), 'sessionRequired', {
630
+ singletonServices: mockSingletonServices,
631
+ data: () => ({}),
632
+ auth: true,
633
+ wire: {},
634
+ }),
635
+ {
636
+ message: 'Authentication required',
637
+ }
638
+ )
639
+ })
640
+
641
+ test('should warn when auth is disabled for a session-required function but still enforce session', async () => {
642
+ const warnings: string[] = []
643
+ const singletonServices = {
644
+ ...mockSingletonServices,
645
+ logger: {
646
+ ...mockSingletonServices.logger,
647
+ warn: (message: string) => {
648
+ warnings.push(message)
649
+ },
650
+ },
651
+ } as any
652
+
653
+ addTestFunction('sessionRequired', {
654
+ func: async () => 'ok',
655
+ auth: false,
656
+ })
657
+ pikkuState(null, 'function', 'meta').sessionRequired.sessionless = false
658
+
659
+ await assert.rejects(
660
+ () =>
661
+ runPikkuFunc('rpc', Math.random().toString(), 'sessionRequired', {
662
+ singletonServices,
663
+ data: () => ({}),
664
+ auth: false,
665
+ wire: {},
666
+ }),
667
+ {
668
+ message: 'Authentication required',
669
+ }
670
+ )
671
+
672
+ assert.equal(warnings.length, 1)
673
+ assert.match(
674
+ warnings[0]!,
675
+ /requires a session but auth was explicitly disabled/
676
+ )
677
+ })
678
+
679
+ test('should use backward-compatible auth checks when sessionless metadata is absent', async () => {
680
+ addTestFunction('legacyAuth', {
681
+ func: async () => 'ok',
682
+ auth: true,
683
+ })
684
+ delete pikkuState(null, 'function', 'meta').legacyAuth.sessionless
685
+
686
+ await assert.rejects(
687
+ () =>
688
+ runPikkuFunc('rpc', Math.random().toString(), 'legacyAuth', {
689
+ singletonServices: mockSingletonServices,
690
+ data: () => ({}),
691
+ auth: false,
692
+ wire: {},
693
+ }),
694
+ {
695
+ message: 'Authentication required',
696
+ }
697
+ )
698
+ })
699
+
700
+ test('should reject readonly sessions for non-readonly functions', async () => {
701
+ addTestFunction('readonlyBlocked', {
702
+ func: async () => 'ok',
703
+ })
704
+ pikkuState(null, 'function', 'meta').readonlyBlocked.sessionless = true
705
+
706
+ await assert.rejects(
707
+ () =>
708
+ runPikkuFunc('rpc', Math.random().toString(), 'readonlyBlocked', {
709
+ singletonServices: mockSingletonServices,
710
+ data: () => ({}),
711
+ auth: false,
712
+ wire: { session: { readonly: true } as any },
713
+ }),
714
+ ReadonlySessionError
715
+ )
716
+ })
717
+
718
+ test('should set session helpers on the wire when a session service is provided', async () => {
719
+ const sessionStore = {
720
+ get: async () => undefined,
721
+ }
722
+ const sessionService = new PikkuSessionService(sessionStore as any)
723
+ const wire: any = {}
724
+ let receivedWire: any
725
+
726
+ addTestFunction('sessionHelpers', {
727
+ func: async (_services: any, _data: any, innerWire: any) => {
728
+ receivedWire = innerWire
729
+ innerWire.setSession({ userId: 'u1' })
730
+ return innerWire.getSession()
731
+ },
732
+ })
733
+
734
+ const result = await runPikkuFunc(
735
+ 'rpc',
736
+ Math.random().toString(),
737
+ 'sessionHelpers',
738
+ {
739
+ singletonServices: {
740
+ ...mockSingletonServices,
741
+ sessionStore,
742
+ } as any,
743
+ data: () => ({}),
744
+ auth: false,
745
+ wire,
746
+ sessionService,
747
+ }
748
+ )
749
+
750
+ assert.deepEqual(result, { userId: 'u1' })
751
+ assert.equal(typeof receivedWire.setSession, 'function')
752
+ assert.equal(typeof receivedWire.clearSession, 'function')
753
+ assert.equal(typeof receivedWire.getSession, 'function')
754
+ assert.equal(typeof receivedWire.hasSessionChanged, 'function')
755
+ })
756
+
757
+ test('should load session from sessionStore using pikkuUserId from the wire', async () => {
758
+ let sessionLookups = 0
759
+ let receivedSession: any
760
+
761
+ addTestFunction('loadStoredSession', {
762
+ func: async (_services: any, _data: any, wire: any) => {
763
+ receivedSession = wire.session
764
+ return 'ok'
765
+ },
766
+ })
767
+
768
+ await runPikkuFunc('rpc', Math.random().toString(), 'loadStoredSession', {
769
+ singletonServices: {
770
+ ...mockSingletonServices,
771
+ sessionStore: {
772
+ get: async (userId: string) => {
773
+ sessionLookups++
774
+ assert.equal(userId, 'user-1')
775
+ return { userId, loaded: true }
776
+ },
777
+ },
778
+ } as any,
779
+ data: () => ({}),
780
+ auth: false,
781
+ wire: { pikkuUserId: 'user-1' },
782
+ })
783
+
784
+ assert.equal(sessionLookups, 1)
785
+ assert.deepEqual(receivedSession, { userId: 'user-1', loaded: true })
786
+ })
787
+
788
+ test('should pass addon-scoped singleton services and wrap bare workflow names', async () => {
789
+ pikkuState(null, 'addons', 'packages').set('stripe', {
790
+ package: '@addon/stripe',
791
+ } as any)
792
+
793
+ const workflowCalls: unknown[][] = []
794
+ let singletonFactoryCalls = 0
795
+ let configFactoryCalls = 0
796
+
797
+ pikkuState('@addon/stripe', 'package', 'factories', {
798
+ createConfig: async () => {
799
+ configFactoryCalls++
800
+ return { name: 'addon-config' }
801
+ },
802
+ createSingletonServices: async () => {
803
+ singletonFactoryCalls++
804
+ return {
805
+ logger: mockSingletonServices.logger,
806
+ workflowService: {
807
+ startWorkflow: async (...args: unknown[]) => {
808
+ workflowCalls.push(args)
809
+ return { runId: 'wf-1' }
810
+ },
811
+ },
812
+ }
813
+ },
814
+ } as any)
815
+
816
+ addFunction(
817
+ 'addonFunc',
818
+ {
819
+ func: async (services: any, _data: any) => {
820
+ await services.workflowService.startWorkflow('chargeCustomer', {
821
+ amount: 10,
822
+ })
823
+ await services.workflowService.startWorkflow('stripe:alreadyScoped', {
824
+ amount: 20,
825
+ })
826
+ return 'ok'
827
+ },
828
+ } as any,
829
+ '@addon/stripe'
830
+ )
831
+ pikkuState('@addon/stripe', 'function', 'meta').addonFunc = {
832
+ pikkuFuncId: 'addonFunc',
833
+ sessionless: true,
834
+ permissions: [],
835
+ } as any
836
+
837
+ await runPikkuFunc('rpc', Math.random().toString(), 'addonFunc', {
838
+ singletonServices: {
839
+ ...mockSingletonServices,
840
+ config: { parent: true },
841
+ variables: {},
842
+ } as any,
843
+ data: () => ({}),
844
+ auth: false,
845
+ wire: {},
846
+ packageName: '@addon/stripe',
847
+ })
848
+
849
+ assert.equal(configFactoryCalls, 1)
850
+ assert.equal(singletonFactoryCalls, 1)
851
+ assert.deepEqual(workflowCalls, [
852
+ ['stripe:chargeCustomer', { amount: 10 }],
853
+ ['stripe:alreadyScoped', { amount: 20 }],
854
+ ])
855
+ })
856
+
857
+ test('should reuse cached addon singleton services on subsequent calls', async () => {
858
+ let singletonFactoryCalls = 0
859
+ pikkuState('@addon/cache', 'package', 'factories', {
860
+ createSingletonServices: async () => {
861
+ singletonFactoryCalls++
862
+ return {
863
+ logger: mockSingletonServices.logger,
864
+ }
865
+ },
866
+ } as any)
867
+
868
+ addFunction(
869
+ 'cachedFunc',
870
+ {
871
+ func: async () => 'ok',
872
+ } as any,
873
+ '@addon/cache'
874
+ )
875
+ pikkuState('@addon/cache', 'function', 'meta').cachedFunc = {
876
+ pikkuFuncId: 'cachedFunc',
877
+ sessionless: true,
878
+ permissions: [],
879
+ } as any
880
+
881
+ await runPikkuFunc('rpc', '1', 'cachedFunc', {
882
+ singletonServices: mockSingletonServices,
883
+ data: () => ({}),
884
+ auth: false,
885
+ wire: {},
886
+ packageName: '@addon/cache',
887
+ })
888
+ await runPikkuFunc('rpc', '2', 'cachedFunc', {
889
+ singletonServices: mockSingletonServices,
890
+ data: () => ({}),
891
+ auth: false,
892
+ wire: {},
893
+ packageName: '@addon/cache',
894
+ })
895
+
896
+ assert.equal(singletonFactoryCalls, 1)
897
+ })
898
+
899
+ test('should use addon createWireServices instead of the caller createWireServices', async () => {
900
+ let callerCreateWireServicesUsed = false
901
+ let addonCreateWireServicesUsed = false
902
+ let receivedServices: any
903
+
904
+ pikkuState('@addon/wires', 'package', 'factories', {
905
+ createSingletonServices: async () => ({
906
+ logger: mockSingletonServices.logger,
907
+ }),
908
+ createWireServices: async () => {
909
+ addonCreateWireServicesUsed = true
910
+ return { addonWire: true }
911
+ },
912
+ } as any)
913
+
914
+ addFunction(
915
+ 'wireFunc',
916
+ {
917
+ func: async (services: any) => {
918
+ receivedServices = services
919
+ return 'ok'
920
+ },
921
+ } as any,
922
+ '@addon/wires'
923
+ )
924
+ pikkuState('@addon/wires', 'function', 'meta').wireFunc = {
925
+ pikkuFuncId: 'wireFunc',
926
+ sessionless: true,
927
+ permissions: [],
928
+ } as any
929
+
930
+ await runPikkuFunc('rpc', 'wire', 'wireFunc', {
931
+ singletonServices: mockSingletonServices,
932
+ createWireServices: async () => {
933
+ callerCreateWireServicesUsed = true
934
+ return { callerWire: true }
935
+ },
936
+ data: () => ({}),
937
+ auth: false,
938
+ wire: {},
939
+ packageName: '@addon/wires',
940
+ })
941
+
942
+ assert.equal(callerCreateWireServicesUsed, false)
943
+ assert.equal(addonCreateWireServicesUsed, true)
944
+ assert.deepEqual(receivedServices, {
945
+ logger: mockSingletonServices.logger,
946
+ addonWire: true,
947
+ })
948
+ })
949
+
950
+ test('should lazily memoize the rpc getter on the wire', async () => {
951
+ let receivedWire: any
952
+
953
+ addTestFunction('rpcGetter', {
954
+ func: async (_services: any, _data: any, wire: any) => {
955
+ receivedWire = wire
956
+ const rpcA = wire.rpc
957
+ const rpcB = wire.rpc
958
+ return rpcA === rpcB
959
+ },
960
+ })
961
+
962
+ const result = await runPikkuFunc('rpc', 'rpc-getter', 'rpcGetter', {
963
+ singletonServices: mockSingletonServices,
964
+ data: () => ({}),
965
+ auth: false,
966
+ wire: {},
967
+ })
968
+
969
+ assert.equal(result, true)
970
+ assert.ok(receivedWire.rpc)
971
+ })
972
+ })
973
+
974
+ describe('function-runner helpers', () => {
975
+ test('runPikkuFuncDirectly should pass through the provided wire and session helpers', async () => {
976
+ let receivedWire: any
977
+ const sessionService = new PikkuSessionService({
978
+ get: async () => undefined,
979
+ } as any)
980
+
981
+ addTestFunction('directFunc', {
982
+ func: async (_services: any, _data: any, wire: any) => {
983
+ receivedWire = wire
984
+ return 'ok'
985
+ },
986
+ })
987
+
988
+ const result = await runPikkuFuncDirectly(
989
+ 'directFunc',
990
+ mockServices,
991
+ { traceId: 'trace-1' },
992
+ { hello: 'world' },
993
+ sessionService
994
+ )
995
+
996
+ assert.equal(result, 'ok')
997
+ assert.equal(receivedWire.traceId, 'trace-1')
998
+ assert.equal(typeof receivedWire.getSession, 'function')
999
+ })
1000
+
1001
+ test('getFunctionNames and getAllFunctionNames should include addon namespaces', () => {
1002
+ addTestFunction('rootFunc', { func: async () => 'ok' })
1003
+ pikkuState(null, 'addons', 'packages').set('stripe', {
1004
+ package: '@addon/stripe',
1005
+ } as any)
1006
+ addFunction(
1007
+ 'addonFunc',
1008
+ {
1009
+ func: async () => 'ok',
1010
+ } as any,
1011
+ '@addon/stripe'
1012
+ )
1013
+ pikkuState('@addon/stripe', 'function', 'meta').addonFunc = {
1014
+ pikkuFuncId: 'addonFunc',
1015
+ sessionless: true,
1016
+ permissions: [],
1017
+ } as any
1018
+
1019
+ assert.deepEqual(getFunctionNames().sort(), ['rootFunc'])
1020
+ assert.deepEqual(getFunctionNames('@addon/stripe').sort(), ['addonFunc'])
1021
+ assert.deepEqual(getAllFunctionNames().sort(), [
1022
+ 'rootFunc',
1023
+ 'stripe:addonFunc',
1024
+ ])
1025
+ })
536
1026
  })
@@ -271,10 +271,11 @@ export type CorePikkuFunctionConfig<
271
271
  InputSchema extends StandardSchemaV1 | undefined = undefined,
272
272
  OutputSchema extends StandardSchemaV1 | undefined = undefined,
273
273
  > = {
274
- /** Optional human-readable title for the function */
274
+ /** Short human-readable name (e.g. "Create Todo") */
275
275
  title?: string
276
- /** Optional description of what the function does */
276
+ /** Longer-form description of what the function does */
277
277
  description?: string
278
+ /** Explicit logical name override; lets multiple exports share a versioned base */
278
279
  override?: string
279
280
  version?: number
280
281
  tags?: string[]
@@ -111,7 +111,7 @@ describe('handleHTTPError', () => {
111
111
  )
112
112
 
113
113
  assert.strictEqual(http._state.statusCode, 400)
114
- assert.ok(http._state.jsonBody.message.includes('client error'))
114
+ assert.strictEqual(http._state.jsonBody.message, 'Invalid input')
115
115
  assert.strictEqual(http._state.jsonBody.errorId, 'tracker-2')
116
116
  })
117
117
 
@@ -36,7 +36,10 @@ export const handleHTTPError = (
36
36
  // Set status and response body
37
37
  http?.response?.status(errorResponse.status)
38
38
  http?.response?.json({
39
- message: errorResponse.message,
39
+ message:
40
+ e instanceof Error && e.message && e.message !== 'An error occurred'
41
+ ? e.message
42
+ : errorResponse.message,
40
43
  payload: (e as any).payload,
41
44
  errorId: traceId,
42
45
  })