@pikku/core 0.12.19 → 0.12.21

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 (146) hide show
  1. package/CHANGELOG.md +77 -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/function-runner.js +53 -3
  6. package/dist/function/functions.types.d.ts +3 -2
  7. package/dist/index.d.ts +3 -3
  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/content-service.d.ts +66 -37
  21. package/dist/services/in-memory-queue-service.js +1 -1
  22. package/dist/services/in-memory-workflow-service.d.ts +15 -13
  23. package/dist/services/in-memory-workflow-service.js +31 -13
  24. package/dist/services/index.d.ts +1 -1
  25. package/dist/services/local-content.d.ts +10 -12
  26. package/dist/services/local-content.js +54 -38
  27. package/dist/services/user-session-service.d.ts +1 -1
  28. package/dist/testing/service-tests.js +24 -0
  29. package/dist/types/core.types.d.ts +4 -0
  30. package/dist/types/state.types.d.ts +2 -18
  31. package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
  32. package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
  33. package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
  34. package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
  35. package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
  36. package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
  37. package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
  38. package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
  39. package/dist/wirings/channel/channel-handler.js +1 -1
  40. package/dist/wirings/channel/channel-store.d.ts +13 -0
  41. package/dist/wirings/channel/channel.types.d.ts +3 -0
  42. package/dist/wirings/channel/local/local-channel-runner.js +23 -5
  43. package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
  44. package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
  45. package/dist/wirings/cli/cli-runner.js +24 -5
  46. package/dist/wirings/cli/command-parser.js +19 -4
  47. package/dist/wirings/http/http-runner.js +72 -36
  48. package/dist/wirings/http/http.types.d.ts +1 -0
  49. package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
  50. package/dist/wirings/http/web-request.js +32 -0
  51. package/dist/wirings/rpc/rpc-runner.d.ts +3 -2
  52. package/dist/wirings/rpc/rpc-runner.js +45 -11
  53. package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
  54. package/dist/wirings/workflow/graph/graph-node.js +4 -0
  55. package/dist/wirings/workflow/graph/graph-runner.js +12 -10
  56. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
  57. package/dist/wirings/workflow/index.d.ts +1 -1
  58. package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
  59. package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
  60. package/dist/wirings/workflow/workflow.types.d.ts +34 -0
  61. package/package.json +1 -1
  62. package/run-tests.sh +4 -1
  63. package/src/dev/hot-reload.test.ts +62 -11
  64. package/src/dev/hot-reload.ts +28 -7
  65. package/src/errors/error-handler.ts +8 -2
  66. package/src/errors/error.test.ts +2 -0
  67. package/src/function/function-runner.test.ts +500 -10
  68. package/src/function/function-runner.ts +68 -3
  69. package/src/function/functions.types.ts +3 -2
  70. package/src/index.ts +22 -3
  71. package/src/middleware/index.ts +11 -2
  72. package/src/middleware/telemetry.ts +2 -2
  73. package/src/middleware-runner.test.ts +16 -16
  74. package/src/middleware-runner.ts +42 -30
  75. package/src/permissions.test.ts +27 -24
  76. package/src/permissions.ts +41 -25
  77. package/src/pikku-request.test.ts +35 -0
  78. package/src/pikku-request.ts +1 -1
  79. package/src/pikku-state.ts +2 -3
  80. package/src/run-tests-script.test.ts +18 -0
  81. package/src/services/ai-agent-runner-service.ts +1 -0
  82. package/src/services/content-service.ts +79 -51
  83. package/src/services/in-memory-queue-service.ts +1 -1
  84. package/src/services/in-memory-session-store.ts +1 -2
  85. package/src/services/in-memory-workflow-service.test.ts +33 -11
  86. package/src/services/in-memory-workflow-service.ts +49 -13
  87. package/src/services/index.ts +10 -1
  88. package/src/services/local-content.test.ts +54 -0
  89. package/src/services/local-content.ts +80 -53
  90. package/src/services/typed-credential-service.ts +3 -3
  91. package/src/services/typed-secret-service.ts +3 -3
  92. package/src/services/typed-variables-service.ts +3 -3
  93. package/src/services/user-session-service.ts +4 -4
  94. package/src/testing/service-tests.ts +30 -0
  95. package/src/types/core.types.ts +6 -2
  96. package/src/types/state.types.ts +2 -13
  97. package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
  98. package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
  99. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
  100. package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
  101. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
  102. package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
  103. package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
  104. package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
  105. package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
  106. package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
  107. package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
  108. package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
  109. package/src/wirings/channel/channel-handler.test.ts +272 -0
  110. package/src/wirings/channel/channel-handler.ts +1 -1
  111. package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
  112. package/src/wirings/channel/channel-store.ts +19 -0
  113. package/src/wirings/channel/channel.types.ts +4 -0
  114. package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
  115. package/src/wirings/channel/local/local-channel-runner.ts +41 -5
  116. package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
  117. package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
  118. package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
  119. package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
  120. package/src/wirings/cli/cli-runner.test.ts +255 -2
  121. package/src/wirings/cli/cli-runner.ts +31 -10
  122. package/src/wirings/cli/cli.types.ts +4 -8
  123. package/src/wirings/cli/command-parser.test.ts +83 -0
  124. package/src/wirings/cli/command-parser.ts +23 -4
  125. package/src/wirings/http/http-runner.test.ts +296 -1
  126. package/src/wirings/http/http-runner.ts +87 -57
  127. package/src/wirings/http/http.types.ts +11 -26
  128. package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
  129. package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
  130. package/src/wirings/http/web-request.test.ts +115 -0
  131. package/src/wirings/http/web-request.ts +43 -5
  132. package/src/wirings/mcp/mcp-runner.test.ts +367 -0
  133. package/src/wirings/queue/queue.types.ts +2 -5
  134. package/src/wirings/rpc/rpc-runner.test.ts +511 -0
  135. package/src/wirings/rpc/rpc-runner.ts +60 -21
  136. package/src/wirings/rpc/wire-addon.test.ts +57 -0
  137. package/src/wirings/workflow/graph/graph-node.ts +12 -0
  138. package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
  139. package/src/wirings/workflow/graph/graph-runner.ts +28 -10
  140. package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
  141. package/src/wirings/workflow/index.ts +1 -0
  142. package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
  143. package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
  144. package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
  145. package/src/wirings/workflow/workflow.types.ts +68 -5
  146. package/tsconfig.tsbuildinfo +1 -1
