@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 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) || expression.text !== 'wireAddon')
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 wireAddon: ${name} → ${pkg}`);
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);
@@ -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",
@@ -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.102",
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.137"
57
+ "@pikku/core": "^0.12.139"
58
58
  },
59
59
  "devDependencies": {
60
- "@pikku/core": "^0.12.137",
60
+ "@pikku/core": "^0.12.139",
61
61
  "@types/node": "^24.13.3"
62
62
  },
63
63
  "engines": {