@pikku/inspector 0.12.81 → 0.12.82
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 +27 -0
- package/dist/add/add-middleware.js +66 -42
- package/dist/add/add-wire-addon.js +6 -4
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +1 -0
- package/dist/inspector.js +1 -0
- package/dist/types.d.ts +16 -1
- package/dist/utils/load-addon-functions-meta.js +24 -11
- package/dist/utils/serialize-inspector-state.d.ts +1 -0
- package/dist/utils/serialize-inspector-state.js +2 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## 0.12.82
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- c842054: Middleware instance ids are now allocated per group across the whole inspection, not per source file, and a build that would drop a global middleware registration fails instead of shipping.
|
|
6
|
+
|
|
7
|
+
The per-group index restarted at 0 for every file, so the second file to register against a group minted ids the first already owned and overwrote its entries in `middleware.instances` — `instanceIds` listed one key twice while `count` correctly said two. For `addGlobalMiddleware` that was not cosmetic. Global middleware belongs to no wire group, so the instance map is the only record that its module must be imported, and per-unit deploy codegen emits its side-effect imports from that map. An app file registering a global middleware was erased by the generated auth scaffold registering its own, the app's module was never imported, its registration never ran — and because auth commonly lives in global middleware, a deployed unit answered every authenticated route with a 401 against a clean build log.
|
|
8
|
+
|
|
9
|
+
The inspector now also records each `addGlobalMiddleware` file in `middleware.globalFiles`, a plain set that no key collision can corrupt; codegen emits the side-effect imports from it, generates the middleware file when global middleware is a project's only middleware, and asserts against the text it emitted that every such file is imported — naming the files if not, rather than letting the unit deploy without its auth gate.
|
|
10
|
+
|
|
11
|
+
- dfd7019: Resolve the MCP server instance per log message, and restore the wireAddon
|
|
12
|
+
declaration's source file.
|
|
13
|
+
- 1469e73: The app names which of an addon's functions reach MCP
|
|
14
|
+
|
|
15
|
+
`wireAddon`'s `mcp` takes a list as well as `true`. `true` still offers every
|
|
16
|
+
function the addon declared `mcp: true`; a list names the tools this deployment
|
|
17
|
+
offers, whether or not the addon declared them, and is typed against the
|
|
18
|
+
function names that addon publishes — a typo is a compile error rather than a
|
|
19
|
+
tool silently missing from the menu.
|
|
20
|
+
|
|
21
|
+
- Updated dependencies [c842054]
|
|
22
|
+
- Updated dependencies [dfd7019]
|
|
23
|
+
- Updated dependencies [dfd7019]
|
|
24
|
+
- Updated dependencies [9b978e7]
|
|
25
|
+
- Updated dependencies [1469e73]
|
|
26
|
+
- @pikku/core@0.12.113
|
|
27
|
+
|
|
1
28
|
## 0.12.81
|
|
2
29
|
|
|
3
30
|
### Patch Changes
|
|
@@ -47,6 +47,52 @@ function recordMiddlewareGroup(groups, key, group) {
|
|
|
47
47
|
services: Array.from(new Set([...existing.services.services, ...group.services.services])),
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Record one `add*Middleware` call's entries in the instance map, giving each
|
|
52
|
+
* entry an id that is unique across the whole inspection.
|
|
53
|
+
*
|
|
54
|
+
* The index used to restart at 0 for every call, so a second file registering
|
|
55
|
+
* against the same group (or against `global`, which is one group for the whole
|
|
56
|
+
* project) minted ids the first file already owned and silently overwrote its
|
|
57
|
+
* entries. For `global:*` that is fatal rather than cosmetic: the instance map
|
|
58
|
+
* is the only record of a global registration, and per-unit deploy codegen
|
|
59
|
+
* emits its side-effect imports from that map — a lost entry means the module
|
|
60
|
+
* is never imported, `addGlobalMiddleware` never runs, and an auth gate is
|
|
61
|
+
* simply absent from the deployed unit with nothing in the build log.
|
|
62
|
+
*
|
|
63
|
+
* Allocating from the ids already present makes N registrations against one
|
|
64
|
+
* group produce N distinct entries, whatever order the files are visited in.
|
|
65
|
+
*/
|
|
66
|
+
function registerMiddlewareInstances(instances, groupType, groupKey, entries, sourceFile, position) {
|
|
67
|
+
// The inspector may revisit a file; the same call site is not a second
|
|
68
|
+
// registration, and re-allocating would inflate the map on every pass.
|
|
69
|
+
const alreadyRecorded = Object.entries(instances)
|
|
70
|
+
.filter(([id, instance]) => instance.sourceFile === sourceFile &&
|
|
71
|
+
instance.position === position &&
|
|
72
|
+
id.startsWith(`${makeContextBasedId(groupType, groupKey)}:`))
|
|
73
|
+
.map(([id]) => id);
|
|
74
|
+
if (alreadyRecorded.length === entries.length) {
|
|
75
|
+
return alreadyRecorded;
|
|
76
|
+
}
|
|
77
|
+
const instanceIds = [];
|
|
78
|
+
let next = 0;
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
let instanceId = makeContextBasedId(groupType, groupKey, String(next));
|
|
81
|
+
while (instances[instanceId]) {
|
|
82
|
+
next++;
|
|
83
|
+
instanceId = makeContextBasedId(groupType, groupKey, String(next));
|
|
84
|
+
}
|
|
85
|
+
instances[instanceId] = {
|
|
86
|
+
definitionId: entry.definitionId,
|
|
87
|
+
sourceFile,
|
|
88
|
+
position,
|
|
89
|
+
isFactoryCall: entry.isFactoryCall,
|
|
90
|
+
};
|
|
91
|
+
instanceIds.push(instanceId);
|
|
92
|
+
next++;
|
|
93
|
+
}
|
|
94
|
+
return instanceIds;
|
|
95
|
+
}
|
|
50
96
|
function renameTempDefinitions(state, definitionIds, groupType, groupKey, storeKey = 'middleware') {
|
|
51
97
|
const tempIndices = definitionIds
|
|
52
98
|
.map((name, i) => (name.startsWith('__temp_') ? i : -1))
|
|
@@ -237,17 +283,10 @@ export const addMiddleware = (logger, node, checker, state) => {
|
|
|
237
283
|
renameTempDefinitions(state, definitionIds, 'tag', tag);
|
|
238
284
|
}
|
|
239
285
|
const sourceFile = node.getSourceFile().fileName;
|
|
240
|
-
const instanceIds =
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
definitionId: definitionIds[i],
|
|
245
|
-
sourceFile,
|
|
246
|
-
position: node.getStart(),
|
|
247
|
-
isFactoryCall: refs[i].isFactoryCall,
|
|
248
|
-
};
|
|
249
|
-
instanceIds.push(instanceId);
|
|
250
|
-
}
|
|
286
|
+
const instanceIds = registerMiddlewareInstances(state.middleware.instances, 'tag', tag, refs.map((ref, i) => ({
|
|
287
|
+
definitionId: definitionIds[i],
|
|
288
|
+
isFactoryCall: ref.isFactoryCall,
|
|
289
|
+
})), sourceFile, node.getStart());
|
|
251
290
|
const allServices = new Set();
|
|
252
291
|
for (const defId of definitionIds) {
|
|
253
292
|
const def = state.middleware.definitions[defId];
|
|
@@ -309,15 +348,14 @@ export const addMiddleware = (logger, node, checker, state) => {
|
|
|
309
348
|
renameTempDefinitions(state, definitionIds, 'global', 'middleware');
|
|
310
349
|
}
|
|
311
350
|
const sourceFile = node.getSourceFile().fileName;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
351
|
+
registerMiddlewareInstances(state.middleware.instances, 'global', 'middleware', refs.map((ref, i) => ({
|
|
352
|
+
definitionId: definitionIds[i],
|
|
353
|
+
isFactoryCall: ref.isFactoryCall,
|
|
354
|
+
})), sourceFile, node.getStart());
|
|
355
|
+
// The instance map is one map for the whole project, and codegen reads it
|
|
356
|
+
// by string-prefix. This set is the independent witness that lets codegen
|
|
357
|
+
// fail loudly if a global registration ever goes missing from it.
|
|
358
|
+
state.middleware.globalFiles.add(sourceFile);
|
|
321
359
|
// Without this, bootstrap codegen's "import every file with a wire-call"
|
|
322
360
|
// pass skips middleware-only files and the registration never runs.
|
|
323
361
|
state.http.files.add(sourceFile);
|
|
@@ -347,17 +385,10 @@ export const addMiddleware = (logger, node, checker, state) => {
|
|
|
347
385
|
renameTempDefinitions(state, definitionIds, 'http', pattern);
|
|
348
386
|
}
|
|
349
387
|
const sourceFile = node.getSourceFile().fileName;
|
|
350
|
-
const instanceIds =
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
definitionId: definitionIds[i],
|
|
355
|
-
sourceFile,
|
|
356
|
-
position: node.getStart(),
|
|
357
|
-
isFactoryCall: refs[i].isFactoryCall,
|
|
358
|
-
};
|
|
359
|
-
instanceIds.push(instanceId);
|
|
360
|
-
}
|
|
388
|
+
const instanceIds = registerMiddlewareInstances(state.middleware.instances, 'http', pattern, refs.map((ref, i) => ({
|
|
389
|
+
definitionId: definitionIds[i],
|
|
390
|
+
isFactoryCall: ref.isFactoryCall,
|
|
391
|
+
})), sourceFile, node.getStart());
|
|
361
392
|
const allServices = new Set();
|
|
362
393
|
for (const defId of definitionIds) {
|
|
363
394
|
const def = state.middleware.definitions[defId];
|
|
@@ -581,17 +612,10 @@ export const addMiddleware = (logger, node, checker, state) => {
|
|
|
581
612
|
const definitionIds = refs.map((r) => r.definitionId);
|
|
582
613
|
renameTempDefinitions(state, definitionIds, 'tag', tag, 'channelMiddleware');
|
|
583
614
|
const sourceFile = node.getSourceFile().fileName;
|
|
584
|
-
const instanceIds =
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
definitionId: definitionIds[i],
|
|
589
|
-
sourceFile,
|
|
590
|
-
position: node.getStart(),
|
|
591
|
-
isFactoryCall: refs[i].isFactoryCall,
|
|
592
|
-
};
|
|
593
|
-
instanceIds.push(instanceId);
|
|
594
|
-
}
|
|
615
|
+
const instanceIds = registerMiddlewareInstances(state.channelMiddleware.instances, 'tag', tag, refs.map((ref, i) => ({
|
|
616
|
+
definitionId: definitionIds[i],
|
|
617
|
+
isFactoryCall: ref.isFactoryCall,
|
|
618
|
+
})), sourceFile, node.getStart());
|
|
595
619
|
const allServices = new Set();
|
|
596
620
|
for (const defId of definitionIds) {
|
|
597
621
|
const def = state.channelMiddleware.definitions[defId];
|
|
@@ -70,10 +70,12 @@ export function addWireAddon(node, state, logger) {
|
|
|
70
70
|
else if (key === 'rpcEndpoint' && ts.isStringLiteral(prop.initializer)) {
|
|
71
71
|
rpcEndpoint = prop.initializer.text;
|
|
72
72
|
}
|
|
73
|
-
else if (key === 'mcp'
|
|
74
|
-
|
|
75
|
-
prop.initializer.kind === ts.SyntaxKind.
|
|
76
|
-
|
|
73
|
+
else if (key === 'mcp') {
|
|
74
|
+
mcp =
|
|
75
|
+
prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
|
76
|
+
prop.initializer.kind === ts.SyntaxKind.FalseKeyword
|
|
77
|
+
? prop.initializer.kind === ts.SyntaxKind.TrueKeyword
|
|
78
|
+
: parseStringArray(prop.initializer);
|
|
77
79
|
}
|
|
78
80
|
else if (key === 'auth' &&
|
|
79
81
|
(prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export declare enum ErrorCode {
|
|
|
32
32
|
REMOTE_ADDON_NOT_DEV_DEPENDENCY = "PKU338",
|
|
33
33
|
REMOTE_ADDON_AUTH_UNRESOLVED = "PKU339",
|
|
34
34
|
ADDON_NOT_INSTALLED = "PKU340",
|
|
35
|
+
ADDON_MCP_FUNCTION_NOT_FOUND = "PKU341",
|
|
35
36
|
DB_COLUMN_NAME_TYPE_CONTRADICTION = "PKU480",
|
|
36
37
|
DB_JSON_COLUMN_UNTYPED = "PKU481",
|
|
37
38
|
DB_FORMAT_ON_NON_STRING = "PKU482",
|
package/dist/error-codes.js
CHANGED
|
@@ -35,6 +35,7 @@ export var ErrorCode;
|
|
|
35
35
|
ErrorCode["REMOTE_ADDON_NOT_DEV_DEPENDENCY"] = "PKU338";
|
|
36
36
|
ErrorCode["REMOTE_ADDON_AUTH_UNRESOLVED"] = "PKU339";
|
|
37
37
|
ErrorCode["ADDON_NOT_INSTALLED"] = "PKU340";
|
|
38
|
+
ErrorCode["ADDON_MCP_FUNCTION_NOT_FOUND"] = "PKU341";
|
|
38
39
|
// Database schema codegen warnings
|
|
39
40
|
ErrorCode["DB_COLUMN_NAME_TYPE_CONTRADICTION"] = "PKU480";
|
|
40
41
|
ErrorCode["DB_JSON_COLUMN_UNTYPED"] = "PKU481";
|
package/dist/inspector.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -162,6 +162,16 @@ export interface InspectorMiddlewareState {
|
|
|
162
162
|
definitions: Record<string, InspectorMiddlewareDefinition>;
|
|
163
163
|
instances: Record<string, InspectorMiddlewareInstance>;
|
|
164
164
|
tagMiddleware: Map<string, MiddlewareGroupMeta>;
|
|
165
|
+
/**
|
|
166
|
+
* Every source file with a top-level `addGlobalMiddleware` call.
|
|
167
|
+
*
|
|
168
|
+
* Global middleware belongs to no wire group, so the only thing that makes it
|
|
169
|
+
* run in a generated bundle is a side-effect import of its source file. This
|
|
170
|
+
* set is the record codegen emits those imports from, and the one it checks
|
|
171
|
+
* itself against — a plain set of file names, so nothing can overwrite an
|
|
172
|
+
* entry the way a keyed map could.
|
|
173
|
+
*/
|
|
174
|
+
globalFiles: Set<string>;
|
|
165
175
|
}
|
|
166
176
|
export interface InspectorChannelMiddlewareState {
|
|
167
177
|
definitions: Record<string, InspectorMiddlewareDefinition>;
|
|
@@ -565,7 +575,12 @@ export interface InspectorState {
|
|
|
565
575
|
/** The app source file whose `wireAddon` call declared this instance. */
|
|
566
576
|
file?: string;
|
|
567
577
|
rpcEndpoint?: string;
|
|
568
|
-
|
|
578
|
+
/**
|
|
579
|
+
* `true` offers every function the addon declared `mcp: true`; a list
|
|
580
|
+
* names the functions to offer, whatever the addon declared. Absent
|
|
581
|
+
* when the declared value was not a statically-knowable literal.
|
|
582
|
+
*/
|
|
583
|
+
mcp?: boolean | string[];
|
|
569
584
|
/**
|
|
570
585
|
* The addon's own gates. `runPikkuFunc` applies these to every function
|
|
571
586
|
* in the package, on every wiring path — not just `namespace:function`.
|
|
@@ -2,6 +2,7 @@ import { existsSync } from 'fs';
|
|
|
2
2
|
import { readFile, readdir } from 'fs/promises';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { join, dirname } from 'path';
|
|
5
|
+
import { ErrorCode } from '../error-codes.js';
|
|
5
6
|
const isHTTPRouteConfig = (value) => typeof value === 'object' &&
|
|
6
7
|
value !== null &&
|
|
7
8
|
'method' in value &&
|
|
@@ -147,19 +148,31 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
147
148
|
state.addonFunctions[namespace] = meta;
|
|
148
149
|
logger.debug(`Loaded ${Object.keys(meta).length} addon functions for '${namespace}' from ${decl.package}`);
|
|
149
150
|
registerAddonTypes(state, metaPath, meta);
|
|
150
|
-
//
|
|
151
|
+
// `mcp: true` offers what the addon declared as tools; `mcp: [...]` names
|
|
152
|
+
// the functions to offer, whatever the addon declared — the consuming app
|
|
153
|
+
// decides which of the addon's functions a model gets to see.
|
|
151
154
|
if (decl.mcp) {
|
|
155
|
+
const selected = Array.isArray(decl.mcp) ? new Set(decl.mcp) : null;
|
|
152
156
|
for (const [funcName, funcMeta] of Object.entries(meta)) {
|
|
153
|
-
if (funcMeta.mcp) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
if (selected ? !selected.has(funcName) : !funcMeta.mcp) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const toolName = `${namespace}:${funcName}`;
|
|
161
|
+
state.mcpEndpoints.toolsMeta[toolName] = {
|
|
162
|
+
pikkuFuncId: `${namespace}:${funcName}`,
|
|
163
|
+
name: toolName,
|
|
164
|
+
description: funcMeta.description || funcMeta.title || funcName,
|
|
165
|
+
inputSchema: funcMeta.inputSchemaName ?? null,
|
|
166
|
+
outputSchema: funcMeta.outputSchemaName ?? null,
|
|
167
|
+
tags: funcMeta.tags,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// A name the addon does not publish is a tool the app believes it
|
|
171
|
+
// offers and never does, so it fails the build rather than silently
|
|
172
|
+
// offering one tool fewer than the list says.
|
|
173
|
+
for (const funcName of selected ?? []) {
|
|
174
|
+
if (!(funcName in meta)) {
|
|
175
|
+
logger.critical(ErrorCode.ADDON_MCP_FUNCTION_NOT_FOUND, `wireAddon('${namespace}') lists '${funcName}' under mcp, but ${decl.package} publishes no such function.`);
|
|
163
176
|
}
|
|
164
177
|
}
|
|
165
178
|
}
|
|
@@ -275,6 +275,7 @@ export interface SerializableInspectorState {
|
|
|
275
275
|
middleware: {
|
|
276
276
|
definitions: InspectorState['middleware']['definitions'];
|
|
277
277
|
instances: InspectorState['middleware']['instances'];
|
|
278
|
+
globalFiles: string[];
|
|
278
279
|
tagMiddleware: Array<[
|
|
279
280
|
string,
|
|
280
281
|
InspectorState['middleware']['tagMiddleware'] extends Map<string, infer V> ? V : never
|
|
@@ -150,6 +150,7 @@ export function serializeInspectorState(state) {
|
|
|
150
150
|
middleware: {
|
|
151
151
|
definitions: state.middleware.definitions,
|
|
152
152
|
instances: state.middleware.instances,
|
|
153
|
+
globalFiles: Array.from(state.middleware.globalFiles),
|
|
153
154
|
tagMiddleware: Array.from(state.middleware.tagMiddleware.entries()),
|
|
154
155
|
},
|
|
155
156
|
channelMiddleware: {
|
|
@@ -335,6 +336,7 @@ export function deserializeInspectorState(data) {
|
|
|
335
336
|
middleware: {
|
|
336
337
|
definitions: data.middleware.definitions,
|
|
337
338
|
instances: data.middleware.instances || {},
|
|
339
|
+
globalFiles: new Set(data.middleware.globalFiles || []),
|
|
338
340
|
tagMiddleware: new Map(data.middleware.tagMiddleware),
|
|
339
341
|
},
|
|
340
342
|
channelMiddleware: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.82",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"zod-to-ts": "^2.1.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@pikku/core": "^0.12.
|
|
58
|
+
"@pikku/core": "^0.12.113"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@pikku/core": "^0.12.
|
|
61
|
+
"@pikku/core": "^0.12.113",
|
|
62
62
|
"@types/node": "^24.13.3"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|