@adhdev/daemon-core 0.8.52 → 0.8.54

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.
Files changed (48) hide show
  1. package/dist/commands/cli-manager.d.ts +8 -1
  2. package/dist/index.d.ts +5 -1
  3. package/dist/index.js +896 -299
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +887 -300
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/providers/acp-provider-instance.d.ts +2 -1
  8. package/dist/providers/cli-provider-instance.d.ts +3 -1
  9. package/dist/providers/cli-script-results.d.ts +8 -0
  10. package/dist/providers/contracts.d.ts +58 -9
  11. package/dist/providers/control-effects.d.ts +4 -1
  12. package/dist/providers/io-contracts.d.ts +91 -0
  13. package/dist/providers/provider-schema.d.ts +5 -0
  14. package/dist/session-host/runtime-surface.d.ts +16 -0
  15. package/dist/session-host/startup-restore-policy.d.ts +1 -0
  16. package/dist/shared-types.d.ts +6 -0
  17. package/dist/types.d.ts +3 -3
  18. package/node_modules/@adhdev/session-host-core/dist/index.d.mts +16 -1
  19. package/node_modules/@adhdev/session-host-core/dist/index.d.ts +16 -1
  20. package/node_modules/@adhdev/session-host-core/dist/index.js +59 -0
  21. package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
  22. package/node_modules/@adhdev/session-host-core/dist/index.mjs +54 -0
  23. package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
  24. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  25. package/package.json +1 -1
  26. package/src/commands/cdp-commands.ts +27 -0
  27. package/src/commands/chat-commands.ts +7 -2
  28. package/src/commands/cli-manager.ts +9 -5
  29. package/src/commands/handler.ts +1 -9
  30. package/src/commands/stream-commands.ts +26 -36
  31. package/src/daemon/dev-server.ts +4 -18
  32. package/src/index.d.ts +3 -0
  33. package/src/index.ts +12 -1
  34. package/src/providers/acp-provider-instance.ts +156 -14
  35. package/src/providers/cli-provider-instance.ts +54 -5
  36. package/src/providers/cli-script-results.ts +39 -0
  37. package/src/providers/contracts.ts +72 -19
  38. package/src/providers/control-effects.ts +86 -1
  39. package/src/providers/io-contracts.ts +340 -0
  40. package/src/providers/provider-loader.ts +18 -13
  41. package/src/providers/provider-schema.ts +154 -0
  42. package/src/session-host/runtime-surface.ts +80 -0
  43. package/src/session-host/startup-restore-policy.d.ts +1 -0
  44. package/src/session-host/startup-restore-policy.js +7 -0
  45. package/src/session-host/startup-restore-policy.ts +7 -0
  46. package/src/shared-types.ts +6 -0
  47. package/src/status/builders.ts +5 -81
  48. package/src/types.ts +3 -3
@@ -33,7 +33,22 @@ export interface ReadChatResult {
33
33
  }
34
34
 
35
35
  import type { ChatMessage } from '../types.js';
36
- export type { ChatMessage };
36
+ import {
37
+ flattenMessageParts,
38
+ normalizeInputEnvelope,
39
+ normalizeMessageParts,
40
+ } from './io-contracts.js';
41
+ export {
42
+ flattenMessageParts,
43
+ normalizeInputEnvelope,
44
+ normalizeMessageParts,
45
+ } from './io-contracts.js';
46
+ import type {
47
+ InputEnvelope,
48
+ InputPart,
49
+ MessagePart,
50
+ } from './io-contracts.js';
51
+ export type { ChatMessage, InputEnvelope, InputPart, MessagePart };
37
52
 
38
53
  export type AgentStatus =
39
54
  | 'idle'
