@pikku/core 0.12.18 → 0.12.20

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 (38) hide show
  1. package/CHANGELOG.md +76 -14
  2. package/dist/function/function-runner.js +53 -3
  3. package/dist/function/functions.types.d.ts +1 -0
  4. package/dist/index.d.ts +1 -1
  5. package/dist/services/content-service.d.ts +66 -37
  6. package/dist/services/in-memory-workflow-service.d.ts +5 -2
  7. package/dist/services/in-memory-workflow-service.js +3 -1
  8. package/dist/services/index.d.ts +1 -1
  9. package/dist/services/local-content.d.ts +10 -12
  10. package/dist/services/local-content.js +46 -37
  11. package/dist/services/workflow-service.d.ts +5 -2
  12. package/dist/wirings/cli/channel/cli-channel-runner.js +4 -2
  13. package/dist/wirings/cli/cli-runner.js +4 -2
  14. package/dist/wirings/rpc/rpc-runner.d.ts +3 -2
  15. package/dist/wirings/rpc/rpc-runner.js +32 -8
  16. package/dist/wirings/workflow/graph/graph-runner.js +4 -1
  17. package/dist/wirings/workflow/index.d.ts +1 -1
  18. package/dist/wirings/workflow/pikku-workflow-service.d.ts +5 -2
  19. package/dist/wirings/workflow/pikku-workflow-service.js +6 -1
  20. package/dist/wirings/workflow/workflow.types.d.ts +14 -0
  21. package/package.json +1 -1
  22. package/src/function/function-runner.ts +68 -3
  23. package/src/function/functions.types.ts +5 -14
  24. package/src/index.ts +10 -1
  25. package/src/services/content-service.ts +79 -51
  26. package/src/services/in-memory-workflow-service.test.ts +21 -0
  27. package/src/services/in-memory-workflow-service.ts +8 -1
  28. package/src/services/index.ts +10 -1
  29. package/src/services/local-content.ts +68 -52
  30. package/src/services/workflow-service.ts +6 -1
  31. package/src/wirings/cli/channel/cli-channel-runner.ts +4 -2
  32. package/src/wirings/cli/cli-runner.ts +4 -2
  33. package/src/wirings/rpc/rpc-runner.ts +45 -18
  34. package/src/wirings/workflow/graph/graph-runner.ts +5 -1
  35. package/src/wirings/workflow/index.ts +1 -0
  36. package/src/wirings/workflow/pikku-workflow-service.ts +13 -2
  37. package/src/wirings/workflow/workflow.types.ts +15 -0
  38. package/tsconfig.tsbuildinfo +1 -1
@@ -1,7 +1,18 @@
1
1
  import { createReadStream, createWriteStream, promises } from 'fs'
2
2
  import { mkdir, readFile } from 'fs/promises'
3
3
  import { resolve, normalize } from 'path'
4
- import type { ContentService, JWTService, Logger } from '@pikku/core/services'
4
+ import type {
5
+ BucketKeyArgs,
6
+ ContentService,
7
+ CopyFileArgs,
8
+ GetUploadURLArgs,
9
+ JWTService,
10
+ Logger,
11
+ SignContentKeyArgs,
12
+ SignURLArgs,
13
+ UploadURLResult,
14
+ WriteFileArgs,
15
+ } from '@pikku/core/services'
5
16
  import { pipeline } from 'stream/promises'
6
17
  import type { Readable } from 'stream'
7
18
 
@@ -20,15 +31,20 @@ export class LocalContent implements ContentService {
20
31
  private jwt?: JWTService
21
32
  ) {}
22
33
 
