@pikku/core 0.12.56 → 0.12.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/dev/hot-reload.d.ts +7 -3
- package/dist/dev/hot-reload.js +90 -47
- package/dist/dev/reload-meta.d.ts +24 -0
- package/dist/dev/reload-meta.js +99 -0
- package/dist/services/local-gateway-service.js +4 -3
- package/dist/wirings/gateway/gateway-runner.d.ts +2 -1
- package/dist/wirings/gateway/gateway-runner.js +31 -9
- package/dist/wirings/gateway/gateway.types.d.ts +14 -3
- package/dist/wirings/gateway/index.d.ts +2 -2
- package/dist/wirings/gateway/index.js +1 -1
- package/dist/wirings/workflow/graph/graph-runner.js +14 -0
- package/package.json +2 -2
- package/src/dev/hot-reload.test.ts +15 -8
- package/src/dev/hot-reload.ts +99 -54
- package/src/dev/reload-meta.test.ts +154 -0
- package/src/dev/reload-meta.ts +138 -0
- package/src/services/local-gateway-service.ts +7 -3
- package/src/wirings/gateway/gateway-runner.test.ts +70 -0
- package/src/wirings/gateway/gateway-runner.ts +37 -8
- package/src/wirings/gateway/gateway.types.ts +17 -2
- package/src/wirings/gateway/index.ts +6 -1
- package/src/wirings/workflow/graph/graph-runner.test.ts +134 -0
- package/src/wirings/workflow/graph/graph-runner.ts +12 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## 0.12.58
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 7b17b14: Allow a workflow-graph node's `func` to reference a registered AI agent by name, dispatched as an agent run — exactly like sub-workflows. `executeGraphStep`/`executeGraphNodeInline` now check the agent registry and dispatch matching nodes via the agent-run path (`rpc.agent.run`), so the node's result is the agent's declared output and downstream nodes can `ref()` it. The generated `pikkuWorkflowGraph` wrapper widens its node-func union to also accept `keyof FlattenedWorkflowMap` and `keyof FlattenedAgentMap`, and `ref()` resolves an agent node's output keys.
|
|
6
|
+
- daec082: Drop Node 22 support — the minimum supported runtime is now Node 24 (LTS).
|
|
7
|
+
|
|
8
|
+
Node 22 deadlocks `pikku dev` at `loadUserBootstrap` (tsx `register()` + `require(esm)` cycle handling on node 22.12+), and Node 20 is already below our floor. The `engines.node` requirement is raised to `>=24` across all packages, matching `.nvmrc` and the CI test matrix. Closes #751.
|
|
9
|
+
|
|
10
|
+
- e0fd352: wireGateway: allow `adapter` to be a factory `(services) => GatewayAdapter | Promise<GatewayAdapter>`, resolved lazily on first inbound request (webhook/websocket) or gateway start (listener) and cached. Real platform adapters (WhatsApp Cloud API, Slack) need secrets that only exist after boot, while wireGateway runs at module load — a factory bridges that. Factory adapters register the GET verify route unconditionally since verifyWebhook can't be probed before first resolve.
|
|
11
|
+
|
|
12
|
+
## 0.12.57
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- 60ad8cb: fix dev-server hot reload so edited AND new functions/routes apply without a restart
|
|
17
|
+
- `@pikku/core`: the hot reloader fed raw zod `input`/`output` schemas into the JSON-schema map, so `compileAllSchemas` threw `Failed to compile schema` on every reload and the reload aborted (only the function body sometimes swapped, half-updated). It now registers function implementations only and leaves schemas to the codegen JSON output. New function exports are registered too (previously only already-registered names were replaced). Reloads write into the startup functions map directly to avoid a race with the dev watcher's codegen-scoped state swap, and re-import via a uniquely-named sibling copy since neither Bun nor tsx bust the module cache on a `?t=` query.
|
|
18
|
+
- New `reloadGeneratedMeta` (exported from `@pikku/core/dev`) re-reads the regenerated wiring meta + JSON schemas into the running process so new/changed routes, RPCs, queues and agents resolve without a restart.
|
|
19
|
+
- `@pikku/cli`: `pikku dev` now calls `reloadGeneratedMeta` after each watch-triggered codegen pass and re-imports the changed files once fresh meta is in state, so a NEW route in a changed wiring file registers (its `wireHTTP` no longer no-ops on missing meta).
|
|
20
|
+
- `@pikku/schema-cfworker`: `compileSchema` recompiles when a schema's value changes (not only on first sight), so hot-reloaded schemas take effect.
|
|
21
|
+
|
|
22
|
+
- 8f5c998: Fix dev hot-reload dropping runtime-registered function/queue meta. `reloadGeneratedMeta` replaced the whole `function`/`queue` meta maps with the generated JSON, wiping entries the framework registers at service-init (the workflow orchestrator, per-workflow queue workers, and other `addFunction`'d internals that never appear in the generated files). Workflow jobs then failed with `Function meta not found: pikkuWorkflowOrchestrator`. The reload now merges over the existing maps so those internals survive.
|
|
23
|
+
|
|
1
24
|
## 0.12.56
|
|
2
25
|
|
|
3
26
|
### Patch Changes
|
package/dist/dev/hot-reload.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { Logger } from '../services/logger.js';
|
|
2
|
+
export * from './reload-meta.js';
|
|
2
3
|
interface PikkuDevReloaderOptions {
|
|
3
4
|
srcDirectories: string[];
|
|
4
5
|
logger: Logger;
|
|
5
6
|
pikkuDir?: string;
|
|
6
7
|
}
|
|
7
|
-
export
|
|
8
|
+
export interface PikkuDevReloaderHandle {
|
|
8
9
|
close: () => void;
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
/** Re-import every file changed since the last drain (post-codegen, once
|
|
11
|
+
* fresh meta is in state). */
|
|
12
|
+
reimportPending: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare function pikkuDevReloader(options: PikkuDevReloaderOptions): Promise<PikkuDevReloaderHandle>;
|
package/dist/dev/hot-reload.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { watch } from 'node:fs';
|
|
2
|
-
import { stat, readFile } from 'node:fs/promises';
|
|
3
|
-
import { join, resolve, relative } from 'node:path';
|
|
2
|
+
import { stat, readFile, copyFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join, resolve, relative } from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { register } from 'tsx/esm/api';
|
|
6
6
|
import { pikkuState } from '../pikku-state.js';
|
|
7
|
-
import { addFunction } from '../function/function-runner.js';
|
|
8
7
|
import { clearMiddlewareCache } from '../middleware-runner.js';
|
|
9
8
|
import { clearPermissionsCache } from '../permissions.js';
|
|
10
9
|
import { clearChannelMiddlewareCache } from '../wirings/channel/channel-middleware-runner.js';
|
|
11
10
|
import { httpRouter } from '../wirings/http/routers/http-router.js';
|
|
12
|
-
|
|
11
|
+
export * from './reload-meta.js';
|
|
13
12
|
const isFunctionConfig = (value) => {
|
|
14
13
|
return (typeof value === 'object' &&
|
|
15
14
|
value !== null &&
|
|
@@ -44,11 +43,31 @@ const ensureTsxRegistered = () => {
|
|
|
44
43
|
register();
|
|
45
44
|
tsxRegistered = true;
|
|
46
45
|
};
|
|
46
|
+
let tempCounter = 0;
|
|
47
47
|
const reimportModule = async (filePath, useTsx = false) => {
|
|
48
48
|
try {
|
|
49
49
|
if (useTsx) {
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
// Import a uniquely-named sibling copy: a `?t=` query does NOT bust
|
|
51
|
+
// the cache on either runtime (Bun keys module identity on the bare
|
|
52
|
+
// path; tsx's transform cache keys on the file path too), so a fresh
|
|
53
|
+
// path is the only reliable re-import. Same directory → identical
|
|
54
|
+
// resolution for relative and package-`imports` (#…) specifiers. The
|
|
55
|
+
// dot-prefix keeps it out of the watcher.
|
|
56
|
+
if (!process.versions.bun) {
|
|
57
|
+
// Node needs tsx's loader to import raw .ts; Bun imports it natively.
|
|
58
|
+
ensureTsxRegistered();
|
|
59
|
+
}
|
|
60
|
+
const abs = resolve(filePath);
|
|
61
|
+
const tempPath = join(dirname(abs), `.pikku-hot-${++tempCounter}-${basename(abs)}`);
|
|
62
|
+
await copyFile(abs, tempPath);
|
|
63
|
+
try {
|
|
64
|
+
return await import(pathToFileURL(tempPath).href);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await rm(tempPath, { force: true }).catch(() => {
|
|
68
|
+
// Best-effort temp cleanup; a leftover dotfile is watcher-ignored.
|
|
69
|
+
});
|
|
70
|
+
}
|
|
52
71
|
}
|
|
53
72
|
const content = await readFile(resolve(filePath), 'utf-8');
|
|
54
73
|
const dataUrl = 'data:text/javascript;base64,' + Buffer.from(content).toString('base64');
|
|
@@ -62,7 +81,10 @@ const isWatchedTsFile = (filename) => {
|
|
|
62
81
|
return (filename.endsWith('.ts') &&
|
|
63
82
|
!filename.endsWith('.test.ts') &&
|
|
64
83
|
!filename.endsWith('.d.ts') &&
|
|
65
|
-
!filename.endsWith('.gen.ts')
|
|
84
|
+
!filename.endsWith('.gen.ts') &&
|
|
85
|
+
// Hidden files: editor/sed atomic-write temps and our own hot-reload
|
|
86
|
+
// sibling copies must never trigger a reload of themselves.
|
|
87
|
+
!basename(filename).startsWith('.'));
|
|
66
88
|
};
|
|
67
89
|
export async function pikkuDevReloader(options) {
|
|
68
90
|
const { srcDirectories, logger, pikkuDir = '.pikku' } = options;
|
|
@@ -73,6 +95,7 @@ export async function pikkuDevReloader(options) {
|
|
|
73
95
|
const handleFileChange = async (changedTsFile) => {
|
|
74
96
|
const start = Date.now();
|
|
75
97
|
const reloadedNames = [];
|
|
98
|
+
const addedNames = [];
|
|
76
99
|
const srcDir = absSrcDirs.find((d) => changedTsFile.startsWith(d));
|
|
77
100
|
if (!srcDir)
|
|
78
101
|
return;
|
|
@@ -87,48 +110,59 @@ export async function pikkuDevReloader(options) {
|
|
|
87
110
|
logger.error(`Failed to import: ${relative(process.cwd(), importPath)} (keeping old code)`);
|
|
88
111
|
return;
|
|
89
112
|
}
|
|
90
|
-
|
|
113
|
+
// Register every function-config export — replacing known functions AND
|
|
114
|
+
// adding new ones (a brand-new function becomes callable as soon as its
|
|
115
|
+
// meta lands via reloadGeneratedMeta after the next codegen pass).
|
|
116
|
+
// Write into the map captured at startup, NOT pikkuState's current one:
|
|
117
|
+
// a dev-server watcher may have temporarily swapped in a codegen-scoped
|
|
118
|
+
// map for the same file event (runAllWithCommandState), and a write to
|
|
119
|
+
// that map is silently discarded when it restores the original.
|
|
120
|
+
// Schemas are NOT touched here: `input`/`output` hold raw zod schemas,
|
|
121
|
+
// while the schema map carries codegen-generated JSON schemas — mixing the
|
|
122
|
+
// two crashed every reload. Fresh JSON schemas arrive via
|
|
123
|
+
// reloadGeneratedMeta once codegen has re-emitted them.
|
|
91
124
|
for (const [exportName, exportValue] of Object.entries(mod)) {
|
|
92
|
-
if (isFunctionConfig(exportValue)
|
|
93
|
-
|
|
125
|
+
if (!isFunctionConfig(exportValue))
|
|
126
|
+
continue;
|
|
127
|
+
const isNew = !functionsMap.has(exportName);
|
|
128
|
+
functionsMap.set(exportName, exportValue);
|
|
129
|
+
if (isNew)
|
|
130
|
+
addedNames.push(exportName);
|
|
131
|
+
else
|
|
94
132
|
reloadedNames.push(exportName);
|
|
95
|
-
if (exportValue.input) {
|
|
96
|
-
addSchema(exportName, exportValue.input);
|
|
97
|
-
schemasChanged = true;
|
|
98
|
-
}
|
|
99
|
-
if (exportValue.output) {
|
|
100
|
-
addSchema(`${exportName}Output`, exportValue.output);
|
|
101
|
-
schemasChanged = true;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
133
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
catch (err) {
|
|
115
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
116
|
-
if (msg.includes('SchemaService') ||
|
|
117
|
-
(msg.includes('schema') && msg.includes('not'))) {
|
|
118
|
-
logger.warn('Schema recompilation skipped (no SchemaService)');
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
logger.error(`Schema recompilation failed: ${msg}`);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
134
|
+
// Re-importing the module re-ran its wire* side effects (wireHTTP et al
|
|
135
|
+
// are keyed map-sets, so re-registration replaces/adds) — reset the
|
|
136
|
+
// router and caches even when no function export changed, so a
|
|
137
|
+
// wiring-only file edit rebuilds the route matchers too.
|
|
138
|
+
clearMiddlewareCache();
|
|
139
|
+
clearPermissionsCache();
|
|
140
|
+
clearChannelMiddlewareCache();
|
|
141
|
+
httpRouter.reset();
|
|
142
|
+
if (reloadedNames.length > 0 || addedNames.length > 0) {
|
|
126
143
|
const elapsed = Date.now() - start;
|
|
127
|
-
|
|
144
|
+
const parts = [];
|
|
145
|
+
if (reloadedNames.length > 0)
|
|
146
|
+
parts.push(reloadedNames.join(', '));
|
|
147
|
+
if (addedNames.length > 0)
|
|
148
|
+
parts.push(`new: ${addedNames.join(', ')}`);
|
|
149
|
+
logger.info(`Hot-reloaded: ${parts.join('; ')} (${elapsed}ms)`);
|
|
128
150
|
}
|
|
129
151
|
};
|
|
130
152
|
let debounceTimer;
|
|
131
153
|
const pendingChanges = new Set();
|
|
154
|
+
// Files re-imported since the last reimportPending() drain. A dev-server
|
|
155
|
+
// watcher drains this after its codegen pass so wire* registrations that
|
|
156
|
+
// were skipped for missing meta (a NEW route) run again with fresh meta.
|
|
157
|
+
const postCodegenQueue = new Set();
|
|
158
|
+
const safeHandleFileChange = async (file) => {
|
|
159
|
+
try {
|
|
160
|
+
await handleFileChange(file);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
logger.error(`Hot-reload error for ${relative(process.cwd(), file)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
132
166
|
const scheduleReload = (filePath) => {
|
|
133
167
|
pendingChanges.add(filePath);
|
|
134
168
|
if (debounceTimer)
|
|
@@ -137,12 +171,8 @@ export async function pikkuDevReloader(options) {
|
|
|
137
171
|
const files = [...pendingChanges];
|
|
138
172
|
pendingChanges.clear();
|
|
139
173
|
for (const file of files) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
logger.error(`Hot-reload error for ${relative(process.cwd(), file)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
145
|
-
}
|
|
174
|
+
postCodegenQueue.add(file);
|
|
175
|
+
await safeHandleFileChange(file);
|
|
146
176
|
}
|
|
147
177
|
}, 50);
|
|
148
178
|
};
|
|
@@ -168,5 +198,18 @@ export async function pikkuDevReloader(options) {
|
|
|
168
198
|
watcher.close();
|
|
169
199
|
}
|
|
170
200
|
},
|
|
201
|
+
// Drain the queue of recently re-imported files and import them again.
|
|
202
|
+
// A dev-server watcher calls this AFTER its codegen pass has refreshed
|
|
203
|
+
// the generated meta (reloadGeneratedMeta): wire* registrations skip
|
|
204
|
+
// routes whose meta doesn't exist yet, so a wiring file changed
|
|
205
|
+
// alongside a NEW function only registers its new route when
|
|
206
|
+
// re-imported after the fresh meta has landed.
|
|
207
|
+
reimportPending: async () => {
|
|
208
|
+
const files = [...postCodegenQueue];
|
|
209
|
+
postCodegenQueue.clear();
|
|
210
|
+
for (const file of files) {
|
|
211
|
+
await safeHandleFileChange(file);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
171
214
|
};
|
|
172
215
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Logger } from '../services/logger.js';
|
|
2
|
+
import type { SchemaService } from '../services/schema-service.js';
|
|
3
|
+
export interface ReloadGeneratedMetaOptions {
|
|
4
|
+
/** The project's generated output directory (the CLI's resolved outDir). */
|
|
5
|
+
pikkuDir: string;
|
|
6
|
+
logger: Logger;
|
|
7
|
+
/** Used to recompile validators for changed schemas; falls back to the
|
|
8
|
+
* schema service on the registered singleton services. */
|
|
9
|
+
schemaService?: SchemaService;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Re-reads the codegen output (wiring meta + JSON schemas) into the running
|
|
13
|
+
* process so new and changed functions become callable without a server
|
|
14
|
+
* restart. The generated `*-meta.gen.ts` files are plain
|
|
15
|
+
* `pikkuState(area, key, <json>)` side effects, but they cannot be
|
|
16
|
+
* re-imported after a dev-time codegen — the ESM cache pins both the wrapper
|
|
17
|
+
* and its JSON import — so this reads the JSON sources directly and applies
|
|
18
|
+
* the same state.
|
|
19
|
+
*
|
|
20
|
+
* Meant to be called by a dev-server watcher after each codegen pass. Routes
|
|
21
|
+
* registered by NEW `wireHTTP` files are not picked up (their modules were
|
|
22
|
+
* never imported); those still need a restart.
|
|
23
|
+
*/
|
|
24
|
+
export declare function reloadGeneratedMeta(options: ReloadGeneratedMetaOptions): Promise<void>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { pikkuState } from '../pikku-state.js';
|
|
4
|
+
import { addSchema, compileAllSchemas } from '../schema.js';
|
|
5
|
+
import { clearMiddlewareCache } from '../middleware-runner.js';
|
|
6
|
+
import { clearPermissionsCache } from '../permissions.js';
|
|
7
|
+
import { clearChannelMiddlewareCache } from '../wirings/channel/channel-middleware-runner.js';
|
|
8
|
+
import { httpRouter } from '../wirings/http/routers/http-router.js';
|
|
9
|
+
const readJson = async (logger, file) => {
|
|
10
|
+
let raw;
|
|
11
|
+
try {
|
|
12
|
+
raw = await readFile(file, 'utf-8');
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// The project doesn't use this wiring type — nothing generated.
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
logger.error(`Hot-reload could not parse ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Re-reads the codegen output (wiring meta + JSON schemas) into the running
|
|
28
|
+
* process so new and changed functions become callable without a server
|
|
29
|
+
* restart. The generated `*-meta.gen.ts` files are plain
|
|
30
|
+
* `pikkuState(area, key, <json>)` side effects, but they cannot be
|
|
31
|
+
* re-imported after a dev-time codegen — the ESM cache pins both the wrapper
|
|
32
|
+
* and its JSON import — so this reads the JSON sources directly and applies
|
|
33
|
+
* the same state.
|
|
34
|
+
*
|
|
35
|
+
* Meant to be called by a dev-server watcher after each codegen pass. Routes
|
|
36
|
+
* registered by NEW `wireHTTP` files are not picked up (their modules were
|
|
37
|
+
* never imported); those still need a restart.
|
|
38
|
+
*/
|
|
39
|
+
export async function reloadGeneratedMeta(options) {
|
|
40
|
+
const { pikkuDir, logger, schemaService } = options;
|
|
41
|
+
const dir = resolve(pikkuDir);
|
|
42
|
+
const functionsMeta = await readJson(logger, join(dir, 'function/pikku-functions-meta.gen.json'));
|
|
43
|
+
// Merge over the existing map, don't replace it: framework internals like
|
|
44
|
+
// pikkuWorkflowOrchestrator / the per-workflow queue workers are registered
|
|
45
|
+
// at service-init (pikku-workflow-service.ts), never in the generated JSON —
|
|
46
|
+
// a wholesale replace drops them and workflow jobs then fail with
|
|
47
|
+
// "Function meta not found: pikkuWorkflowOrchestrator".
|
|
48
|
+
if (functionsMeta) {
|
|
49
|
+
const existing = pikkuState(null, 'function', 'meta') ?? {};
|
|
50
|
+
pikkuState(null, 'function', 'meta', { ...existing, ...functionsMeta });
|
|
51
|
+
}
|
|
52
|
+
const httpMeta = await readJson(logger, join(dir, 'http/pikku-http-wirings-meta.gen.json'));
|
|
53
|
+
if (httpMeta)
|
|
54
|
+
pikkuState(null, 'http', 'meta', httpMeta);
|
|
55
|
+
const rpcMeta = await readJson(logger, join(dir, 'rpc/pikku-rpc-wirings-meta.internal.gen.json'));
|
|
56
|
+
if (rpcMeta)
|
|
57
|
+
pikkuState(null, 'rpc', 'meta', rpcMeta);
|
|
58
|
+
const queueMeta = await readJson(logger, join(dir, 'queue/pikku-queue-workers-wirings-meta.gen.json'));
|
|
59
|
+
// Same reason as function meta: the workflow service adds its orchestrator /
|
|
60
|
+
// step queues (and wf-orchestrator-* / wf-step-* per-workflow queues) here at
|
|
61
|
+
// init, absent from the generated JSON — merge so they survive the reload.
|
|
62
|
+
if (queueMeta) {
|
|
63
|
+
const existing = pikkuState(null, 'queue', 'meta') ?? {};
|
|
64
|
+
pikkuState(null, 'queue', 'meta', { ...existing, ...queueMeta });
|
|
65
|
+
}
|
|
66
|
+
const agentMeta = await readJson(logger, join(dir, 'agent/pikku-agent-wirings-meta.gen.json'));
|
|
67
|
+
if (agentMeta?.agentsMeta) {
|
|
68
|
+
pikkuState(null, 'agent', 'agentsMeta', agentMeta.agentsMeta);
|
|
69
|
+
}
|
|
70
|
+
// Generated JSON schemas: <outDir>/schemas/schemas/<Name>.schema.json.
|
|
71
|
+
// Re-adding replaces the map entry; the schema service recompiles any
|
|
72
|
+
// validator whose stored schema value no longer matches.
|
|
73
|
+
const schemasDir = join(dir, 'schemas', 'schemas');
|
|
74
|
+
let schemaFiles = [];
|
|
75
|
+
try {
|
|
76
|
+
schemaFiles = await readdir(schemasDir);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// No generated schemas — a schema-less project.
|
|
80
|
+
}
|
|
81
|
+
for (const file of schemaFiles) {
|
|
82
|
+
if (!file.endsWith('.schema.json'))
|
|
83
|
+
continue;
|
|
84
|
+
const schema = await readJson(logger, join(schemasDir, file));
|
|
85
|
+
if (schema) {
|
|
86
|
+
addSchema(file.slice(0, -'.schema.json'.length), schema);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
clearMiddlewareCache();
|
|
90
|
+
clearPermissionsCache();
|
|
91
|
+
clearChannelMiddlewareCache();
|
|
92
|
+
httpRouter.reset();
|
|
93
|
+
try {
|
|
94
|
+
compileAllSchemas(logger, schemaService);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
logger.error(`Hot-reload schema recompilation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pikkuState, getSingletonServices } from '../pikku-state.js';
|
|
2
|
-
import { createListenerMessageHandler } from '../wirings/gateway/gateway-runner.js';
|
|
2
|
+
import { createListenerMessageHandler, resolveGatewayAdapter, } from '../wirings/gateway/gateway-runner.js';
|
|
3
3
|
/**
|
|
4
4
|
* Local GatewayService implementation.
|
|
5
5
|
*
|
|
@@ -27,8 +27,9 @@ export class LocalGatewayService {
|
|
|
27
27
|
if (this.activeAdapters.has(name))
|
|
28
28
|
continue;
|
|
29
29
|
const handleMessage = createListenerMessageHandler(name, config, singletonServices);
|
|
30
|
-
await config
|
|
31
|
-
|
|
30
|
+
const adapter = await resolveGatewayAdapter(config, singletonServices);
|
|
31
|
+
await adapter.init(handleMessage);
|
|
32
|
+
this.activeAdapters.set(name, adapter);
|
|
32
33
|
singletonServices.logger.info(`Started listener gateway: ${name}`);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { CoreGateway } from './gateway.types.js';
|
|
1
|
+
import type { CoreGateway, GatewayAdapter } from './gateway.types.js';
|
|
2
|
+
export declare const resolveGatewayAdapter: (config: CoreGateway, services: CoreSingletonServices) => Promise<GatewayAdapter>;
|
|
2
3
|
import type { CoreSingletonServices } from '../../types/core.types.js';
|
|
3
4
|
/**
|
|
4
5
|
* Register a messaging gateway.
|
|
@@ -2,6 +2,23 @@ import { pikkuState } from '../../pikku-state.js';
|
|
|
2
2
|
import { addFunction } from '../../function/function-runner.js';
|
|
3
3
|
import { runMiddleware } from '../../middleware-runner.js';
|
|
4
4
|
import { httpRouter } from '../http/routers/http-router.js';
|
|
5
|
+
/**
|
|
6
|
+
* Lazily resolve a gateway's adapter. Factories are invoked once with the
|
|
7
|
+
* singleton services and cached (promise-cached, so concurrent first
|
|
8
|
+
* requests share one construction).
|
|
9
|
+
*/
|
|
10
|
+
const resolvedAdapters = new WeakMap();
|
|
11
|
+
export const resolveGatewayAdapter = (config, services) => {
|
|
12
|
+
let resolved = resolvedAdapters.get(config);
|
|
13
|
+
if (!resolved) {
|
|
14
|
+
resolved =
|
|
15
|
+
typeof config.adapter === 'function'
|
|
16
|
+
? Promise.resolve(config.adapter(services))
|
|
17
|
+
: Promise.resolve(config.adapter);
|
|
18
|
+
resolvedAdapters.set(config, resolved);
|
|
19
|
+
}
|
|
20
|
+
return resolved;
|
|
21
|
+
};
|
|
5
22
|
/**
|
|
6
23
|
* Register a messaging gateway.
|
|
7
24
|
*
|
|
@@ -67,7 +84,9 @@ const wireWebhookGateway = (config) => {
|
|
|
67
84
|
auth: false,
|
|
68
85
|
});
|
|
69
86
|
// --- GET handler (webhook verification, e.g. WhatsApp challenge) ---------
|
|
70
|
-
|
|
87
|
+
// Factory adapters can't be probed for verifyWebhook until first resolve,
|
|
88
|
+
// so register the GET route unconditionally for them.
|
|
89
|
+
if (typeof adapter === 'function' || adapter.verifyWebhook) {
|
|
71
90
|
const verifyFuncId = `gateway__${name}__verify`;
|
|
72
91
|
funcMeta[verifyFuncId] = {
|
|
73
92
|
pikkuFuncId: verifyFuncId,
|
|
@@ -82,7 +101,7 @@ const wireWebhookGateway = (config) => {
|
|
|
82
101
|
};
|
|
83
102
|
const verifyHandler = {
|
|
84
103
|
auth: false,
|
|
85
|
-
func: createWebhookVerifyHandler(
|
|
104
|
+
func: createWebhookVerifyHandler(config),
|
|
86
105
|
};
|
|
87
106
|
addFunction(verifyFuncId, verifyHandler);
|
|
88
107
|
if (!routes.has('get')) {
|
|
@@ -110,9 +129,10 @@ const wireWebhookGateway = (config) => {
|
|
|
110
129
|
* 6. Auto-send response via adapter if func returns outbound content
|
|
111
130
|
*/
|
|
112
131
|
const createWebhookPostHandler = (config) => {
|
|
113
|
-
const { name,
|
|
132
|
+
const { name, func: userFunc, middleware: userMiddleware } = config;
|
|
114
133
|
const userFuncConfig = userFunc;
|
|
115
134
|
return async (services, data, wire) => {
|
|
135
|
+
const adapter = await resolveGatewayAdapter(config, services);
|
|
116
136
|
// Check for POST-based webhook verification (e.g. Slack url_verification)
|
|
117
137
|
if (adapter.verifyWebhook) {
|
|
118
138
|
const verifyResult = await adapter.verifyWebhook(data, wire.http?.request);
|
|
@@ -157,8 +177,9 @@ const createWebhookPostHandler = (config) => {
|
|
|
157
177
|
* Creates the GET handler for webhook verification challenges.
|
|
158
178
|
* Passes query parameters to the adapter's verifyWebhook method.
|
|
159
179
|
*/
|
|
160
|
-
const createWebhookVerifyHandler = (
|
|
161
|
-
return async (
|
|
180
|
+
const createWebhookVerifyHandler = (config) => {
|
|
181
|
+
return async (services, _data, wire) => {
|
|
182
|
+
const adapter = await resolveGatewayAdapter(config, services);
|
|
162
183
|
if (!adapter.verifyWebhook) {
|
|
163
184
|
return { error: 'Verification not supported' };
|
|
164
185
|
}
|
|
@@ -174,7 +195,7 @@ const createWebhookVerifyHandler = (adapter) => {
|
|
|
174
195
|
// WebSocket gateway — client connects via WebSocket
|
|
175
196
|
// ---------------------------------------------------------------------------
|
|
176
197
|
const wireWebsocketGateway = (config) => {
|
|
177
|
-
const { name, route
|
|
198
|
+
const { name, route } = config;
|
|
178
199
|
if (!route) {
|
|
179
200
|
throw new Error(`WebSocket gateway '${name}' requires a route`);
|
|
180
201
|
}
|
|
@@ -211,8 +232,8 @@ const wireWebsocketGateway = (config) => {
|
|
|
211
232
|
// Register onConnect
|
|
212
233
|
addFunction(connectFuncId, {
|
|
213
234
|
auth: false,
|
|
214
|
-
func: async (
|
|
215
|
-
;
|
|
235
|
+
func: async (services, _data, wire) => {
|
|
236
|
+
const adapter = await resolveGatewayAdapter(config, services);
|
|
216
237
|
wire.gateway = {
|
|
217
238
|
gatewayName: name,
|
|
218
239
|
senderId: '',
|
|
@@ -227,6 +248,7 @@ const wireWebsocketGateway = (config) => {
|
|
|
227
248
|
addFunction(messageFuncId, {
|
|
228
249
|
auth: false,
|
|
229
250
|
func: async (services, data, wire) => {
|
|
251
|
+
const adapter = await resolveGatewayAdapter(config, services);
|
|
230
252
|
const parsed = adapter.parse(data);
|
|
231
253
|
if (!parsed)
|
|
232
254
|
return;
|
|
@@ -290,10 +312,10 @@ const wireListenerGateway = (config) => {
|
|
|
290
312
|
* @param singletonServices - Singleton services to pass to handler/middleware
|
|
291
313
|
*/
|
|
292
314
|
export const createListenerMessageHandler = (name, config, singletonServices) => {
|
|
293
|
-
const { adapter } = config;
|
|
294
315
|
const userFuncConfig = config.func;
|
|
295
316
|
const userMiddleware = config.middleware;
|
|
296
317
|
return async (rawData) => {
|
|
318
|
+
const adapter = await resolveGatewayAdapter(config, singletonServices);
|
|
297
319
|
const parsed = adapter.parse(rawData);
|
|
298
320
|
if (!parsed)
|
|
299
321
|
return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CommonWireMeta, CorePikkuMiddleware, CorePikkuMiddlewareGroup } from '../../types/core.types.js';
|
|
1
|
+
import type { CommonWireMeta, CorePikkuMiddleware, CorePikkuMiddlewareGroup, CoreSingletonServices } from '../../types/core.types.js';
|
|
2
2
|
import type { CorePikkuFunctionConfig, CorePermissionGroup, CorePikkuPermission } from '../../function/functions.types.js';
|
|
3
3
|
import type { PikkuHTTPRequest } from '../http/http.types.js';
|
|
4
4
|
/**
|
|
@@ -69,6 +69,16 @@ export interface GatewayAdapter {
|
|
|
69
69
|
* Receives the data (body or query params) and the Pikku HTTP request for additional inspection. */
|
|
70
70
|
verifyWebhook?(data: unknown, request?: PikkuHTTPRequest): WebhookVerificationResult | Promise<WebhookVerificationResult>;
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Factory that builds a GatewayAdapter from singleton services.
|
|
74
|
+
*
|
|
75
|
+
* Real platform adapters (WhatsApp Cloud API, Slack, …) need secrets or
|
|
76
|
+
* services that only exist after boot, while `wireGateway` runs at module
|
|
77
|
+
* load. Pass a factory instead of an instance and it is resolved lazily on
|
|
78
|
+
* the first inbound request (webhook/websocket) or on gateway start
|
|
79
|
+
* (listener), then cached for the lifetime of the gateway.
|
|
80
|
+
*/
|
|
81
|
+
export type GatewayAdapterFactory = (services: CoreSingletonServices) => GatewayAdapter | Promise<GatewayAdapter>;
|
|
72
82
|
/**
|
|
73
83
|
* The gateway wire object available on wire.gateway inside handler functions and middleware
|
|
74
84
|
*/
|
|
@@ -100,8 +110,9 @@ export type CoreGateway<PikkuFunctionConfig = CorePikkuFunctionConfig<any, any>,
|
|
|
100
110
|
/** HTTP route for webhook/websocket types */
|
|
101
111
|
route?: string;
|
|
102
112
|
platform?: string;
|
|
103
|
-
/** The gateway adapter (parse inbound, send outbound)
|
|
104
|
-
|
|
113
|
+
/** The gateway adapter (parse inbound, send outbound), or a factory
|
|
114
|
+
* resolved lazily from singleton services (for adapters needing secrets) */
|
|
115
|
+
adapter: GatewayAdapter | GatewayAdapterFactory;
|
|
105
116
|
/** The handler function that processes parsed messages */
|
|
106
117
|
func: PikkuFunctionConfig;
|
|
107
118
|
/** Optional middleware chain (e.g., auth) */
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { wireGateway, createListenerMessageHandler } from './gateway-runner.js';
|
|
2
|
-
export type { GatewayAdapter, GatewayAttachment, GatewayInboundMessage, GatewayOutboundMessage, GatewayMeta, GatewaysMeta, GatewayTransportType, CoreGateway, PikkuGateway, WebhookVerificationResult, } from './gateway.types.js';
|
|
1
|
+
export { wireGateway, createListenerMessageHandler, resolveGatewayAdapter, } from './gateway-runner.js';
|
|
2
|
+
export type { GatewayAdapter, GatewayAdapterFactory, GatewayAttachment, GatewayInboundMessage, GatewayOutboundMessage, GatewayMeta, GatewaysMeta, GatewayTransportType, CoreGateway, PikkuGateway, WebhookVerificationResult, } from './gateway.types.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { wireGateway, createListenerMessageHandler } from './gateway-runner.js';
|
|
1
|
+
export { wireGateway, createListenerMessageHandler, resolveGatewayAdapter, } from './gateway-runner.js';
|
|
@@ -441,6 +441,9 @@ export async function executeGraphStep(workflowService, rpcService, runId, stepI
|
|
|
441
441
|
try {
|
|
442
442
|
let result;
|
|
443
443
|
const subWorkflowMeta = pikkuState(null, 'workflows', 'meta')[rpcName];
|
|
444
|
+
const agentMeta = subWorkflowMeta
|
|
445
|
+
? undefined
|
|
446
|
+
: pikkuState(null, 'agent', 'agentsMeta')[rpcName];
|
|
444
447
|
if (subWorkflowMeta) {
|
|
445
448
|
const childWire = {
|
|
446
449
|
type: 'workflow',
|
|
@@ -465,6 +468,10 @@ export async function executeGraphStep(workflowService, rpcService, runId, stepI
|
|
|
465
468
|
throw new ChildWorkflowStartedException(runId, stepId, childRunId);
|
|
466
469
|
}
|
|
467
470
|
}
|
|
471
|
+
else if (agentMeta) {
|
|
472
|
+
const agentRun = await rpcService.agent.run(rpcName, data);
|
|
473
|
+
result = agentRun.result;
|
|
474
|
+
}
|
|
468
475
|
else {
|
|
469
476
|
result = await invokeGraphNodeRpc(workflowService, rpcService, runId, stepId, nodeId, rpcName, data, graphName);
|
|
470
477
|
}
|
|
@@ -523,6 +530,9 @@ async function executeGraphNodeInline(workflowService, rpcService, runId, graphN
|
|
|
523
530
|
try {
|
|
524
531
|
let result;
|
|
525
532
|
const subWorkflowMeta = pikkuState(null, 'workflows', 'meta')[rpcName];
|
|
533
|
+
const agentMeta = subWorkflowMeta
|
|
534
|
+
? undefined
|
|
535
|
+
: pikkuState(null, 'agent', 'agentsMeta')[rpcName];
|
|
526
536
|
if (subWorkflowMeta) {
|
|
527
537
|
const childWire = {
|
|
528
538
|
type: 'workflow',
|
|
@@ -541,6 +551,10 @@ async function executeGraphNodeInline(workflowService, rpcService, runId, graphN
|
|
|
541
551
|
}
|
|
542
552
|
result = childRun?.output;
|
|
543
553
|
}
|
|
554
|
+
else if (agentMeta) {
|
|
555
|
+
const agentRun = await rpcService.agent.run(rpcName, input);
|
|
556
|
+
result = agentRun.result;
|
|
557
|
+
}
|
|
544
558
|
else {
|
|
545
559
|
result = await invokeGraphNodeRpc(workflowService, rpcService, runId, stepState.stepId, nodeId, rpcName, input, graphName);
|
|
546
560
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/core",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.58",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -72,6 +72,6 @@
|
|
|
72
72
|
"typescript": "^6.0.3"
|
|
73
73
|
},
|
|
74
74
|
"engines": {
|
|
75
|
-
"node": ">=
|
|
75
|
+
"node": ">=24"
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -152,29 +152,36 @@ describe('pikkuDevReloader', { concurrency: false }, () => {
|
|
|
152
152
|
assert.ok(reloadLog, 'Should log hot-reload message')
|
|
153
153
|
})
|
|
154
154
|
|
|
155
|
-
test('should
|
|
155
|
+
test('should register a brand-new function export', async (t) => {
|
|
156
156
|
if (!(await ensureRecursiveWatchAvailable(t, tmpDir))) return
|
|
157
157
|
|
|
158
158
|
addFunction('registeredFunc', {
|
|
159
159
|
func: async () => ({ name: 'registered' }),
|
|
160
160
|
})
|
|
161
161
|
|
|
162
|
-
await writeFunctionModule(tmpDir, 'unknownFunc.ts', '{ name: "unknown" }')
|
|
163
|
-
|
|
164
162
|
reloader = await pikkuDevReloader({
|
|
165
163
|
srcDirectories: [tmpDir],
|
|
166
164
|
logger: mockLogger,
|
|
167
165
|
pikkuDir: tmpDir,
|
|
168
166
|
})
|
|
169
167
|
|
|
170
|
-
await writeFunctionModule(tmpDir, 'unknownFunc.ts', '{ name: "
|
|
168
|
+
await writeFunctionModule(tmpDir, 'unknownFunc.ts', '{ name: "unknown" }')
|
|
171
169
|
|
|
172
170
|
await wait(300)
|
|
173
171
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
172
|
+
const func = pikkuState(null, 'function', 'functions').get('unknownFunc')!
|
|
173
|
+
assert.ok(func, 'New function export should be registered')
|
|
174
|
+
assert.deepEqual(await func.func({} as any, {}, {} as any), {
|
|
175
|
+
name: 'unknown',
|
|
176
|
+
})
|
|
177
|
+
const newLog = mockLogger
|
|
178
|
+
.getLogs()
|
|
179
|
+
.find(
|
|
180
|
+
(l) =>
|
|
181
|
+
l.message.includes('Hot-reloaded') &&
|
|
182
|
+
l.message.includes('new: unknownFunc')
|
|
183
|
+
)
|
|
184
|
+
assert.ok(newLog, 'Should log the newly registered function')
|
|
178
185
|
})
|
|
179
186
|
|
|
180
187
|
test('should keep old code when JS import fails', async (t) => {
|