@pikku/inspector 0.12.102 → 0.12.103
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 +11 -0
- package/dist/add/add-define-screens.d.ts +8 -0
- package/dist/add/add-define-screens.js +140 -0
- package/dist/add/add-wire-addon.js +39 -2
- package/dist/error-codes.d.ts +5 -0
- package/dist/error-codes.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/inspector.js +3 -1
- package/dist/types.d.ts +42 -0
- package/dist/utils/load-addon-functions-meta.d.ts +0 -5
- package/dist/utils/load-addon-functions-meta.js +52 -0
- package/dist/utils/post-process.d.ts +2 -0
- package/dist/utils/post-process.js +33 -0
- package/dist/utils/serialize-inspector-state.d.ts +5 -0
- package/dist/utils/serialize-inspector-state.js +4 -0
- package/dist/visit.js +2 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## 0.12.103
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 53ac40d: An addon can ship screens: `wireAddon({ ui: true })` mounts what the package declares with `defineScreens`, the inspector reads them, `pikku all` emits the screens manifest and the installed-addons registry, and `x-pikku-addon` narrows a session to the addon's role. `pikku new addon --ui` scaffolds one; its tables must be prefixed `<name>_`.
|
|
6
|
+
- Updated dependencies [a196a21]
|
|
7
|
+
- Updated dependencies [53ac40d]
|
|
8
|
+
- Updated dependencies [443fd40]
|
|
9
|
+
- Updated dependencies [56c8047]
|
|
10
|
+
- @pikku/core@0.12.139
|
|
11
|
+
|
|
1
12
|
## 0.12.102
|
|
2
13
|
|
|
3
14
|
### Patch Changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import type { InspectorState, InspectorLogger } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detect `defineScreens({ title, icon, screens })` and record the manifest.
|
|
5
|
+
* A screen with a field that is not a literal is reported and left out: a
|
|
6
|
+
* navigation entry the host cannot read would otherwise vanish with no sign.
|
|
7
|
+
*/
|
|
8
|
+
export declare function addDefineScreens(node: ts.Node, state: InspectorState, logger: InspectorLogger): void;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { ErrorCode } from '../error-codes.js';
|
|
3
|
+
const propName = (prop) => {
|
|
4
|
+
if (!ts.isPropertyAssignment(prop))
|
|
5
|
+
return undefined;
|
|
6
|
+
return ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)
|
|
7
|
+
? prop.name.text
|
|
8
|
+
: undefined;
|
|
9
|
+
};
|
|
10
|
+
const stringArray = (node) => {
|
|
11
|
+
if (!ts.isArrayLiteralExpression(node))
|
|
12
|
+
return undefined;
|
|
13
|
+
const values = [];
|
|
14
|
+
for (const element of node.elements) {
|
|
15
|
+
if (!ts.isStringLiteral(element))
|
|
16
|
+
return undefined;
|
|
17
|
+
values.push(element.text);
|
|
18
|
+
}
|
|
19
|
+
return values;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The module specifier of `() => import('./screens/x')`. Anything else cannot
|
|
23
|
+
* be handed to the host's bundler as a lazy import, so it is not read.
|
|
24
|
+
*/
|
|
25
|
+
const lazyImportSpecifier = (node) => {
|
|
26
|
+
if (!ts.isArrowFunction(node))
|
|
27
|
+
return undefined;
|
|
28
|
+
const body = node.body;
|
|
29
|
+
if (!ts.isCallExpression(body) ||
|
|
30
|
+
body.expression.kind !== ts.SyntaxKind.ImportKeyword)
|
|
31
|
+
return undefined;
|
|
32
|
+
const [specifier] = body.arguments;
|
|
33
|
+
return specifier && ts.isStringLiteral(specifier) ? specifier.text : undefined;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Detect `defineScreens({ title, icon, screens })` and record the manifest.
|
|
37
|
+
* A screen with a field that is not a literal is reported and left out: a
|
|
38
|
+
* navigation entry the host cannot read would otherwise vanish with no sign.
|
|
39
|
+
*/
|
|
40
|
+
export function addDefineScreens(node, state, logger) {
|
|
41
|
+
if (!ts.isCallExpression(node))
|
|
42
|
+
return;
|
|
43
|
+
const { expression, arguments: args } = node;
|
|
44
|
+
if (!ts.isIdentifier(expression) || expression.text !== 'defineScreens')
|
|
45
|
+
return;
|
|
46
|
+
const [firstArg] = args;
|
|
47
|
+
if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
|
|
48
|
+
return;
|
|
49
|
+
const fail = (message) => logger.critical(ErrorCode.ADDON_SCREENS_NOT_STATIC, message);
|
|
50
|
+
let title;
|
|
51
|
+
let icon;
|
|
52
|
+
const screens = [];
|
|
53
|
+
for (const prop of firstArg.properties) {
|
|
54
|
+
const key = propName(prop);
|
|
55
|
+
if (!key || !ts.isPropertyAssignment(prop))
|
|
56
|
+
continue;
|
|
57
|
+
if (key === 'title' && ts.isStringLiteral(prop.initializer)) {
|
|
58
|
+
title = prop.initializer.text;
|
|
59
|
+
}
|
|
60
|
+
else if (key === 'icon' && ts.isStringLiteral(prop.initializer)) {
|
|
61
|
+
icon = prop.initializer.text;
|
|
62
|
+
}
|
|
63
|
+
else if (key === 'screens') {
|
|
64
|
+
if (!ts.isArrayLiteralExpression(prop.initializer)) {
|
|
65
|
+
fail(`defineScreens's screens must be an array literal, got: ${prop.initializer.getText()}`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
for (const element of prop.initializer.elements) {
|
|
69
|
+
if (!ts.isObjectLiteralExpression(element)) {
|
|
70
|
+
fail(`A defineScreens screen must be an object literal, got: ${element.getText()}`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
let path;
|
|
74
|
+
let screenTitle;
|
|
75
|
+
let nav;
|
|
76
|
+
let scopes;
|
|
77
|
+
let component;
|
|
78
|
+
let app;
|
|
79
|
+
for (const field of element.properties) {
|
|
80
|
+
const fieldKey = propName(field);
|
|
81
|
+
if (!fieldKey || !ts.isPropertyAssignment(field))
|
|
82
|
+
continue;
|
|
83
|
+
const value = field.initializer;
|
|
84
|
+
if (fieldKey === 'path' && ts.isStringLiteral(value)) {
|
|
85
|
+
path = value.text;
|
|
86
|
+
}
|
|
87
|
+
else if (fieldKey === 'title' && ts.isStringLiteral(value)) {
|
|
88
|
+
screenTitle = value.text;
|
|
89
|
+
}
|
|
90
|
+
else if (fieldKey === 'nav' &&
|
|
91
|
+
(value.kind === ts.SyntaxKind.TrueKeyword ||
|
|
92
|
+
value.kind === ts.SyntaxKind.FalseKeyword)) {
|
|
93
|
+
nav = value.kind === ts.SyntaxKind.TrueKeyword;
|
|
94
|
+
}
|
|
95
|
+
else if (fieldKey === 'scopes') {
|
|
96
|
+
scopes = stringArray(value);
|
|
97
|
+
if (!scopes) {
|
|
98
|
+
fail(`A screen's scopes must be an array of string literals, got: ${value.getText()}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (fieldKey === 'component') {
|
|
102
|
+
component = lazyImportSpecifier(value);
|
|
103
|
+
if (!component) {
|
|
104
|
+
fail(`A screen's component must be () => import('<literal>'), got: ${value.getText()}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (fieldKey === 'app' && ts.isStringLiteral(value)) {
|
|
108
|
+
app = value.text;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!path || !screenTitle || (!component && !app)) {
|
|
112
|
+
fail(`A defineScreens screen needs a literal path and title, and either a lazy component or an app: ${element.getText()}`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (component && app) {
|
|
116
|
+
fail(`Screen '${path}' sets both component and app; a screen is one or the other`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
screens.push({
|
|
120
|
+
path,
|
|
121
|
+
title: screenTitle,
|
|
122
|
+
...(nav !== undefined ? { nav } : {}),
|
|
123
|
+
...(scopes ? { scopes } : {}),
|
|
124
|
+
...(component ? { component } : { app: app }),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!title) {
|
|
130
|
+
fail(`defineScreens needs a literal title`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
logger.debug(`• Found defineScreens: ${title} (${screens.length} screens)`);
|
|
134
|
+
state.screensManifest = {
|
|
135
|
+
title,
|
|
136
|
+
...(icon ? { icon } : {}),
|
|
137
|
+
file: node.getSourceFile().fileName,
|
|
138
|
+
screens,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -87,7 +87,8 @@ export function addWireAddon(node, state, logger) {
|
|
|
87
87
|
if (!ts.isCallExpression(node))
|
|
88
88
|
return;
|
|
89
89
|
const { expression, arguments: args } = node;
|
|
90
|
-
if (!ts.isIdentifier(expression) ||
|
|
90
|
+
if (!ts.isIdentifier(expression) ||
|
|
91
|
+
expression.text !== 'wireAddon')
|
|
91
92
|
return;
|
|
92
93
|
const [firstArg] = args;
|
|
93
94
|
if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
|
|
@@ -99,8 +100,10 @@ export function addWireAddon(node, state, logger) {
|
|
|
99
100
|
let expose;
|
|
100
101
|
let mcpEndpoint;
|
|
101
102
|
let auth;
|
|
103
|
+
let ui;
|
|
102
104
|
let tags;
|
|
103
105
|
let scopes;
|
|
106
|
+
let uses;
|
|
104
107
|
let secretOverrides;
|
|
105
108
|
let variableOverrides;
|
|
106
109
|
let credentialOverrides;
|
|
@@ -108,6 +111,9 @@ export function addWireAddon(node, state, logger) {
|
|
|
108
111
|
let credentialGrants;
|
|
109
112
|
let globalSecrets;
|
|
110
113
|
let globalCredentials;
|
|
114
|
+
let contentBucket;
|
|
115
|
+
let contentGrants;
|
|
116
|
+
let globalContent;
|
|
111
117
|
for (const prop of firstArg.properties) {
|
|
112
118
|
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
|
|
113
119
|
continue;
|
|
@@ -154,12 +160,25 @@ export function addWireAddon(node, state, logger) {
|
|
|
154
160
|
prop.initializer.kind === ts.SyntaxKind.FalseKeyword)) {
|
|
155
161
|
auth = prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
|
|
156
162
|
}
|
|
163
|
+
else if (key === 'ui' &&
|
|
164
|
+
(prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
|
165
|
+
prop.initializer.kind === ts.SyntaxKind.FalseKeyword)) {
|
|
166
|
+
ui = prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
|
|
167
|
+
}
|
|
157
168
|
else if (key === 'tags') {
|
|
158
169
|
tags = parseStringArray(prop.initializer);
|
|
159
170
|
}
|
|
160
171
|
else if (key === 'scopes') {
|
|
161
172
|
scopes = parseStringArray(prop.initializer);
|
|
162
173
|
}
|
|
174
|
+
else if (key === 'uses') {
|
|
175
|
+
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
176
|
+
uses = parseStringRecord(prop.initializer);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
logger.critical(ErrorCode.ADDON_EXPOSE_NOT_STATIC, `wireAddon's uses must be an object literal, got: ${prop.initializer.getText()}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
163
182
|
else if (key === 'secretOverrides' &&
|
|
164
183
|
ts.isObjectLiteralExpression(prop.initializer)) {
|
|
165
184
|
secretOverrides = parseStringRecord(prop.initializer);
|
|
@@ -190,12 +209,26 @@ export function addWireAddon(node, state, logger) {
|
|
|
190
209
|
? prop.initializer.text
|
|
191
210
|
: prop.initializer.getText();
|
|
192
211
|
}
|
|
212
|
+
else if (key === 'contentBucket' &&
|
|
213
|
+
ts.isStringLiteral(prop.initializer)) {
|
|
214
|
+
contentBucket = prop.initializer.text;
|
|
215
|
+
}
|
|
216
|
+
else if (key === 'contentGrants' &&
|
|
217
|
+
ts.isObjectLiteralExpression(prop.initializer)) {
|
|
218
|
+
contentGrants = parseStringRecord(prop.initializer);
|
|
219
|
+
}
|
|
220
|
+
else if (key === 'globalContent') {
|
|
221
|
+
globalContent = ts.isStringLiteral(prop.initializer)
|
|
222
|
+
? prop.initializer.text
|
|
223
|
+
: prop.initializer.getText();
|
|
224
|
+
}
|
|
193
225
|
}
|
|
194
226
|
if (!name || !pkg)
|
|
195
227
|
return;
|
|
196
|
-
logger.debug(`• Found
|
|
228
|
+
logger.debug(`• Found ${expression.text}: ${name} → ${pkg}`);
|
|
197
229
|
state.rpc.wireAddonDeclarations.set(name, {
|
|
198
230
|
package: pkg,
|
|
231
|
+
...(ui ? { ui } : {}),
|
|
199
232
|
file: node.getSourceFile().fileName,
|
|
200
233
|
rpcEndpoint,
|
|
201
234
|
mcp,
|
|
@@ -204,6 +237,7 @@ export function addWireAddon(node, state, logger) {
|
|
|
204
237
|
auth,
|
|
205
238
|
tags,
|
|
206
239
|
scopes,
|
|
240
|
+
uses,
|
|
207
241
|
secretOverrides,
|
|
208
242
|
variableOverrides,
|
|
209
243
|
credentialOverrides,
|
|
@@ -211,6 +245,9 @@ export function addWireAddon(node, state, logger) {
|
|
|
211
245
|
credentialGrants,
|
|
212
246
|
globalSecrets,
|
|
213
247
|
globalCredentials,
|
|
248
|
+
contentBucket,
|
|
249
|
+
contentGrants,
|
|
250
|
+
globalContent,
|
|
214
251
|
});
|
|
215
252
|
state.rpc.usedAddons.add(name);
|
|
216
253
|
state.rpc.wireAddonFiles.add(node.getSourceFile().fileName);
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -36,6 +36,11 @@ export declare enum ErrorCode {
|
|
|
36
36
|
ADDON_MCP_FUNCTION_NOT_FOUND = "PKU341",
|
|
37
37
|
ADDON_EXPOSE_FUNCTION_NOT_FOUND = "PKU343",
|
|
38
38
|
ADDON_EXPOSE_NOT_STATIC = "PKU344",
|
|
39
|
+
ADDON_SCREENS_NOT_STATIC = "PKU345",
|
|
40
|
+
ADDON_UI_HAS_NO_SCREENS = "PKU346",
|
|
41
|
+
ADDON_USES_NOT_WIRED = "PKU347",
|
|
42
|
+
ADDON_NEEDS_NOT_MAPPED = "PKU348",
|
|
43
|
+
ADDON_CONTENT_GRANT_INVALID = "PKU349",
|
|
39
44
|
DB_COLUMN_NAME_TYPE_CONTRADICTION = "PKU480",
|
|
40
45
|
DB_JSON_COLUMN_UNTYPED = "PKU481",
|
|
41
46
|
DB_FORMAT_ON_NON_STRING = "PKU482",
|
package/dist/error-codes.js
CHANGED
|
@@ -39,6 +39,11 @@ export var ErrorCode;
|
|
|
39
39
|
ErrorCode["ADDON_MCP_FUNCTION_NOT_FOUND"] = "PKU341";
|
|
40
40
|
ErrorCode["ADDON_EXPOSE_FUNCTION_NOT_FOUND"] = "PKU343";
|
|
41
41
|
ErrorCode["ADDON_EXPOSE_NOT_STATIC"] = "PKU344";
|
|
42
|
+
ErrorCode["ADDON_SCREENS_NOT_STATIC"] = "PKU345";
|
|
43
|
+
ErrorCode["ADDON_UI_HAS_NO_SCREENS"] = "PKU346";
|
|
44
|
+
ErrorCode["ADDON_USES_NOT_WIRED"] = "PKU347";
|
|
45
|
+
ErrorCode["ADDON_NEEDS_NOT_MAPPED"] = "PKU348";
|
|
46
|
+
ErrorCode["ADDON_CONTENT_GRANT_INVALID"] = "PKU349";
|
|
42
47
|
// Database schema codegen warnings
|
|
43
48
|
ErrorCode["DB_COLUMN_NAME_TYPE_CONTRADICTION"] = "PKU480";
|
|
44
49
|
ErrorCode["DB_JSON_COLUMN_UNTYPED"] = "PKU481";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { inspect, getInitialInspectorState } from './inspector.js';
|
|
2
2
|
export type { TypesMap } from './types-map.js';
|
|
3
|
-
export type { AddonConfig, AuthDefinition, CredentialOverrideMeta, InspectorDiagnostic, InspectorFilters, InspectorHTTPState, InspectorLogger, InspectorMiddlewareState, InspectorOptions, InspectorPermissionState, InspectorState, MetaInputTypes, MiddlewareGroupMeta, MiddlewareRegistration, SchemaRef, } from './types.js';
|
|
3
|
+
export type { AddonConfig, AuthDefinition, CredentialOverrideMeta, ScreensManifestMeta, AddonScreenMeta, InspectorDiagnostic, InspectorFilters, InspectorHTTPState, InspectorLogger, InspectorMiddlewareState, InspectorOptions, InspectorPermissionState, InspectorState, MetaInputTypes, MiddlewareGroupMeta, MiddlewareRegistration, SchemaRef, } from './types.js';
|
|
4
4
|
export type { FilesAndMethodsErrors } from './utils/get-files-and-methods.js';
|
|
5
5
|
export { ErrorCode } from './error-codes.js';
|
|
6
6
|
export type { DiagnosticSeverity, CodedDiagnostic } from './error-codes.js';
|
package/dist/inspector.js
CHANGED
|
@@ -9,7 +9,7 @@ import { findCommonAncestor } from './utils/find-root-dir.js';
|
|
|
9
9
|
import { createSourceFileCacheHost } from './source-file-cache.js';
|
|
10
10
|
import { createNestedProjectFilter } from './utils/nested-project-filter.js';
|
|
11
11
|
import { toProgramPath } from './utils/program-path.js';
|
|
12
|
-
import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateAgentToolReferences, validateAgentScorerReferences, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, validateSystemRoleScopes, validatePersonaRoles, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, validateNoSecretAliasServices, registerDerivedOAuth2AppSecrets, validateSecretUsage, computeDiagnostics, validateSchemaReferences, validateSchemaWiringSeparation, validateScenarioServices, validateScenarioSteps, validateScenarioFeatures, validateWorkflowGraphAddons, } from './utils/post-process.js';
|
|
12
|
+
import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateAgentToolReferences, validateAgentScorerReferences, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateAddonUses, validateAddonContentGrants, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, validateSystemRoleScopes, validatePersonaRoles, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, validateNoSecretAliasServices, registerDerivedOAuth2AppSecrets, validateSecretUsage, computeDiagnostics, validateSchemaReferences, validateSchemaWiringSeparation, validateScenarioServices, validateScenarioSteps, validateScenarioFeatures, validateWorkflowGraphAddons, } from './utils/post-process.js';
|
|
13
13
|
import { validateExposedFunctionsGated } from './utils/validate-exposed-functions-gated.js';
|
|
14
14
|
import { validateTagsResolveToMiddleware } from './utils/validate-tags-resolve-to-middleware.js';
|
|
15
15
|
import { annotateHttpRouteAuth } from './utils/annotate-http-route-auth.js';
|
|
@@ -427,6 +427,8 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
427
427
|
validateSecretOverrides(logger, state);
|
|
428
428
|
validateVariableOverrides(logger, state);
|
|
429
429
|
validateCredentialOverrides(logger, state);
|
|
430
|
+
validateAddonUses(logger, state);
|
|
431
|
+
validateAddonContentGrants(logger, state);
|
|
430
432
|
validateRemoteAddonDependencies(logger, state);
|
|
431
433
|
validateRemoteAddonAuth(logger, state);
|
|
432
434
|
applyDeclaredScopes(state);
|
package/dist/types.d.ts
CHANGED
|
@@ -491,7 +491,38 @@ export type CredentialOverrideMeta = string | {
|
|
|
491
491
|
name?: string;
|
|
492
492
|
mode?: 'singleton' | 'wire';
|
|
493
493
|
};
|
|
494
|
+
/** A screen of an addon as `defineScreens` declares it, read statically. */
|
|
495
|
+
export type AddonScreenMeta = {
|
|
496
|
+
path: string;
|
|
497
|
+
title: string;
|
|
498
|
+
nav?: boolean;
|
|
499
|
+
scopes?: string[];
|
|
500
|
+
} & ({
|
|
501
|
+
component: string;
|
|
502
|
+
app?: undefined;
|
|
503
|
+
} | {
|
|
504
|
+
app: string;
|
|
505
|
+
component?: undefined;
|
|
506
|
+
});
|
|
507
|
+
export type ScreensManifestMeta = {
|
|
508
|
+
title: string;
|
|
509
|
+
icon?: string;
|
|
510
|
+
/** The source file whose `defineScreens` call declared this manifest. */
|
|
511
|
+
file: string;
|
|
512
|
+
screens: AddonScreenMeta[];
|
|
513
|
+
/**
|
|
514
|
+
* The addon's role: every scope its own functions require, derived by
|
|
515
|
+
* `pikku all` and never declared. Absent from the raw `defineScreens` read.
|
|
516
|
+
* On a wired instance it also carries the host's `wireAddon` scopes, which
|
|
517
|
+
* the runner demands of every function in the package.
|
|
518
|
+
*/
|
|
519
|
+
scopes?: string[];
|
|
520
|
+
};
|
|
494
521
|
export interface InspectorState {
|
|
522
|
+
/** Set when this package calls `defineScreens`: the screens it ships. */
|
|
523
|
+
screensManifest?: ScreensManifestMeta;
|
|
524
|
+
/** The screens of the addons this project wires with `ui: true`, by namespace. */
|
|
525
|
+
addonScreens?: Record<string, ScreensManifestMeta>;
|
|
495
526
|
rootDir: string;
|
|
496
527
|
singletonServicesTypeImportMap: PathToNameAndType;
|
|
497
528
|
wireServicesTypeImportMap: PathToNameAndType;
|
|
@@ -605,7 +636,11 @@ export interface InspectorState {
|
|
|
605
636
|
package: string;
|
|
606
637
|
/** The app source file whose `wireAddon` call declared this instance. */
|
|
607
638
|
file?: string;
|
|
639
|
+
/** Declared with `ui: true`: the package must ship screens, and the host mounts them. */
|
|
640
|
+
ui?: boolean;
|
|
608
641
|
rpcEndpoint?: string;
|
|
642
|
+
/** The other addons this instance may call: package -> the name it is wired under. */
|
|
643
|
+
uses?: Record<string, string>;
|
|
609
644
|
/**
|
|
610
645
|
* `true` offers every function the addon declared `mcp: true`; a list
|
|
611
646
|
* names the functions to offer, whatever the addon declared. Absent
|
|
@@ -660,6 +695,13 @@ export interface InspectorState {
|
|
|
660
695
|
* `CredentialService` instead of one scoped to its declared credentials.
|
|
661
696
|
*/
|
|
662
697
|
globalCredentials?: string;
|
|
698
|
+
contentBucket?: string;
|
|
699
|
+
contentGrants?: Record<string, string>;
|
|
700
|
+
/**
|
|
701
|
+
* The app's stated reason for handing this instance the whole
|
|
702
|
+
* `ContentService` instead of one scoped to its own folder.
|
|
703
|
+
*/
|
|
704
|
+
globalContent?: string;
|
|
663
705
|
}>;
|
|
664
706
|
wireAddonFiles: Set<string>;
|
|
665
707
|
};
|
|
@@ -8,11 +8,6 @@ export declare const describeMissingAddonMeta: (namespace: string, packageName:
|
|
|
8
8
|
* Every source starts off: the app turns one on at runtime.
|
|
9
9
|
*/
|
|
10
10
|
export declare const namespaceAddonWebhookSources: (sources: WebhookSourcesMeta, namespace: string) => WebhookSourcesMeta;
|
|
11
|
-
/**
|
|
12
|
-
* After the setup sweep discovers wireAddon() declarations, load each addon
|
|
13
|
-
* package's function metadata so that wiring handlers (channels, HTTP routes,
|
|
14
|
-
* schedules, etc.) can look up addon function types during the routes sweep.
|
|
15
|
-
*/
|
|
16
11
|
export declare function loadAddonFunctionsMeta(logger: InspectorLogger, state: InspectorState): Promise<void>;
|
|
17
12
|
/**
|
|
18
13
|
* Load addon schemas into state.schemas. Called after generateAllSchemas
|
|
@@ -181,6 +181,27 @@ export const namespaceAddonWebhookSources = (sources, namespace) => {
|
|
|
181
181
|
* package's function metadata so that wiring handlers (channels, HTTP routes,
|
|
182
182
|
* schedules, etc.) can look up addon function types during the routes sweep.
|
|
183
183
|
*/
|
|
184
|
+
const loadScreensManifest = async (require, packageName) => {
|
|
185
|
+
const path = resolveAddonMeta(require, packageName, 'screens/pikku-screens-meta');
|
|
186
|
+
if (!path)
|
|
187
|
+
return null;
|
|
188
|
+
return JSON.parse(await readFile(path, 'utf-8'));
|
|
189
|
+
};
|
|
190
|
+
const readAddonNeeds = async (dirs, packageName) => {
|
|
191
|
+
const dir = findInstalledPackageDir(dirs, packageName);
|
|
192
|
+
if (!dir)
|
|
193
|
+
return [];
|
|
194
|
+
try {
|
|
195
|
+
const pkg = JSON.parse(await readFile(join(dir, 'package.json'), 'utf-8'));
|
|
196
|
+
const uses = pkg?.pikku?.uses;
|
|
197
|
+
return Array.isArray(uses)
|
|
198
|
+
? uses.filter((u) => typeof u === 'string')
|
|
199
|
+
: [];
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
};
|
|
184
205
|
export async function loadAddonFunctionsMeta(logger, state) {
|
|
185
206
|
const { wireAddonDeclarations } = state.rpc;
|
|
186
207
|
if (wireAddonDeclarations.size === 0)
|
|
@@ -194,6 +215,37 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
194
215
|
const dirs = addonResolutionDirs(state.rootDir, decl.file);
|
|
195
216
|
const require = createAddonResolver(dirs);
|
|
196
217
|
try {
|
|
218
|
+
for (const needed of await readAddonNeeds(dirs, decl.package)) {
|
|
219
|
+
if (!decl.uses?.[needed]) {
|
|
220
|
+
logger.critical(ErrorCode.ADDON_NEEDS_NOT_MAPPED, `${decl.package} needs ${needed}, but wireAddon('${namespace}') does not map it. Wire ${needed} and add uses: { '${needed}': '<name>' } to this wireAddon.`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (decl.ui) {
|
|
224
|
+
let manifest;
|
|
225
|
+
let unreadable = false;
|
|
226
|
+
try {
|
|
227
|
+
manifest = await loadScreensManifest(require, decl.package);
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
logger.critical(ErrorCode.ADDON_UI_HAS_NO_SCREENS, `wireAddon('${namespace}') sets ui: true, but the screens manifest of ${decl.package} could not be read: ${e instanceof Error ? e.message : e}`);
|
|
231
|
+
unreadable = true;
|
|
232
|
+
}
|
|
233
|
+
if (unreadable) {
|
|
234
|
+
// reported above; the addon's functions and scopes still load
|
|
235
|
+
}
|
|
236
|
+
else if (!manifest || manifest.screens.length === 0) {
|
|
237
|
+
logger.critical(ErrorCode.ADDON_UI_HAS_NO_SCREENS, `wireAddon('${namespace}') sets ui: true, but ${decl.package} declares no screens. Add a defineScreens call to the package, or drop ui.`);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
;
|
|
241
|
+
(state.addonScreens ??= {})[namespace] = {
|
|
242
|
+
...manifest,
|
|
243
|
+
scopes: [
|
|
244
|
+
...new Set([...(manifest.scopes ?? []), ...(decl.scopes ?? [])]),
|
|
245
|
+
].sort(),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
197
249
|
// Verbose first. `description` is one of the fields stripped from the
|
|
198
250
|
// minimal copy, and it is what an addon's function is offered to a model
|
|
199
251
|
// under — both as an MCP tool below and as an agent tool. Read the minimal
|
|
@@ -31,6 +31,8 @@ export declare function extractWireNames(list?: Array<MiddlewareMetadata | Permi
|
|
|
31
31
|
*/
|
|
32
32
|
export declare function aggregateRequiredServices(state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
33
33
|
export declare function validateSecretOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
34
|
+
export declare function validateAddonUses(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
35
|
+
export declare function validateAddonContentGrants(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
34
36
|
export declare function validateCredentialOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
35
37
|
export declare function validateVariableOverrides(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, "typesLookup">): void;
|
|
36
38
|
/**
|
|
@@ -393,6 +393,39 @@ export function validateSecretOverrides(logger, state) {
|
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
395
|
}
|
|
396
|
+
export function validateAddonUses(logger, state) {
|
|
397
|
+
const { wireAddonDeclarations } = state.rpc;
|
|
398
|
+
if (!wireAddonDeclarations || wireAddonDeclarations.size === 0)
|
|
399
|
+
return;
|
|
400
|
+
for (const [namespace, addonDecl] of wireAddonDeclarations.entries()) {
|
|
401
|
+
for (const [pkg, target] of Object.entries(addonDecl.uses ?? {})) {
|
|
402
|
+
const wired = wireAddonDeclarations.get(target);
|
|
403
|
+
if (!wired) {
|
|
404
|
+
logger.critical(ErrorCode.ADDON_USES_NOT_WIRED, `Addon '${namespace}' uses '${pkg}' as '${target}', but no addon is wired under the name '${target}'. Wired: ${Array.from(wireAddonDeclarations.keys()).join(", ")}`);
|
|
405
|
+
}
|
|
406
|
+
else if (wired.package !== pkg) {
|
|
407
|
+
logger.critical(ErrorCode.ADDON_USES_NOT_WIRED, `Addon '${namespace}' uses '${pkg}' as '${target}', but '${target}' is wired from '${wired.package}'.`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
export function validateAddonContentGrants(logger, state) {
|
|
413
|
+
const { wireAddonDeclarations } = state.rpc;
|
|
414
|
+
if (!wireAddonDeclarations || wireAddonDeclarations.size === 0)
|
|
415
|
+
return;
|
|
416
|
+
const buckets = new Set(Array.from(wireAddonDeclarations.entries()).map(([namespace, decl]) => decl.contentBucket ?? namespace));
|
|
417
|
+
for (const [namespace, decl] of wireAddonDeclarations.entries()) {
|
|
418
|
+
for (const [prefix, mode] of Object.entries(decl.contentGrants ?? {})) {
|
|
419
|
+
if (mode !== "read" && mode !== "write") {
|
|
420
|
+
logger.critical(ErrorCode.ADDON_CONTENT_GRANT_INVALID, `Addon '${namespace}' grants '${prefix}' as '${mode}', but a content grant must be 'read' or 'write'.`);
|
|
421
|
+
}
|
|
422
|
+
const first = prefix.split("/").find((segment) => segment !== "");
|
|
423
|
+
if (!first || !buckets.has(first)) {
|
|
424
|
+
logger.critical(ErrorCode.ADDON_CONTENT_GRANT_INVALID, `Addon '${namespace}' grants content under '${prefix}', but '${first ?? ""}' is not the content bucket of any wired addon. Buckets: ${Array.from(buckets).join(", ")}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
396
429
|
export function validateCredentialOverrides(logger, state) {
|
|
397
430
|
const { wireAddonDeclarations } = state.rpc;
|
|
398
431
|
if (!wireAddonDeclarations || wireAddonDeclarations.size === 0)
|
|
@@ -204,6 +204,9 @@ export interface SerializableInspectorState {
|
|
|
204
204
|
credentialGrants?: string[];
|
|
205
205
|
globalSecrets?: string;
|
|
206
206
|
globalCredentials?: string;
|
|
207
|
+
contentBucket?: string;
|
|
208
|
+
contentGrants?: Record<string, string>;
|
|
209
|
+
globalContent?: string;
|
|
207
210
|
}
|
|
208
211
|
]>;
|
|
209
212
|
wireAddonFiles: string[];
|
|
@@ -317,6 +320,8 @@ export interface SerializableInspectorState {
|
|
|
317
320
|
openAPISpec: Record<string, any> | null;
|
|
318
321
|
diagnostics: InspectorDiagnostic[];
|
|
319
322
|
addonFunctions: InspectorState['addonFunctions'];
|
|
323
|
+
screensManifest?: InspectorState['screensManifest'];
|
|
324
|
+
addonScreens?: InspectorState['addonScreens'];
|
|
320
325
|
exportedContracts: InspectorState['exportedContracts'];
|
|
321
326
|
}
|
|
322
327
|
/**
|
|
@@ -182,6 +182,8 @@ export function serializeInspectorState(state) {
|
|
|
182
182
|
openAPISpec: state.openAPISpec,
|
|
183
183
|
diagnostics: state.diagnostics,
|
|
184
184
|
addonFunctions: state.addonFunctions,
|
|
185
|
+
screensManifest: state.screensManifest,
|
|
186
|
+
addonScreens: state.addonScreens,
|
|
185
187
|
exportedContracts: state.exportedContracts,
|
|
186
188
|
};
|
|
187
189
|
}
|
|
@@ -378,6 +380,8 @@ export function deserializeInspectorState(data) {
|
|
|
378
380
|
openAPISpec: data.openAPISpec || null,
|
|
379
381
|
diagnostics: data.diagnostics || [],
|
|
380
382
|
addonFunctions: data.addonFunctions || {},
|
|
383
|
+
screensManifest: data.screensManifest,
|
|
384
|
+
addonScreens: data.addonScreens,
|
|
381
385
|
exportedContracts: data.exportedContracts || {
|
|
382
386
|
http: {},
|
|
383
387
|
cli: {},
|
package/dist/visit.js
CHANGED
|
@@ -15,6 +15,7 @@ import { addChannel } from './add/add-channel.js';
|
|
|
15
15
|
import { addGateway } from './add/add-gateway.js';
|
|
16
16
|
import { addRPCInvocations } from './add/add-rpc-invocations.js';
|
|
17
17
|
import { addWireAddon } from './add/add-wire-addon.js';
|
|
18
|
+
import { addDefineScreens } from './add/add-define-screens.js';
|
|
18
19
|
import { addWireRemoteAddon } from './add/add-wire-remote-addon.js';
|
|
19
20
|
import { addMiddleware } from './add/add-middleware.js';
|
|
20
21
|
import { addPermission } from './add/add-permission.js';
|
|
@@ -47,6 +48,7 @@ export const visitSetup = (logger, checker, node, state, options) => {
|
|
|
47
48
|
addOutgoingWebhook(logger, node, checker, state);
|
|
48
49
|
addRPCInvocations(node, state, logger);
|
|
49
50
|
addWireAddon(node, state, logger);
|
|
51
|
+
addDefineScreens(node, state, logger);
|
|
50
52
|
addWireRemoteAddon(node, state, logger);
|
|
51
53
|
addMiddleware(logger, node, checker, state, options);
|
|
52
54
|
addPermission(logger, node, checker, state, options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.103",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"zod": "^4.4.3"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@pikku/core": "^0.12.
|
|
57
|
+
"@pikku/core": "^0.12.139"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@pikku/core": "^0.12.
|
|
60
|
+
"@pikku/core": "^0.12.139",
|
|
61
61
|
"@types/node": "^24.13.3"
|
|
62
62
|
},
|
|
63
63
|
"engines": {
|