23
- private safePath(assetKey: string): string {
34
+ private safePath(bucket: string, key: string): string {
24
35
  const base = resolve(this.config.localFileUploadPath)
25
- const target = resolve(base, normalize(assetKey))
36
+ const scoped = resolve(base, normalize(bucket))
37
+ const target = resolve(scoped, normalize(key))
26
38
  if (!target.startsWith(base + '/') && target !== base) {
27
39
  throw new Error('Invalid asset key')
28
40
  }
29
41
  return target
30
42
  }
31
43
 
44
+ private joinKey(bucket: string, key: string): string {
45
+ return `${bucket}/${key}`
46
+ }
47
+
32
48
  public async init() {}
33
49
 
34
50
  private async signParams(
@@ -58,105 +74,105 @@ export class LocalContent implements ContentService {
58
74
  return params.toString()
59
75
  }
60
76
 
61
- public async signURL(
62
- url: string,
63
- dateLessThan: Date,
64
- dateGreaterThan?: Date
65
- ): Promise<string> {
66
- const params = await this.signParams(dateLessThan, dateGreaterThan)
67
- return `${url}?${params}`
77
+ public async signURL(args: SignURLArgs): Promise<string> {
78
+ const params = await this.signParams(
79
+ args.dateLessThan,
80
+ args.dateGreaterThan
81
+ )
82
+ return `${args.url}?${params}`
68
83
  }
69
84
 
70
- public async signContentKey(
71
- assetKey: string,
72
- dateLessThan: Date,
73
- dateGreaterThan?: Date
74
- ): Promise<string> {
85
+ public async signContentKey(args: SignContentKeyArgs): Promise<string> {
86
+ const fullKey = this.joinKey(args.bucket, args.contentKey)
75
87
  const base = this.config.server
76
- ? `${this.config.server}${this.config.assetUrlPrefix}/${assetKey}`
77
- : `${this.config.assetUrlPrefix}/${assetKey}`
78
- return this.signURL(base, dateLessThan, dateGreaterThan)
88
+ ? `${this.config.server}${this.config.assetUrlPrefix}/${fullKey}`
89
+ : `${this.config.assetUrlPrefix}/${fullKey}`
90
+ return this.signURL({
91
+ url: base,
92
+ dateLessThan: args.dateLessThan,
93
+ dateGreaterThan: args.dateGreaterThan,
94
+ })
79
95
  }
80
96
 
81
- public async getUploadURL(assetKey: string) {
82
- this.logger.debug(`Going to upload with key: ${assetKey}`)
97
+ public async getUploadURL(args: GetUploadURLArgs): Promise<UploadURLResult> {
98
+ const fullKey = this.joinKey(args.bucket, args.fileKey)
99
+ this.logger.debug(`Going to upload with key: ${fullKey}`)
83
100
  return {
84
- uploadUrl: `${this.config.uploadUrlPrefix}/${assetKey}`,
85
- assetKey,
101
+ uploadUrl: `${this.config.uploadUrlPrefix}/${fullKey}`,
102
+ assetKey: fullKey,
86
103
  }
87
104
  }
88
105
 
89
- public async writeFile(
90
- assetKey: string,
91
- stream: ReadableStream | NodeJS.ReadableStream
92
- ): Promise<boolean> {
93
- this.logger.debug(`Writing file: ${assetKey}`)
106
+ public async writeFile(args: WriteFileArgs): Promise<boolean> {
107
+ this.logger.debug(`Writing file: ${args.bucket}/${args.key}`)
94
108
 
95
- const path = this.safePath(assetKey)
109
+ const path = this.safePath(args.bucket, args.key)
96
110
 
97
111
  try {
98
112
  await this.createDirectoryForFile(path)
99
113
  const fileStream = createWriteStream(path)
100
- // Use pipeline to properly manage stream piping and errors
101
- await pipeline(stream as Readable, fileStream)
114
+ await pipeline(args.stream as Readable, fileStream)
102
115
  return true
103
116
  } catch (e) {
104
117
  console.error(e)
105
- this.logger.error(`Error writing content ${assetKey}`, e)
118
+ this.logger.error(`Error writing content ${args.bucket}/${args.key}`, e)
106
119
  return false
107
120
  }
108
121
  }
109
122
 
110
- public async copyFile(
111
- assetKey: string,
112
- fromAbsolutePath: string
113
- ): Promise<boolean> {
114
- this.logger.debug(`Writing file: ${assetKey}`)
123
+ public async copyFile(args: CopyFileArgs): Promise<boolean> {
124
+ this.logger.debug(`Writing file: ${args.bucket}/${args.key}`)
115
125
  try {
116
- const path = this.safePath(assetKey)
126
+ const path = this.safePath(args.bucket, args.key)
117
127
  await this.createDirectoryForFile(path)
118
- await promises.copyFile(fromAbsolutePath, path)
128
+ await promises.copyFile(args.fromAbsolutePath, path)
129
+ return true
119
130
  } catch (e) {
120
131
  console.error(e)
121
- this.logger.error(`Error inserting content ${assetKey}`, e)
132
+ this.logger.error(`Error inserting content ${args.bucket}/${args.key}`, e)
122
133
  }
123
134
  return false
124
135
  }
125
136
 
126
137
  public async readFile(
127
- assetKey: string
138
+ args: BucketKeyArgs
128
139
  ): Promise<ReadableStream | NodeJS.ReadableStream> {
129
- this.logger.debug(`Getting key: ${assetKey}`)
140
+ this.logger.debug(`Getting key: ${args.bucket}/${args.key}`)
130
141
 
131
- const filePath = this.safePath(assetKey)
142
+ const filePath = this.safePath(args.bucket, args.key)
132
143
 
133
144
  try {
134
145
  const stream = createReadStream(filePath)
135
- // Handle early stream errors (like file not found, permission denied, etc.)
136
146
  stream.on('error', (err) => {
137
- this.logger.error(`Error getting content ${assetKey}`, err)
147
+ this.logger.error(
148
+ `Error getting content ${args.bucket}/${args.key}`,
149
+ err
150
+ )
138
151
  })
139
152
 
140
153
  return stream
141
154
  } catch (e) {
142
- this.logger.error(`Error setting up stream for ${assetKey}`, e)
155
+ this.logger.error(
156
+ `Error setting up stream for ${args.bucket}/${args.key}`,
157
+ e
158
+ )
143
159
  throw e
144
160
  }
145
161
  }
146
162
 
147
- public async readFileAsBuffer(assetKey: string): Promise<Buffer> {
148
- const filePath = this.safePath(assetKey)
149
- this.logger.debug(`Reading file as buffer: ${assetKey}`)
163
+ public async readFileAsBuffer(args: BucketKeyArgs): Promise<Buffer> {
164
+ const filePath = this.safePath(args.bucket, args.key)
165
+ this.logger.debug(`Reading file as buffer: ${args.bucket}/${args.key}`)
150
166
  return readFile(filePath)
151
167
  }
152
168
 
153
- public async deleteFile(assetKey: string): Promise<boolean> {
154
- this.logger.debug(`deleting key: ${assetKey}`)
169
+ public async deleteFile(args: BucketKeyArgs): Promise<boolean> {
170
+ this.logger.debug(`deleting key: ${args.bucket}/${args.key}`)
155
171
  try {
156
- await promises.unlink(this.safePath(assetKey))
172
+ await promises.unlink(this.safePath(args.bucket, args.key))
157
173
  return true
158
174
  } catch (e: any) {
159
- this.logger.error(`Error deleting content ${assetKey}`, e)
175
+ this.logger.error(`Error deleting content ${args.bucket}/${args.key}`, e)
160
176
  }
161
177
  return false
162
178
  }
@@ -1,6 +1,7 @@
1
1
  import type { SerializedError } from '../types/core.types.js'
2
2
  import type {
3
3
  WorkflowRun,
4
+ WorkflowPlannedStep,
4
5
  WorkflowRunWire,
5
6
  WorkflowRunStatus,
6
7
  StepState,
@@ -19,7 +20,11 @@ export interface WorkflowService {
19
20
  input: any,
20
21
  inline: boolean,
21
22
  graphHash: string,
22
- wire: WorkflowRunWire
23
+ wire: WorkflowRunWire,
24
+ options?: {
25
+ deterministic?: boolean
26
+ plannedSteps?: WorkflowPlannedStep[]
27
+ }
23
28
  ): Promise<string>
24
29
  getRun(id: string): Promise<WorkflowRun | null>
25
30
  getRunStatus(id: string): Promise<WorkflowRunStatus | null>
@@ -3,10 +3,12 @@ import type { CorePikkuCLIRender, CLIMeta } from '../cli.types.js'
3
3
  import { generateCommandHelp, parseCLIArguments } from '../command-parser.js'
4
4
 
5
5
  /**
6
- * Default JSON renderer for CLI output
6
+ * Default JSON renderer for CLI output. Emits single-line NDJSON so
7
+ * the output stays machine-parseable when the CLI is run with
8
+ * `--output json`; pretty-printing here would break NDJSON consumers.
7
9
  */
8
10
  const defaultJSONRenderer: CorePikkuCLIRender<any> = (_services, data) => {
9
- console.log(JSON.stringify(data, null, 2))
11
+ console.log(JSON.stringify(data))
10
12
  }
11
13
 
12
14
  /**
@@ -45,10 +45,12 @@ export class CLIError extends Error {
45
45
  }
46
46
 
47
47
  /**
48
- * Default JSON renderer for CLI output
48
+ * Default JSON renderer for CLI output. Emits single-line NDJSON so
49
+ * the output stays machine-parseable when the CLI is run with
50
+ * `--output json`; pretty-printing here would break NDJSON consumers.
49
51
  */
50
52
  const defaultJSONRenderer: CorePikkuCLIRender<any> = (_services, data) => {
51
- console.log(JSON.stringify(data, null, 2))
53
+ console.log(JSON.stringify(data))
52
54
  }
53
55
 
54
56
  /**
@@ -54,7 +54,25 @@ export const resolveNamespace = (
54
54
  }
55
55
  }
56
56
 
57
- const getPikkuFunctionName = (rpcName: string): string => {
57
+ /**
58
+ * Resolve a bare (non-namespaced) RPC name to its pikkuFuncId, preferring
59
+ * the caller's addon package when provided. Returns the function name plus
60
+ * the package scope that resolved it (null = root), so callers can thread
61
+ * the scope into runPikkuFunc without a second lookup.
62
+ */
63
+ const resolvePikkuFunction = (
64
+ rpcName: string,
65
+ packageName: string | null = null
66
+ ): { pikkuFuncId: string; packageName: string | null } => {
67
+ // Addon-scoped calls: try the caller's package function meta first.
68
+ // (RPC meta only lives in root; addon functions are registered under their package.)
69
+ if (packageName) {
70
+ const pkgFunctions = pikkuState(packageName, 'function', 'meta')
71
+ const pkgMeta = pkgFunctions?.[rpcName]
72
+ if (pkgMeta) {
73
+ return { pikkuFuncId: pkgMeta.pikkuFuncId || rpcName, packageName }
74
+ }
75
+ }
58
76
  const rpc = pikkuState(null, 'rpc', 'meta')
59
77
  let rpcMeta = rpc[rpcName]
60
78
  if (!rpcMeta) {
@@ -66,7 +84,7 @@ const getPikkuFunctionName = (rpcName: string): string => {
66
84
  if (!rpcMeta) {
67
85
  throw new RPCNotFoundError(rpcName)
68
86
  }
69
- return rpcMeta
87
+ return { pikkuFuncId: rpcMeta, packageName: null }
70
88
  }
71
89
 
72
90
  // Context-aware RPC client for use within services
@@ -77,7 +95,8 @@ export class ContextAwareRPCService {
77
95
  private options: {
78
96
  requiresAuth?: boolean
79
97
  sessionService?: SessionService<CoreUserSession>
80
- }
98
+ },
99
+ private packageName: string | null = null
81
100
  ) {}
82
101
 
83
102
  public async rpcExposed(funcName: string, data: any): Promise<any> {
@@ -131,17 +150,22 @@ export class ContextAwareRPCService {
131
150
  }
132
151
  }
133
152
 
134
- // Try local function, then fall back to deployment service (remote)
153
+ // Bare name: resolve via caller's package scope first (if any), then root.
154
+ // Note: intra-addon bare calls do NOT re-apply the addon's external
155
+ // addonConfig.auth/tags — those gates are only applied on the external
156
+ // 'namespace:func' boundary via invokeAddonFunction.
135
157
  try {
158
+ const resolved = resolvePikkuFunction(funcName, this.packageName)
136
159
  return await runPikkuFunc<In, Out>(
137
160
  'rpc',
138
161
  funcName,
139
- getPikkuFunctionName(funcName),
162
+ resolved.pikkuFuncId,
140
163
  {
141
164
  auth: this.options.requiresAuth,
142
165
  singletonServices: this.services,
143
166
  data: () => data,
144
167
  wire: updatedWire,
168
+ packageName: resolved.packageName,
145
169
  }
146
170
  )
147
171
  } catch (e) {
@@ -232,17 +256,14 @@ export class ContextAwareRPCService {
232
256
  }
233
257
 
234
258
  try {
235
- return await runPikkuFunc<In, Out>(
236
- 'rpc',
237
- rpcName,
238
- getPikkuFunctionName(rpcName),
239
- {
240
- auth: this.options.requiresAuth,
241
- singletonServices: this.services,
242
- data: () => data,
243
- wire: mergedWire,
244
- }
245
- )
259
+ const resolved = resolvePikkuFunction(rpcName, this.packageName)
260
+ return await runPikkuFunc<In, Out>('rpc', rpcName, resolved.pikkuFuncId, {
261
+ auth: this.options.requiresAuth,
262
+ singletonServices: this.services,
263
+ data: () => data,
264
+ wire: mergedWire,
265
+ packageName: resolved.packageName,
266
+ })
246
267
  } catch (e) {
247
268
  if (e instanceof RPCNotFoundError && this.services.deploymentService) {
248
269
  const session =
@@ -406,14 +427,20 @@ export class PikkuRPCService<
406
427
  sessionService?: SessionService<CoreUserSession>
407
428
  }
408
429
  | undefined,
409
- depth: number = 0
430
+ depth: number = 0,
431
+ packageName: string | null = null
410
432
  ): TypedRPC {
411
433
  const options =
412
434
  typeof requiresAuthOrOptions === 'object' &&
413
435
  requiresAuthOrOptions !== null
414
436
  ? requiresAuthOrOptions
415
437
  : { requiresAuth: requiresAuthOrOptions }
416
- const serviceRPC = new ContextAwareRPCService(services, wire, options)
438
+ const serviceRPC = new ContextAwareRPCService(
439
+ services,
440
+ wire,
441
+ options,
442
+ packageName
443
+ )
417
444
  return {
418
445
  depth,
419
446
  global: false,
@@ -852,7 +852,11 @@ export async function runWorkflowGraph(
852
852
  triggerInput,
853
853
  inline ?? false,
854
854
  meta.graphHash,
855
- wire ?? { type: 'unknown' }
855
+ wire ?? { type: 'unknown' },
856
+ {
857
+ deterministic: meta.deterministic,
858
+ plannedSteps: meta.plannedSteps,
859
+ }
856
860
  )
857
861
 
858
862
  if (inline) {
@@ -42,6 +42,7 @@ export type {
42
42
  export type {
43
43
  WorkflowService,
44
44
  WorkflowServiceConfig,
45
+ WorkflowPlannedStep,
45
46
  WorkflowRunWire,
46
47
  WorkflowStatus,
47
48
  WorkflowVersionStatus,
@@ -49,6 +49,7 @@ import type {
49
49
  PikkuWorkflowWire,
50
50
  StepState,
51
51
  StepStatus,
52
+ WorkflowPlannedStep,
52
53
  WorkflowRun,
53
54
  WorkflowRunStatus,
54
55
  WorkflowRunWire,
@@ -272,7 +273,11 @@ export abstract class PikkuWorkflowService implements WorkflowService {
272
273
  input: any,
273
274
  inline: boolean,
274
275
  graphHash: string,
275
- wire: WorkflowRunWire
276
+ wire: WorkflowRunWire,
277
+ options?: {
278
+ deterministic?: boolean
279
+ plannedSteps?: WorkflowPlannedStep[]
280
+ }
276
281
  ): Promise<string>
277
282
 
278
283
  /**
@@ -323,6 +328,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
323
328
  status: run.status,
324
329
  startedAt: run.createdAt,
325
330
  completedAt: terminalStatuses.has(run.status) ? run.updatedAt : undefined,
331
+ deterministic: run.deterministic,
332
+ plannedSteps: run.plannedSteps,
326
333
  steps,
327
334
  output: run.status === 'completed' ? run.output : undefined,
328
335
  error: run.error
@@ -689,7 +696,11 @@ export abstract class PikkuWorkflowService implements WorkflowService {
689
696
  input,
690
697
  shouldInline,
691
698
  workflowMeta.graphHash,
692
- wire
699
+ wire,
700
+ {
701
+ deterministic: workflowMeta.deterministic,
702
+ plannedSteps: workflowMeta.plannedSteps,
703
+ }
693
704
  )
694
705
 
695
706
  if (shouldInline) {
@@ -53,6 +53,11 @@ export interface WorkflowServiceConfig {
53
53
  sleeperRPCName: string
54
54
  }
55
55
 
56
+ export interface WorkflowPlannedStep {
57
+ /** Human-readable step label for UI timeline */
58
+ stepName: string
59
+ }
60
+
56
61
  /**
57
62
  * Workflow run status
58
63
  */
@@ -99,6 +104,10 @@ export interface WorkflowRun {
99
104
  inline?: boolean
100
105
  /** Graph hash of the workflow definition at run creation time */
101
106
  graphHash?: string
107
+ /** True when the workflow has a static, pre-computable step timeline */
108
+ deterministic?: boolean
109
+ /** Static planned steps snapshot captured at run start */
110
+ plannedSteps?: WorkflowPlannedStep[]
102
111
  /** Wire origin info (how this run was started) */
103
112
  wire: WorkflowRunWire
104
113
  /** Creation timestamp */
@@ -146,6 +155,8 @@ export interface WorkflowRunStatus {
146
155
  status: WorkflowStatus
147
156
  startedAt: Date
148
157
  completedAt?: Date
158
+ deterministic?: boolean
159
+ plannedSteps?: WorkflowPlannedStep[]
149
160
  steps: Array<{
150
161
  name: string
151
162
  status: StepStatus
@@ -270,6 +281,10 @@ export interface WorkflowRuntimeMeta {
270
281
  entryNodeIds?: string[]
271
282
  /** Hash of graph topology (nodes, edges, input mappings) */
272
283
  graphHash?: string
284
+ /** True when the workflow has a static, pre-computable step timeline */
285
+ deterministic?: boolean
286
+ /** Static planned steps metadata for deterministic workflows */
287
+ plannedSteps?: WorkflowPlannedStep[]
273
288
  }
274
289
 
275
290
  /**