@@ -52,7 +67,7 @@ export interface ModalInfo {
52
67
 
53
68
  export interface ProviderEffectMessage {
54
69
  role?: 'system' | 'assistant' | 'user';
55
- content: string | ContentBlock[];
70
+ content: string | MessagePart[];
56
71
  kind?: string;
57
72
  senderName?: string;
58
73
  }
@@ -71,7 +86,7 @@ export interface ProviderEffectNotification {
71
86
  level?: 'info' | 'success' | 'warning';
72
87
  channels?: ProviderNotificationChannel[];
73
88
  preferenceKey?: ProviderNotificationPreferenceKey;
74
- bubbleContent?: string | ContentBlock[];
89
+ bubbleContent?: string | MessagePart[];
75
90
  }
76
91
 
77
92
  export interface ProviderEffect {
@@ -87,9 +102,9 @@ export interface ProviderEffect {
87
102
  notification?: ProviderEffectNotification;
88
103
  }
89
104
 
90
- // ─── Rich Content Types (ACP Standard) ─────────────────
105
+ // ─── Legacy ACP ContentBlock Types (compatibility adapter) ─────────────────
91
106
  // Based on ACP SDK v0.16.1 schema types.
92
- // All provider categories (ACP, IDE, Extension, CLI) use these as output standard.
107
+ // Internal runtime code should prefer MessagePart/InputEnvelope from io-contracts.ts.
93
108
 
94
109
  /**
95
110
  * ContentBlock — ACP ContentBlock union type
@@ -192,29 +207,25 @@ export interface ToolCallLocation {
192
207
 
193
208
  // ─── Content Helpers ────────────────────────────────────
194
209
 
195
- /** Normalize content: string ContentBlock[] */
196
- export function normalizeContent(content: string | ContentBlock[]): ContentBlock[] {
197
- if (typeof content === 'string') {
198
- return [{ type: 'text', text: content }];
199
- }
200
- return content;
210
+ /** Normalize content into canonical message parts */
211
+ export function normalizeContent(content: string | MessagePart[] | ContentBlock[]): MessagePart[] {
212
+ return normalizeMessageParts(content);
201
213
  }
202
214
 
203
- /** Flatten ContentBlock[] string (backward compat / plain-text extraction) */
204
- export function flattenContent(content: string | ContentBlock[]): string {
215
+ /** Flatten canonical/legacy content into a plain-text fallback string */
216
+ export function flattenContent(content: string | MessagePart[] | ContentBlock[]): string {
205
217
  if (typeof content === 'string') return content;
206
- return content
207
- .filter((b): b is TextBlock => b.type === 'text')
208
- .map(b => b.text)
209
- .join('\n');
218
+ return flattenMessageParts(normalizeMessageParts(content));
210
219
  }
211
220
 
212
- /** SendMessage params — supports rich content (ACP PromptRequest compatible) */
221
+ /** SendMessage params — canonical input envelope with legacy text/prompt compatibility */
213
222
  export interface SendMessageParams {
214
223
  /** Shortcut: text-only message */
215
224
  text?: string;
216
- /** Rich content blocks (ACP ContentBlock[]) */
225
+ /** Rich content blocks (legacy ACP ContentBlock[]) */
217
226
  prompt?: ContentBlock[];
227
+ /** Canonical multipart runtime input */
228
+ input?: InputEnvelope;
218
229
  }
219
230
 
220
231
  // ─── sendMessage() return value ────────────────────────
@@ -332,6 +343,12 @@ export interface ProviderModule {
332
343
  icon?: string;
333
344
  /** Display name (short name) */
334
345
  displayName?: string;
346
+ /** Provider-definition version maintained in adhdev-providers */
347
+ providerVersion?: string;
348
+ /** Inventory/support status label maintained in adhdev-providers */
349
+ status?: string;
350
+ /** Inventory/support detail string maintained in adhdev-providers */
351
+ details?: string;
335
352
  /** Install instructions (shown when command is missing) */
336
353
  install?: string;
337
354
  /** Custom version detection command (e.g. 'cursor --version', 'claude -v') */
@@ -390,6 +407,14 @@ export interface ProviderModule {
390
407
  shell?: boolean;
391
408
  env?: Record<string, string>;
392
409
  };
410
+ /** Delay before submitting typed CLI input (provider-specific TUI tuning) */
411
+ sendDelayMs?: number;
412
+ /** Submit key used after typing into CLI PTY (default: carriage return) */
413
+ sendKey?: string;
414
+ /** How the CLI adapter decides when to submit typed input */
415
+ submitStrategy?: 'wait_for_echo' | 'immediate';
416
+ /** Keep this provider out of the upstream auto-updated bundle */
417
+ disableUpstream?: boolean;
393
418
  approvalKeys?: Record<number, string>;
394
419
  patterns?: {
395
420
  prompt?: RegExp[];
@@ -469,6 +494,14 @@ export interface ProviderModule {
469
494
  // ─── ACP Authentication (auth method definitions) ───
470
495
  /** ACP agent auth methods (multiple supported — in priority order) */
471
496
  auth?: AcpAuthMethod[];
497
+
498
+ // ─── Contract version / capability declaration ───
499
+ contractVersion?: number;
500
+ capabilities?: {
501
+ input?: { multipart?: boolean; mediaTypes?: Array<'text' | 'image' | 'audio' | 'video' | 'resource'> };
502
+ output?: { richContent?: boolean; mediaTypes?: Array<'text' | 'image' | 'audio' | 'video' | 'resource'> };
503
+ controls?: { typedResults?: boolean };
504
+ };
472
505
  }
473
506
 
474
507
  export interface ProviderResumeCapability {
@@ -674,6 +707,26 @@ export interface ProviderControlOption {
674
707
  group?: string;
675
708
  }
676
709
 
710
+ export interface ControlListResult {
711
+ options: ProviderControlOption[];
712
+ currentValue?: string | number | boolean;
713
+ error?: string;
714
+ }
715
+
716
+ export interface ControlSetResult {
717
+ ok: boolean;
718
+ currentValue?: string | number | boolean;
719
+ effects?: ProviderEffect[];
720
+ error?: string;
721
+ }
722
+
723
+ export interface ControlInvokeResult {
724
+ ok: boolean;
725
+ currentValue?: string | number | boolean;
726
+ effects?: ProviderEffect[];
727
+ error?: string;
728
+ }
729
+
677
730
  /**
678
731
  * ProviderControlDef — A single interactive control declared by a provider.
679
732
  *
@@ -1,4 +1,11 @@
1
- import type { ProviderControlDef, ProviderEffect } from './contracts.js';
1
+ import type {
2
+ ControlInvokeResult,
3
+ ControlListResult,
4
+ ControlSetResult,
5
+ ProviderControlDef,
6
+ ProviderControlOption,
7
+ ProviderEffect,
8
+ } from './contracts.js';
2
9
 
3
10
  export type ProviderControlValue = string | number | boolean;
4
11
 
@@ -101,6 +108,84 @@ export function normalizeProviderEffects(data: any): ProviderEffect[] {
101
108
  return effects;
102
109
  }
103
110
 
111
+ export function normalizeControlListResult(data: any): ControlListResult {
112
+ if (data && typeof data === 'object' && Array.isArray(data.options)) {
113
+ return {
114
+ options: normalizeControlOptions(data.options),
115
+ ...(isScalarControlValue(data.currentValue) ? { currentValue: data.currentValue } : {}),
116
+ ...(typeof data.error === 'string' ? { error: data.error } : {}),
117
+ };
118
+ }
119
+
120
+ const rawOptions = Array.isArray(data?.models)
121
+ ? data.models
122
+ : Array.isArray(data?.modes)
123
+ ? data.modes
124
+ : Array.isArray(data?.options)
125
+ ? data.options
126
+ : [];
127
+ const options = normalizeControlOptions(rawOptions);
128
+ return {
129
+ options,
130
+ ...(isScalarControlValue(data?.current) ? { currentValue: data.current } : {}),
131
+ ...(isScalarControlValue(data?.currentValue) ? { currentValue: data.currentValue } : {}),
132
+ ...(typeof data?.error === 'string' ? { error: data.error } : {}),
133
+ };
134
+ }
135
+
136
+ export function normalizeControlSetResult(data: any): ControlSetResult {
137
+ const currentValue = isScalarControlValue(data?.currentValue)
138
+ ? data.currentValue
139
+ : (isScalarControlValue(data?.value) ? data.value : undefined);
140
+ return {
141
+ ok: data?.ok === true || data?.success === true,
142
+ ...(currentValue !== undefined ? { currentValue } : {}),
143
+ ...(Array.isArray(data?.effects) ? { effects: normalizeProviderEffects(data) } : {}),
144
+ ...(typeof data?.error === 'string' ? { error: data.error } : {}),
145
+ };
146
+ }
147
+
148
+ export function normalizeControlInvokeResult(data: any): ControlInvokeResult {
149
+ const currentValue = isScalarControlValue(data?.currentValue)
150
+ ? data.currentValue
151
+ : (isScalarControlValue(data?.value) ? data.value : undefined);
152
+ return {
153
+ ok: data?.ok === true || data?.success === true,
154
+ ...(currentValue !== undefined ? { currentValue } : {}),
155
+ ...(Array.isArray(data?.effects) ? { effects: normalizeProviderEffects(data) } : {}),
156
+ ...(typeof data?.error === 'string' ? { error: data.error } : {}),
157
+ };
158
+ }
159
+
160
+ function normalizeControlOptions(options: unknown[]): ProviderControlOption[] {
161
+ return options
162
+ .map((option) => normalizeControlOption(option))
163
+ .filter((option): option is ProviderControlOption => !!option);
164
+ }
165
+
166
+ function normalizeControlOption(option: unknown): ProviderControlOption | null {
167
+ if (typeof option === 'string') {
168
+ return { value: option, label: option };
169
+ }
170
+ if (!option || typeof option !== 'object') return null;
171
+ const record = option as Record<string, unknown>;
172
+ const value = typeof record.value === 'string'
173
+ ? record.value
174
+ : (typeof record.id === 'string' ? record.id : null);
175
+ if (!value) return null;
176
+ const label = typeof record.label === 'string'
177
+ ? record.label
178
+ : (typeof record.name === 'string' ? record.name : value);
179
+ const normalized: ProviderControlOption = { value, label };
180
+ if (typeof record.description === 'string') normalized.description = record.description;
181
+ if (typeof record.group === 'string') normalized.group = record.group;
182
+ return normalized;
183
+ }
184
+
185
+ function isScalarControlValue(value: unknown): value is ProviderControlValue {
186
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
187
+ }
188
+
104
189
  function normalizeControlValue(value: any): ProviderControlValue {
105
190
  if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
106
191
  return value;
@@ -0,0 +1,340 @@
1
+ import type { ContentAnnotations } from './contracts.js'
2
+
3
+ export type InputPart =
4
+ | TextInputPart
5
+ | ImageInputPart
6
+ | AudioInputPart
7
+ | VideoInputPart
8
+ | ResourceInputPart
9
+
10
+ export interface TextInputPart {
11
+ type: 'text'
12
+ text: string
13
+ }
14
+
15
+ export interface ImageInputPart {
16
+ type: 'image'
17
+ mimeType: string
18
+ uri?: string
19
+ data?: string
20
+ alt?: string
21
+ }
22
+
23
+ export interface AudioInputPart {
24
+ type: 'audio'
25
+ mimeType: string
26
+ uri?: string
27
+ data?: string
28
+ transcript?: string
29
+ }
30
+
31
+ export interface VideoInputPart {
32
+ type: 'video'
33
+ mimeType: string
34
+ uri?: string
35
+ data?: string
36
+ posterUri?: string
37
+ }
38
+
39
+ export interface ResourceInputPart {
40
+ type: 'resource'
41
+ uri: string
42
+ mimeType?: string
43
+ name?: string
44
+ text?: string
45
+ data?: string
46
+ }
47
+
48
+ export interface InputEnvelope {
49
+ parts: InputPart[]
50
+ textFallback: string
51
+ metadata?: {
52
+ source?: 'dashboard' | 'shortcut_api' | 'provider_script' | 'session_replay'
53
+ clientTimestamp?: number
54
+ }
55
+ }
56
+
57
+ export type MessagePart =
58
+ | TextMessagePart
59
+ | ImageMessagePart
60
+ | AudioMessagePart
61
+ | VideoMessagePart
62
+ | ResourceLinkMessagePart
63
+ | ResourceMessagePart
64
+
65
+ export interface TextMessagePart {
66
+ type: 'text'
67
+ text: string
68
+ annotations?: ContentAnnotations
69
+ }
70
+
71
+ export interface ImageMessagePart {
72
+ type: 'image'
73
+ mimeType: string
74
+ uri?: string
75
+ data?: string
76
+ annotations?: ContentAnnotations
77
+ }
78
+
79
+ export interface AudioMessagePart {
80
+ type: 'audio'
81
+ mimeType: string
82
+ uri?: string
83
+ data?: string
84
+ transcript?: string
85
+ annotations?: ContentAnnotations
86
+ }
87
+
88
+ export interface VideoMessagePart {
89
+ type: 'video'
90
+ mimeType: string
91
+ uri?: string
92
+ data?: string
93
+ posterUri?: string
94
+ annotations?: ContentAnnotations
95
+ }
96
+
97
+ export interface ResourceLinkMessagePart {
98
+ type: 'resource_link'
99
+ uri: string
100
+ name: string
101
+ mimeType?: string
102
+ size?: number
103
+ }
104
+
105
+ export interface ResourceMessagePart {
106
+ type: 'resource'
107
+ resource: {
108
+ uri: string
109
+ mimeType?: string | null
110
+ text?: string
111
+ blob?: string
112
+ }
113
+ }
114
+
115
+ export function normalizeInputEnvelope(input: unknown): InputEnvelope {
116
+ const normalized = normalizeInputEnvelopePayload(input)
117
+ const textFallback = normalized.textFallback ?? flattenInputParts(normalized.parts)
118
+ return {
119
+ parts: normalized.parts,
120
+ textFallback,
121
+ ...(normalized.metadata ? { metadata: normalized.metadata } : {}),
122
+ }
123
+ }
124
+
125
+ export function normalizeMessageParts(content: unknown): MessagePart[] {
126
+ if (typeof content === 'string') return [{ type: 'text', text: content }]
127
+ if (!Array.isArray(content)) {
128
+ if (content && typeof content === 'object' && typeof (content as { text?: unknown }).text === 'string') {
129
+ return [{ type: 'text', text: String((content as { text: string }).text) }]
130
+ }
131
+ return []
132
+ }
133
+
134
+ const parts: MessagePart[] = []
135
+ for (const raw of content) {
136
+ if (typeof raw === 'string') {
137
+ parts.push({ type: 'text', text: raw })
138
+ continue
139
+ }
140
+ if (!raw || typeof raw !== 'object') continue
141
+ const part = normalizeMessagePartObject(raw)
142
+ if (part) parts.push(part)
143
+ }
144
+ return parts
145
+ }
146
+
147
+ export function flattenMessageParts(parts: MessagePart[]): string {
148
+ return parts
149
+ .map((part) => {
150
+ if (part.type === 'text') return part.text
151
+ if (part.type === 'resource') return part.resource.text || ''
152
+ return ''
153
+ })
154
+ .filter((value) => value.length > 0)
155
+ .join('\n')
156
+ }
157
+
158
+ function normalizeInputEnvelopePayload(input: unknown): Omit<InputEnvelope, 'textFallback'> & { textFallback?: string } {
159
+ if (typeof input === 'string') {
160
+ return { parts: [{ type: 'text', text: input }], textFallback: input }
161
+ }
162
+
163
+ if (!input || typeof input !== 'object') {
164
+ return { parts: [], textFallback: '' }
165
+ }
166
+
167
+ const record = input as Record<string, unknown>
168
+ const nestedInput = record.input
169
+ if (nestedInput && typeof nestedInput === 'object') {
170
+ const nested = nestedInput as Record<string, unknown>
171
+ return {
172
+ parts: normalizeInputParts(nested.parts ?? nested.prompt),
173
+ textFallback: typeof nested.textFallback === 'string' ? nested.textFallback : undefined,
174
+ metadata: normalizeInputMetadata(nested.metadata),
175
+ }
176
+ }
177
+
178
+ const directText = typeof record.text === 'string'
179
+ ? record.text
180
+ : (typeof record.message === 'string' ? record.message : undefined)
181
+ if (directText !== undefined) {
182
+ return { parts: [{ type: 'text', text: directText }], textFallback: directText }
183
+ }
184
+
185
+ const directParts = normalizeInputParts(record.parts ?? record.prompt)
186
+ return {
187
+ parts: directParts,
188
+ textFallback: typeof record.textFallback === 'string' ? record.textFallback : undefined,
189
+ metadata: normalizeInputMetadata(record.metadata),
190
+ }
191
+ }
192
+
193
+ function normalizeInputMetadata(value: unknown): InputEnvelope['metadata'] | undefined {
194
+ if (!value || typeof value !== 'object') return undefined
195
+ const record = value as Record<string, unknown>
196
+ const metadata: InputEnvelope['metadata'] = {}
197
+ if (record.source === 'dashboard' || record.source === 'shortcut_api' || record.source === 'provider_script' || record.source === 'session_replay') {
198
+ metadata.source = record.source
199
+ }
200
+ if (typeof record.clientTimestamp === 'number' && Number.isFinite(record.clientTimestamp)) {
201
+ metadata.clientTimestamp = record.clientTimestamp
202
+ }
203
+ return Object.keys(metadata).length > 0 ? metadata : undefined
204
+ }
205
+
206
+ function normalizeInputParts(value: unknown): InputPart[] {
207
+ if (!Array.isArray(value)) return []
208
+ const parts: InputPart[] = []
209
+ for (const raw of value) {
210
+ if (typeof raw === 'string') {
211
+ parts.push({ type: 'text', text: raw })
212
+ continue
213
+ }
214
+ if (!raw || typeof raw !== 'object') continue
215
+ const part = normalizeInputPartObject(raw)
216
+ if (part) parts.push(part)
217
+ }
218
+ return parts
219
+ }
220
+
221
+ function normalizeInputPartObject(raw: Record<string, unknown>): InputPart | null {
222
+ const type = raw.type
223
+ if (type === 'text' && typeof raw.text === 'string') {
224
+ return { type, text: raw.text }
225
+ }
226
+ if (type === 'image' && typeof raw.mimeType === 'string') {
227
+ return {
228
+ type,
229
+ mimeType: raw.mimeType,
230
+ ...(typeof raw.uri === 'string' ? { uri: raw.uri } : {}),
231
+ ...(typeof raw.data === 'string' ? { data: raw.data } : {}),
232
+ ...(typeof raw.alt === 'string' ? { alt: raw.alt } : {}),
233
+ }
234
+ }
235
+ if (type === 'audio' && typeof raw.mimeType === 'string') {
236
+ return {
237
+ type,
238
+ mimeType: raw.mimeType,
239
+ ...(typeof raw.uri === 'string' ? { uri: raw.uri } : {}),
240
+ ...(typeof raw.data === 'string' ? { data: raw.data } : {}),
241
+ ...(typeof raw.transcript === 'string' ? { transcript: raw.transcript } : {}),
242
+ }
243
+ }
244
+ if (type === 'video' && typeof raw.mimeType === 'string') {
245
+ return {
246
+ type,
247
+ mimeType: raw.mimeType,
248
+ ...(typeof raw.uri === 'string' ? { uri: raw.uri } : {}),
249
+ ...(typeof raw.data === 'string' ? { data: raw.data } : {}),
250
+ ...(typeof raw.posterUri === 'string' ? { posterUri: raw.posterUri } : {}),
251
+ }
252
+ }
253
+ if (type === 'resource' && typeof raw.uri === 'string') {
254
+ return {
255
+ type,
256
+ uri: raw.uri,
257
+ ...(typeof raw.mimeType === 'string' ? { mimeType: raw.mimeType } : {}),
258
+ ...(typeof raw.name === 'string' ? { name: raw.name } : {}),
259
+ ...(typeof raw.text === 'string' ? { text: raw.text } : {}),
260
+ ...(typeof raw.data === 'string' ? { data: raw.data } : {}),
261
+ }
262
+ }
263
+ if (type === 'resource_link' && typeof raw.uri === 'string') {
264
+ return {
265
+ type: 'resource',
266
+ uri: raw.uri,
267
+ ...(typeof raw.mimeType === 'string' ? { mimeType: raw.mimeType } : {}),
268
+ ...(typeof raw.name === 'string' ? { name: raw.name } : {}),
269
+ }
270
+ }
271
+ return null
272
+ }
273
+
274
+ function normalizeMessagePartObject(raw: Record<string, unknown>): MessagePart | null {
275
+ const type = raw.type
276
+ if (type === 'text' && typeof raw.text === 'string') {
277
+ return { type, text: raw.text }
278
+ }
279
+ if (type === 'image' && typeof raw.mimeType === 'string') {
280
+ return {
281
+ type,
282
+ mimeType: raw.mimeType,
283
+ ...(typeof raw.uri === 'string' ? { uri: raw.uri } : {}),
284
+ ...(typeof raw.data === 'string' ? { data: raw.data } : {}),
285
+ }
286
+ }
287
+ if (type === 'audio' && typeof raw.mimeType === 'string') {
288
+ return {
289
+ type,
290
+ mimeType: raw.mimeType,
291
+ ...(typeof raw.uri === 'string' ? { uri: raw.uri } : {}),
292
+ ...(typeof raw.data === 'string' ? { data: raw.data } : {}),
293
+ ...(typeof raw.transcript === 'string' ? { transcript: raw.transcript } : {}),
294
+ }
295
+ }
296
+ if (type === 'video' && typeof raw.mimeType === 'string') {
297
+ return {
298
+ type,
299
+ mimeType: raw.mimeType,
300
+ ...(typeof raw.uri === 'string' ? { uri: raw.uri } : {}),
301
+ ...(typeof raw.data === 'string' ? { data: raw.data } : {}),
302
+ ...(typeof raw.posterUri === 'string' ? { posterUri: raw.posterUri } : {}),
303
+ }
304
+ }
305
+ if (type === 'resource_link' && typeof raw.uri === 'string' && typeof raw.name === 'string') {
306
+ return {
307
+ type,
308
+ uri: raw.uri,
309
+ name: raw.name,
310
+ ...(typeof raw.mimeType === 'string' ? { mimeType: raw.mimeType } : {}),
311
+ ...(typeof raw.size === 'number' ? { size: raw.size } : {}),
312
+ }
313
+ }
314
+ if (type === 'resource' && raw.resource && typeof raw.resource === 'object') {
315
+ const resource = raw.resource as Record<string, unknown>
316
+ if (typeof resource.uri !== 'string') return null
317
+ return {
318
+ type,
319
+ resource: {
320
+ uri: resource.uri,
321
+ ...(typeof resource.mimeType === 'string' || resource.mimeType === null ? { mimeType: resource.mimeType as string | null } : {}),
322
+ ...(typeof resource.text === 'string' ? { text: resource.text } : {}),
323
+ ...(typeof resource.blob === 'string' ? { blob: resource.blob } : {}),
324
+ },
325
+ }
326
+ }
327
+ return null
328
+ }
329
+
330
+ function flattenInputParts(parts: InputPart[]): string {
331
+ return parts
332
+ .map((part) => {
333
+ if (part.type === 'text') return part.text
334
+ if (part.type === 'audio') return part.transcript || ''
335
+ if (part.type === 'resource') return part.text || ''
336
+ return ''
337
+ })
338
+ .filter((value) => value.length > 0)
339
+ .join('\n')
340
+ }
@@ -29,6 +29,7 @@ import type {
29
29
  ProviderSettingSchema,
30
30
  ResolvedProvider,
31
31
  } from './contracts.js';
32
+ import { validateProviderDefinition } from './provider-schema.js';
32
33
 
33
34
  interface ProviderAvailabilityState {
34
35
  installed: boolean;
@@ -1288,20 +1289,24 @@ export class ProviderLoader {
1288
1289
  extensionIdPattern?: RegExp | string;
1289
1290
  };
1290
1291
 
1291
- if (!mod.type || !mod.name || !mod.category) {
1292
- this.log(`⚠ Invalid provider at ${jsonPath}: missing type/name/category`);
1293
- } else {
1294
- // Restore RegExp fields from JSON (extensionIdPattern)
1295
- if (typeof mod.extensionIdPattern === 'string') {
1296
- const flags = mod.extensionIdPattern_flags || '';
1297
- mod.extensionIdPattern = new RegExp(mod.extensionIdPattern, flags);
1298
- }
1299
- const { extensionIdPattern_flags, extensionIdPattern, ...providerFields } = mod;
1300
- const normalizedProvider: ProviderModule = {
1301
- ...providerFields,
1302
- ...(extensionIdPattern instanceof RegExp ? { extensionIdPattern } : {}),
1303
- };
1292
+ // Restore RegExp fields from JSON (extensionIdPattern)
1293
+ if (typeof mod.extensionIdPattern === 'string') {
1294
+ const flags = mod.extensionIdPattern_flags || '';
1295
+ mod.extensionIdPattern = new RegExp(mod.extensionIdPattern, flags);
1296
+ }
1297
+ const { extensionIdPattern_flags, extensionIdPattern, ...providerFields } = mod;
1298
+ const normalizedProvider: ProviderModule = {
1299
+ ...providerFields,
1300
+ ...(extensionIdPattern instanceof RegExp ? { extensionIdPattern } : {}),
1301
+ };
1304
1302
 
1303
+ const validation = validateProviderDefinition(normalizedProvider);
1304
+ for (const warning of validation.warnings) {
1305
+ this.log(`⚠ ${jsonPath}: ${warning}`);
1306
+ }
1307
+ if (validation.errors.length > 0) {
1308
+ this.log(`⚠ Invalid provider at ${jsonPath}: ${validation.errors.join('; ')}`);
1309
+ } else {
1305
1310
  // Load scripts.js if exists (IDE/Extension)
1306
1311
  // Skip for compatibility-format providers — scripts loaded lazily in resolve()
1307
1312
  const hasCompatibility = Array.isArray(normalizedProvider.compatibility);