@openclaw/plugin-inspector 0.1.3 → 0.3.0

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/src/init.js CHANGED
@@ -5,32 +5,66 @@ import { inferPluginSeams, packageId } from "./config.js";
5
5
 
6
6
  export const defaultInitConfigPath = "plugin-inspector.config.json";
7
7
  export const defaultInitWorkflowPath = ".github/workflows/plugin-inspector.yml";
8
+ export const defaultInitPackageScripts = {
9
+ "plugin:check": "plugin-inspector inspect --no-openclaw",
10
+ "plugin:ci": "plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute",
11
+ };
8
12
 
9
13
  export async function writePluginInspectorInit(options = {}) {
10
14
  const pluginRoot = path.resolve(options.pluginRoot ?? options.cwd ?? process.cwd());
11
15
  const configPath = path.resolve(pluginRoot, options.configPath ?? defaultInitConfigPath);
16
+ const workflowPath = options.ci === true ? path.resolve(pluginRoot, options.workflowPath ?? defaultInitWorkflowPath) : null;
17
+ const packageManager = options.packageManager ?? (await detectPackageManager(pluginRoot));
18
+ const dryRun = options.dryRun === true;
12
19
  const written = [];
13
20
 
14
- if (existsSync(configPath) && options.force !== true) {
21
+ if (!dryRun && existsSync(configPath) && options.force !== true) {
15
22
  throw new Error(`${path.relative(pluginRoot, configPath)} already exists; pass --force to overwrite it`);
16
23
  }
24
+ if (!dryRun && workflowPath && existsSync(workflowPath) && options.force !== true) {
25
+ throw new Error(`${path.relative(pluginRoot, workflowPath)} already exists; pass --force to overwrite it`);
26
+ }
27
+ const packageJsonPath = path.join(pluginRoot, "package.json");
28
+ const packageJson = options.scripts === true ? await readJsonIfExists(packageJsonPath) : null;
29
+ if (options.scripts === true) {
30
+ if (!packageJson) {
31
+ throw new Error("package.json is required to write plugin-inspector package scripts");
32
+ }
33
+ for (const name of Object.keys(defaultInitPackageScripts)) {
34
+ if (!dryRun && packageJson.scripts?.[name] && options.force !== true) {
35
+ throw new Error(`package.json scripts.${name} already exists; pass --force to overwrite it`);
36
+ }
37
+ }
38
+ }
17
39
 
18
40
  const config = await buildPluginInspectorConfig({ pluginRoot });
19
- await mkdir(path.dirname(configPath), { recursive: true });
20
- await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
41
+ if (!dryRun) {
42
+ await mkdir(path.dirname(configPath), { recursive: true });
43
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
44
+ }
21
45
  written.push(configPath);
22
46
 
23
- if (options.ci === true) {
24
- const workflowPath = path.resolve(pluginRoot, options.workflowPath ?? defaultInitWorkflowPath);
25
- if (existsSync(workflowPath) && options.force !== true) {
26
- throw new Error(`${path.relative(pluginRoot, workflowPath)} already exists; pass --force to overwrite it`);
47
+ if (workflowPath) {
48
+ if (!dryRun) {
49
+ await mkdir(path.dirname(workflowPath), { recursive: true });
50
+ await writeFile(workflowPath, renderGithubActionsWorkflow({ packageManager }), "utf8");
27
51
  }
28
- await mkdir(path.dirname(workflowPath), { recursive: true });
29
- await writeFile(workflowPath, renderGithubActionsWorkflow({ packageManager: options.packageManager }), "utf8");
30
52
  written.push(workflowPath);
31
53
  }
32
54
 
33
- return { pluginRoot, configPath, written };
55
+ if (options.scripts === true) {
56
+ const existingScripts = packageJson.scripts ?? {};
57
+ packageJson.scripts = {
58
+ ...existingScripts,
59
+ ...defaultInitPackageScripts,
60
+ };
61
+ if (!dryRun) {
62
+ await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
63
+ }
64
+ written.push(packageJsonPath);
65
+ }
66
+
67
+ return { pluginRoot, configPath, dryRun, packageManager, written };
34
68
  }
35
69
 
36
70
  export async function buildPluginInspectorConfig(options = {}) {
@@ -79,8 +113,7 @@ jobs:
79
113
  node-version: 24
80
114
  cache: ${setup.cache}
81
115
  ${setup.corepack ? " - run: corepack enable\n" : ""} - run: ${setup.install}
82
- - run: ${setup.exec} @openclaw/plugin-inspector check --no-openclaw
83
- - run: PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 ${setup.exec} @openclaw/plugin-inspector check --no-openclaw --runtime --mock-sdk
116
+ - run: ${setup.exec} @openclaw/plugin-inspector ci --no-openclaw --runtime --mock-sdk --allow-execute
84
117
  - uses: actions/upload-artifact@v5
85
118
  if: always()
86
119
  with:
@@ -89,19 +122,62 @@ ${setup.corepack ? " - run: corepack enable\n" : ""} - run: ${setup.in
89
122
  `;
90
123
  }
91
124
 
125
+ export async function detectPackageManager(pluginRoot) {
126
+ const root = path.resolve(pluginRoot ?? process.cwd());
127
+ const packageJson = await readJsonIfExists(path.join(root, "package.json"));
128
+ const packageManager = packageJson?.packageManager;
129
+ if (typeof packageManager === "string") {
130
+ const [name] = packageManager.split("@");
131
+ if (["npm", "pnpm", "yarn", "bun"].includes(name)) {
132
+ return name;
133
+ }
134
+ }
135
+
136
+ if (existsSync(path.join(root, "pnpm-lock.yaml"))) {
137
+ return "pnpm";
138
+ }
139
+ if (existsSync(path.join(root, "yarn.lock"))) {
140
+ return "yarn";
141
+ }
142
+ if (existsSync(path.join(root, "bun.lockb")) || existsSync(path.join(root, "bun.lock"))) {
143
+ return "bun";
144
+ }
145
+ return "npm";
146
+ }
147
+
92
148
  function inferSourceRoot(packageJson) {
93
149
  const entrypoints = [
94
150
  packageJson?.openclaw?.entrypoint,
95
151
  ...(packageJson?.openclaw?.extensions ?? []),
96
152
  ...(packageJson?.openclaw?.runtimeExtensions ?? []),
153
+ ...entrypointStrings(packageJson?.exports?.["."]),
154
+ ...entrypointStrings(packageJson?.exports),
155
+ packageJson?.module,
156
+ packageJson?.main,
97
157
  ].filter((value) => typeof value === "string");
98
- const entrypoint = entrypoints[0] ?? packageJson?.exports?.["."] ?? packageJson?.main ?? "src/index.js";
99
- if (typeof entrypoint === "string" && entrypoint.startsWith("src/")) {
158
+ const entrypoint = entrypoints[0] ?? "src/index.js";
159
+ if (stripRelativePrefix(entrypoint).startsWith("src/")) {
100
160
  return "src";
101
161
  }
102
162
  return ".";
103
163
  }
104
164
 
165
+ function entrypointStrings(value) {
166
+ if (typeof value === "string") {
167
+ return [value];
168
+ }
169
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
170
+ return [];
171
+ }
172
+ return ["import", "default", "require", "node", "module"]
173
+ .map((key) => value[key])
174
+ .filter((item) => typeof item === "string");
175
+ }
176
+
177
+ function stripRelativePrefix(filePath) {
178
+ return filePath.replace(/^\.\//, "");
179
+ }
180
+
105
181
  async function readJsonIfExists(filePath) {
106
182
  if (!existsSync(filePath)) {
107
183
  return null;
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { mkdtemp, rm } from "node:fs/promises";
2
+ import { rmSync } from "node:fs";
3
+ import { mkdtemp } from "node:fs/promises";
3
4
  import { register } from "node:module";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
@@ -26,13 +27,16 @@ async function run(options) {
26
27
  const pluginRoot = path.resolve(options.cwd ?? process.cwd(), options.pluginRoot ?? path.dirname(entrypoint));
27
28
  const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-mock-sdk-"));
28
29
 
29
- try {
30
- const { loaderPath } = await createMockSdkPackage(workspace, { pluginRoot });
31
- register(pathToFileURL(loaderPath));
32
- return await captureLinkedEntrypoint(entrypoint, options);
33
- } finally {
34
- await rm(workspace, { force: true, recursive: true });
35
- }
30
+ cleanupTempDirOnExit(workspace);
31
+ const { loaderPath } = await createMockSdkPackage(workspace, { pluginRoot });
32
+ register(pathToFileURL(loaderPath));
33
+ return await captureLinkedEntrypoint(entrypoint, options);
34
+ }
35
+
36
+ function cleanupTempDirOnExit(dir) {
37
+ process.once("exit", () => {
38
+ rmSync(dir, { force: true, recursive: true });
39
+ });
36
40
  }
37
41
 
38
42
  async function captureLinkedEntrypoint(entrypoint, options) {
package/src/report.js CHANGED
@@ -246,27 +246,79 @@ export async function writeReport(report, options = {}) {
246
246
  export async function writeCompatibilityReport(report, options = {}) {
247
247
  const outDir = path.resolve(options.cwd ?? process.cwd(), options.outDir ?? "reports");
248
248
  const basename = options.basename ?? "plugin-inspector-report";
249
- const jsonPath = path.join(outDir, `${basename}.json`);
250
- const markdownPath = path.join(outDir, `${basename}.md`);
251
- const issuesPath = path.join(outDir, options.issuesBasename ?? "plugin-inspector-issues.md");
249
+ const jsonPath = options.jsonPath ?? path.join(outDir, `${basename}.json`);
250
+ const markdownPath = options.markdownPath ?? path.join(outDir, `${basename}.md`);
251
+ const issuesPath = options.issuesPath ?? path.join(outDir, options.issuesBasename ?? "plugin-inspector-issues.md");
252
+ const markdownOptions = compatibilityRenderOptions(options, {
253
+ title: options.markdownTitle ?? options.title,
254
+ ...options.markdownOptions,
255
+ });
256
+ const issuesOptions = compatibilityRenderOptions(options, {
257
+ title: options.issuesTitle ?? options.title,
258
+ ...options.issuesOptions,
259
+ });
252
260
 
253
261
  return writeArtifacts(
254
262
  [
255
263
  { name: "jsonPath", path: jsonPath, json: report },
256
- { name: "markdownPath", path: markdownPath, markdown: renderCompatibilityMarkdownReport(report) },
257
- { name: "issuesPath", path: issuesPath, markdown: renderCompatibilityIssuesReport(report) },
264
+ { name: "markdownPath", path: markdownPath, markdown: renderCompatibilityMarkdownReport(report, markdownOptions) },
265
+ { name: "issuesPath", path: issuesPath, markdown: renderCompatibilityIssuesReport(report, issuesOptions) },
258
266
  ],
259
267
  { check: options.check },
260
268
  );
261
269
  }
262
270
 
263
- export function renderTextSummary(report) {
264
- return [
271
+ function compatibilityRenderOptions(options, overrides) {
272
+ const renderOptions = {
273
+ formatEvidence: options.formatEvidence,
274
+ severityLabels: options.severityLabels,
275
+ ...overrides,
276
+ };
277
+ return Object.fromEntries(Object.entries(renderOptions).filter(([, value]) => value !== undefined));
278
+ }
279
+
280
+ export function renderTextSummary(report, options = {}) {
281
+ const lines = [
265
282
  `Status: ${report.status.toUpperCase()}`,
266
283
  `Fixtures: ${report.summary.fixtureCount}`,
267
284
  `Breakages: ${report.summary.breakageCount}`,
285
+ ...(typeof report.summary.issueCount === "number" ? [`Issues: ${report.summary.issueCount}`] : []),
268
286
  `Logs: ${report.summary.logCount}`,
269
- ].join("\n");
287
+ ];
288
+ const artifacts = Object.entries(options.artifacts ?? {}).filter(([, filePath]) => Boolean(filePath));
289
+ if (artifacts.length > 0) {
290
+ lines.push("", "Reports:", ...artifacts.map(([name, filePath]) => `- ${artifactLabel(name)}: ${filePath}`));
291
+ }
292
+ const findings = topTextFindings(report, options.topFindings ?? 3);
293
+ if (findings.length > 0) {
294
+ lines.push("", "Top findings:", ...findings.map((finding) => `- ${finding}`));
295
+ }
296
+ return lines.join("\n");
297
+ }
298
+
299
+ function topTextFindings(report, limit) {
300
+ if (report.status === "pass" || limit <= 0) {
301
+ return [];
302
+ }
303
+ return [
304
+ ...(report.breakages ?? []).map((finding) => formatTextFinding(finding, "breakage")),
305
+ ...(report.issues ?? [])
306
+ .filter((issue) => issue.status === "blocking" || issue.severity === "P0" || issue.severity === "P1")
307
+ .map((issue) => formatTextFinding(issue, issue.severity ?? "issue")),
308
+ ...(report.warnings ?? []).map((finding) => formatTextFinding(finding, "warning")),
309
+ ].slice(0, limit);
310
+ }
311
+
312
+ function formatTextFinding(finding, fallbackLevel) {
313
+ const level = finding.level ?? finding.severity ?? fallbackLevel;
314
+ const code = finding.code ? ` ${finding.code}` : "";
315
+ const message = finding.message ?? finding.title ?? "see report";
316
+ const evidence = Array.isArray(finding.evidence) && finding.evidence.length > 0 ? ` (${finding.evidence[0]})` : "";
317
+ return `${String(level).toUpperCase()} ${finding.fixture ?? "unknown"}${code}: ${message}${evidence}`;
318
+ }
319
+
320
+ function artifactLabel(name) {
321
+ return String(name).replace(/Path$/u, "");
270
322
  }
271
323
 
272
324
  export function renderMarkdownReport(report) {
package/src/sdk-mock.js CHANGED
@@ -11,8 +11,11 @@ export const mockSdkSubpathExports = {
11
11
  "emptyPluginConfigSchema",
12
12
  ],
13
13
  core: [
14
+ "buildChannelOutboundSessionRoute",
14
15
  "buildChannelConfigSchema",
15
16
  "buildPluginConfigSchema",
17
+ "createActionGate",
18
+ "createChannelPluginBase",
16
19
  "createChatChannelPlugin",
17
20
  "createDedupeCache",
18
21
  "defineChannelPluginEntry",
@@ -22,12 +25,24 @@ export const mockSdkSubpathExports = {
22
25
  "emptyPluginConfigSchema",
23
26
  "jsonResult",
24
27
  "readNumberParam",
28
+ "readReactionParams",
29
+ "readStringArrayParam",
30
+ "readStringParam",
31
+ ],
32
+ "channel-actions": [
33
+ "createActionGate",
34
+ "jsonResult",
35
+ "readNumberParam",
36
+ "readReactionParams",
37
+ "readStringArrayParam",
25
38
  "readStringParam",
26
39
  ],
27
40
  "channel-core": [
28
41
  "buildChannelConfigSchema",
42
+ "buildChannelOutboundSessionRoute",
29
43
  "buildThreadAwareOutboundSessionRoute",
30
44
  "clearAccountEntryFields",
45
+ "createChannelPluginBase",
31
46
  "createChatChannelPlugin",
32
47
  "defineChannelPluginEntry",
33
48
  "defineSetupPluginEntry",
@@ -718,20 +733,179 @@ function mockSdkSource() {
718
733
  return typeof entry === "function" ? { register: entry } : entry;
719
734
  }
720
735
 
736
+ function normalizeRegistrationMode(api) {
737
+ return api?.registrationMode ?? "full";
738
+ }
739
+
740
+ function isPlainObject(value) {
741
+ return value !== null && typeof value === "object" && !Array.isArray(value);
742
+ }
743
+
744
+ function parseWithSchema(schema, value) {
745
+ return schema && typeof schema.parse === "function" ? schema.parse(value) : value;
746
+ }
747
+
748
+ function createConfigSchema(schema = {}) {
749
+ if (schema && typeof schema.parse === "function") {
750
+ return schema;
751
+ }
752
+ const shape = isPlainObject(schema?.shape) ? schema.shape : isPlainObject(schema?.properties) ? schema.properties : schema;
753
+ return {
754
+ ...schema,
755
+ parse(value = {}) {
756
+ if (!isPlainObject(shape)) {
757
+ return isPlainObject(value) ? value : {};
758
+ }
759
+ const source = isPlainObject(value) ? value : {};
760
+ const output = { ...source };
761
+ for (const [key, fieldSchema] of Object.entries(shape)) {
762
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
763
+ output[key] = parseWithSchema(fieldSchema, source[key]);
764
+ }
765
+ }
766
+ return output;
767
+ },
768
+ };
769
+ }
770
+
721
771
  export function definePluginEntry(entry) {
722
772
  return normalizeEntry(entry);
723
773
  }
724
774
 
725
775
  export function defineChannelPluginEntry(entry) {
726
- return normalizeEntry(entry);
776
+ if (!isPlainObject(entry) || !entry.plugin) {
777
+ return normalizeEntry(entry);
778
+ }
779
+ const resolved = {
780
+ id: entry.id,
781
+ name: entry.name,
782
+ description: entry.description,
783
+ configSchema: createConfigSchema(entry.configSchema),
784
+ channelPlugin: entry.plugin,
785
+ register(api) {
786
+ const mode = normalizeRegistrationMode(api);
787
+ if (mode === "cli-metadata") {
788
+ entry.registerCliMetadata?.(api);
789
+ return;
790
+ }
791
+ api.registerChannel?.({ plugin: entry.plugin });
792
+ entry.setRuntime?.(api.runtime);
793
+ if (mode === "discovery") {
794
+ entry.registerCliMetadata?.(api);
795
+ return;
796
+ }
797
+ if (mode !== "full") {
798
+ return;
799
+ }
800
+ entry.registerCliMetadata?.(api);
801
+ entry.registerFull?.(api);
802
+ },
803
+ };
804
+ if (entry.setRuntime) {
805
+ resolved.setChannelRuntime = entry.setRuntime;
806
+ }
807
+ return resolved;
727
808
  }
728
809
 
729
810
  export function defineSetupPluginEntry(entry) {
730
- return normalizeEntry(entry);
811
+ return isPlainObject(entry) && entry.plugin ? entry : { plugin: entry };
731
812
  }
732
813
 
733
814
  export function createChatChannelPlugin(entry) {
734
- return normalizeEntry(entry);
815
+ if (!isPlainObject(entry) || !entry.base) {
816
+ return normalizeEntry(entry);
817
+ }
818
+ return {
819
+ ...entry.base,
820
+ conversationBindings: {
821
+ supportsCurrentConversationBinding: true,
822
+ ...(entry.base.conversationBindings ?? {}),
823
+ },
824
+ ...(entry.security ? { security: resolveChannelSecurity(entry.security) } : {}),
825
+ ...(entry.pairing ? { pairing: resolveChannelPairing(entry.pairing) } : {}),
826
+ ...(entry.threading ? { threading: resolveChannelThreading(entry.threading) } : {}),
827
+ ...(entry.outbound ? { outbound: resolveChannelOutbound(entry.outbound) } : {}),
828
+ };
829
+ }
830
+
831
+ export function createChannelPluginBase(params = {}) {
832
+ return {
833
+ id: params.id ?? "fixture-channel",
834
+ meta: { id: params.id ?? "fixture-channel", ...(params.meta ?? {}) },
835
+ ...(params.setupWizard ? { setupWizard: params.setupWizard } : {}),
836
+ ...(params.capabilities ? { capabilities: params.capabilities } : {}),
837
+ ...(params.commands ? { commands: params.commands } : {}),
838
+ ...(params.doctor ? { doctor: params.doctor } : {}),
839
+ ...(params.agentPrompt ? { agentPrompt: params.agentPrompt } : {}),
840
+ ...(params.streaming ? { streaming: params.streaming } : {}),
841
+ ...(params.reload ? { reload: params.reload } : {}),
842
+ ...(params.gatewayMethods ? { gatewayMethods: params.gatewayMethods } : {}),
843
+ ...(params.configSchema ? { configSchema: createConfigSchema(params.configSchema) } : {}),
844
+ ...(params.config ? { config: params.config } : {}),
845
+ ...(params.security ? { security: params.security } : {}),
846
+ ...(params.groups ? { groups: params.groups } : {}),
847
+ setup: params.setup ?? (() => ({})),
848
+ };
849
+ }
850
+
851
+ function resolveChannelSecurity(security) {
852
+ if (!isPlainObject(security) || !security.dm) {
853
+ return security;
854
+ }
855
+ return {
856
+ resolveDmPolicy: ({ account } = {}) => ({
857
+ policy: security.dm.resolvePolicy?.(account ?? {}) ?? security.dm.defaultPolicy ?? "allow",
858
+ allowFrom: security.dm.resolveAllowFrom?.(account ?? {}) ?? [],
859
+ }),
860
+ ...(security.collectWarnings ? { collectWarnings: security.collectWarnings } : {}),
861
+ ...(security.collectAuditFindings ? { collectAuditFindings: security.collectAuditFindings } : {}),
862
+ };
863
+ }
864
+
865
+ function resolveChannelPairing(pairing) {
866
+ if (!isPlainObject(pairing) || !pairing.text) {
867
+ return pairing;
868
+ }
869
+ return {
870
+ idLabel: pairing.text.idLabel,
871
+ normalizeAllowEntry: pairing.text.normalizeAllowEntry,
872
+ notifyApproval: (ctx) => pairing.text.notify?.({ ...ctx, message: pairing.text.message }),
873
+ };
874
+ }
875
+
876
+ function resolveChannelThreading(threading) {
877
+ if (!isPlainObject(threading)) {
878
+ return threading;
879
+ }
880
+ if (threading.resolveReplyToMode) {
881
+ return threading;
882
+ }
883
+ return {
884
+ ...threading,
885
+ resolveReplyToMode: () =>
886
+ threading.topLevelReplyToMode ??
887
+ threading.scopedAccountReplyToMode?.fallback ??
888
+ "thread",
889
+ };
890
+ }
891
+
892
+ function resolveChannelOutbound(outbound) {
893
+ if (!isPlainObject(outbound) || !outbound.attachedResults) {
894
+ return outbound;
895
+ }
896
+ const { base = {}, attachedResults } = outbound;
897
+ return {
898
+ ...base,
899
+ ...(attachedResults.sendText
900
+ ? { sendText: async (ctx) => ({ channel: attachedResults.channel, ...(await attachedResults.sendText(ctx)) }) }
901
+ : {}),
902
+ ...(attachedResults.sendMedia
903
+ ? { sendMedia: async (ctx) => ({ channel: attachedResults.channel, ...(await attachedResults.sendMedia(ctx)) }) }
904
+ : {}),
905
+ ...(attachedResults.sendPoll
906
+ ? { sendPoll: async (ctx) => ({ channel: attachedResults.channel, ...(await attachedResults.sendPoll(ctx)) }) }
907
+ : {}),
908
+ };
735
909
  }
736
910
 
737
911
  export function definePlugin(entry) {
@@ -759,13 +933,13 @@ export function defineSingleProviderPluginEntry(options) {
759
933
  }
760
934
 
761
935
  export function buildPluginConfigSchema(schema = {}) {
762
- return schema;
936
+ return createConfigSchema(schema);
763
937
  }
764
938
 
765
- export const emptyPluginConfigSchema = { type: "object", properties: {}, additionalProperties: false };
939
+ export const emptyPluginConfigSchema = createConfigSchema({ type: "object", properties: {}, additionalProperties: false });
766
940
 
767
941
  export function buildChannelConfigSchema(schema = {}) {
768
- return schema;
942
+ return createConfigSchema(schema);
769
943
  }
770
944
 
771
945
  export const emptyChannelConfigSchema = emptyPluginConfigSchema;
@@ -774,13 +948,63 @@ export function jsonResult(value) {
774
948
  return { content: [{ type: "text", text: JSON.stringify(value) }] };
775
949
  }
776
950
 
777
- export function readNumberParam(value, fallback = 0) {
778
- const parsed = Number(value);
779
- return Number.isFinite(parsed) ? parsed : fallback;
951
+ export function readNumberParam(value, keyOrFallback = 0, options = {}) {
952
+ const raw = isPlainObject(value) ? value[keyOrFallback] : value;
953
+ const parsed = Number(raw);
954
+ if (Number.isFinite(parsed)) {
955
+ return options.integer ? Math.trunc(parsed) : parsed;
956
+ }
957
+ return isPlainObject(value) ? undefined : keyOrFallback;
958
+ }
959
+
960
+ export function readStringParam(value, keyOrFallback = "") {
961
+ if (isPlainObject(value)) {
962
+ const raw = value[keyOrFallback];
963
+ return typeof raw === "string" ? raw : undefined;
964
+ }
965
+ return typeof value === "string" ? value : keyOrFallback;
780
966
  }
781
967
 
782
- export function readStringParam(value, fallback = "") {
783
- return typeof value === "string" ? value : fallback;
968
+ export function readStringArrayParam(value, key) {
969
+ const raw = isPlainObject(value) ? value[key] : value;
970
+ if (Array.isArray(raw)) {
971
+ return raw.map((entry) => String(entry));
972
+ }
973
+ return typeof raw === "string" && raw ? [raw] : [];
974
+ }
975
+
976
+ export function readReactionParams(value = {}) {
977
+ return {
978
+ messageId: value.messageId ?? value.id ?? "",
979
+ reaction: value.reaction ?? value.emoji ?? "",
980
+ };
981
+ }
982
+
983
+ export function createActionGate(actions = {}) {
984
+ return (key, defaultValue = true) => {
985
+ const value = actions?.[key];
986
+ return value === undefined ? defaultValue : value !== false;
987
+ };
988
+ }
989
+
990
+ export function buildChannelOutboundSessionRoute(params = {}) {
991
+ const peer = params.peer ?? { kind: params.chatType ?? "direct", id: params.to ?? "fixture-peer" };
992
+ const baseSessionKey = [
993
+ params.agentId ?? "agent",
994
+ params.channel ?? "channel",
995
+ params.accountId ?? "default",
996
+ peer.kind,
997
+ peer.id,
998
+ ].filter(Boolean).join(":");
999
+ return {
1000
+ sessionKey: baseSessionKey,
1001
+ baseSessionKey,
1002
+ peer,
1003
+ chatType: params.chatType ?? peer.kind ?? "direct",
1004
+ from: params.from ?? "fixture-source",
1005
+ to: params.to ?? peer.id,
1006
+ ...(params.threadId !== undefined ? { threadId: params.threadId } : {}),
1007
+ };
784
1008
  }
785
1009
 
786
1010
  export function createDedupeCache() {
@@ -951,14 +1175,28 @@ export function createAuthRateLimiter() {
951
1175
  }
952
1176
 
953
1177
  export function createProviderApiKeyAuthMethod(options = {}) {
954
- return { type: "apiKey", ...options };
1178
+ return {
1179
+ id: options.id ?? "apiKey",
1180
+ type: "apiKey",
1181
+ ...options,
1182
+ async resolve(ctx = {}) {
1183
+ return ctx.apiKey ?? ctx.key ?? ctx.token ?? null;
1184
+ },
1185
+ };
955
1186
  }
956
1187
 
957
1188
  export function buildSingleProviderApiKeyCatalog(options = {}) {
1189
+ const auth = options.auth ?? createProviderApiKeyAuthMethod(options.authOptions);
958
1190
  return {
1191
+ auth,
959
1192
  order: "simple",
960
1193
  async run(ctx) {
961
- return { provider: await options.buildProvider?.(ctx) };
1194
+ const provider = (await options.buildProvider?.(ctx)) ?? options.provider ?? { id: options.id ?? "provider", auth };
1195
+ return {
1196
+ provider,
1197
+ providers: [provider],
1198
+ models: (await options.buildModels?.(ctx)) ?? options.models ?? [],
1199
+ };
962
1200
  },
963
1201
  };
964
1202
  }
@@ -1084,7 +1322,7 @@ export function createSubsystemLogger() {
1084
1322
  }
1085
1323
 
1086
1324
  export function buildThreadAwareOutboundSessionRoute(route = {}) {
1087
- return route;
1325
+ return route.route ?? route;
1088
1326
  }
1089
1327
 
1090
1328
  export function clearAccountEntryFields(entry = {}) {
@@ -0,0 +1,19 @@
1
+ import { buildContractCapture } from "./contract-capture.js";
2
+ import { buildSyntheticProbePlan } from "./synthetic-probes.js";
3
+
4
+ export function buildSyntheticProbePlanFromReport(report, options = {}) {
5
+ const capture = options.capture ?? buildContractCapture({
6
+ report,
7
+ hookAssertions: options.hookAssertions,
8
+ hookContexts: options.hookContexts,
9
+ hookEvents: options.hookEvents,
10
+ registrationArguments: options.registrationArguments,
11
+ registrationAssertions: options.registrationAssertions,
12
+ });
13
+ return buildSyntheticProbePlan({
14
+ capture,
15
+ hookContexts: options.hookContexts,
16
+ hookEvents: options.hookEvents,
17
+ registrationArguments: options.registrationArguments,
18
+ });
19
+ }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { mkdtemp, rm } from "node:fs/promises";
2
+ import { rmSync } from "node:fs";
3
+ import { mkdtemp } from "node:fs/promises";
3
4
  import { register } from "node:module";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
@@ -59,17 +60,20 @@ async function captureForSyntheticProbes(entrypoint, options) {
59
60
  const resolvedEntrypoint = path.resolve(process.cwd(), entrypoint);
60
61
  const pluginRoot = path.resolve(process.cwd(), options.pluginRoot ?? path.dirname(resolvedEntrypoint));
61
62
  const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-synthetic-mock-sdk-"));
62
- try {
63
- const { loaderPath } = await createMockSdkPackage(workspace, { pluginRoot });
64
- register(pathToFileURL(loaderPath));
65
- return captureEntrypoint(entrypoint, {
66
- ...options,
67
- mockSdk: false,
68
- pluginRoot,
69
- });
70
- } finally {
71
- await rm(workspace, { force: true, recursive: true });
72
- }
63
+ cleanupTempDirOnExit(workspace);
64
+ const { loaderPath } = await createMockSdkPackage(workspace, { pluginRoot });
65
+ register(pathToFileURL(loaderPath));
66
+ return captureEntrypoint(entrypoint, {
67
+ ...options,
68
+ mockSdk: false,
69
+ pluginRoot,
70
+ });
71
+ }
72
+
73
+ function cleanupTempDirOnExit(dir) {
74
+ process.once("exit", () => {
75
+ rmSync(dir, { force: true, recursive: true });
76
+ });
73
77
  }
74
78
 
75
79
  function readFlag(commandArgs, name) {