@@ -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
  })
@@ -72,6 +72,54 @@ async function resolveSession(
72
72
  * @param parentServices - The parent/caller's singleton services (used as base)
73
73
  * @returns The package's singleton services
74
74
  */
75
+ /**
76
+ * Find the consumer-defined namespace (from wireAddon) for a given addon package.
77
+ * Returns null if the package isn't registered as an addon.
78
+ */
79
+ const findAddonNamespaceForPackage = (packageName: string): string | null => {
80
+ const addons = pikkuState(null, 'addons', 'packages')
81
+ if (!addons) return null
82
+ for (const [namespace, cfg] of addons.entries()) {
83
+ if (cfg?.package === packageName) return namespace
84
+ }
85
+ return null
86
+ }
87
+
88
+ /**
89
+ * Wrap a workflow service so that bare workflow names passed from inside an
90
+ * addon function are auto-prefixed with the addon's consumer-facing namespace.
91
+ * Without this, `runToCompletion('myWorkflow')` from inside an addon misses
92
+ * the workflow registered under the addon's package scope and throws
93
+ * WorkflowNotFoundError — forcing addons to hardcode their consumer-defined
94
+ * namespace, which couples the addon to its caller.
95
+ *
96
+ * Explicit `'ns:name'` and bare names that already exist in root meta are
97
+ * unaffected; only bare names that would otherwise miss resolution get
98
+ * prefixed.
99
+ */
100
+ const wrapWorkflowServiceForPackage = <T extends object>(
101
+ service: T,
102
+ packageName: string
103
+ ): T => {
104
+ return new Proxy(service, {
105
+ get(target, prop, receiver) {
106
+ if (prop === 'startWorkflow' || prop === 'runToCompletion') {
107
+ const original = Reflect.get(target, prop, receiver) as Function
108
+ return function (this: any, name: string, ...rest: any[]) {
109
+ if (typeof name === 'string' && !name.includes(':')) {
110
+ const namespace = findAddonNamespaceForPackage(packageName)
111
+ if (namespace) {
112
+ name = `${namespace}:${name}`
113
+ }
114
+ }
115
+ return original.call(this, name, ...rest)
116
+ }
117
+ }
118
+ return Reflect.get(target, prop, receiver)
119
+ },
120
+ })
121
+ }
122
+
75
123
  const getOrCreatePackageSingletonServices = async (
76
124
  packageName: string,
77
125
  parentServices: CoreSingletonServices
@@ -101,6 +149,18 @@ const getOrCreatePackageSingletonServices = async (
101
149
  parentServices
102
150
  )
103
151
 
152
+ // Wrap workflowService so that bare names used inside the addon's functions
153
+ // resolve to workflows registered under the addon's package scope.
154
+ if (
155
+ packageServices.workflowService &&
156
+ typeof packageServices.workflowService === 'object'
157
+ ) {
158
+ packageServices.workflowService = wrapWorkflowServiceForPackage(
159
+ packageServices.workflowService as object,
160
+ packageName
161
+ ) as typeof packageServices.workflowService
162
+ }
163
+
104
164
  // Cache the services
105
165
  pikkuState(packageName, 'package', 'singletonServices', packageServices)
106
166
 
@@ -378,11 +438,16 @@ export const runPikkuFunc = async <In = any, Out = any>(
378
438
  wireServices && Object.keys(wireServices).length > 0
379
439
  ? { ...resolvedSingletonServices, ...wireServices }
380
440
  : resolvedSingletonServices
441
+ const callerPackageName = packageName
381
442
  Object.defineProperty(resolvedWire, 'rpc', {
382
443
  get() {
383
- const rpc = rpcService.getContextRPCService(services, resolvedWire, {
384
- sessionService,
385
- })
444
+ const rpc = rpcService.getContextRPCService(
445
+ services,
446
+ resolvedWire,
447
+ { sessionService },
448
+ 0,
449
+ callerPackageName
450
+ )
386
451
  Object.defineProperty(resolvedWire, 'rpc', {
387
452
  value: rpc,
388
453
  writable: true,
@@ -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[]