@jini-ai/daemon 0.6.8 → 0.6.10

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 (79) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/dist/agent-executor/antigravity-permissions.d.ts +11 -0
  3. package/dist/agent-executor/antigravity-permissions.js +64 -0
  4. package/dist/agent-executor/contracts.d.ts +37 -6
  5. package/dist/agent-executor/event-translation.d.ts +6 -1
  6. package/dist/agent-executor/event-translation.js +3 -1
  7. package/dist/agent-executor/events.d.ts +5 -0
  8. package/dist/agent-executor/events.js +143 -87
  9. package/dist/agent-executor/launch.d.ts +5 -3
  10. package/dist/agent-executor/launch.js +315 -217
  11. package/dist/agent-executor/mcp-bridge-probe.js +51 -35
  12. package/dist/agent-executor/mcp-bridge.d.ts +18 -8
  13. package/dist/agent-executor/mcp-bridge.js +105 -65
  14. package/dist/agent-executor/run-output.d.ts +20 -0
  15. package/dist/agent-executor/run-output.js +80 -0
  16. package/dist/attachment-content.js +18 -15
  17. package/dist/core/read-only-tools.d.ts +15 -0
  18. package/dist/core/read-only-tools.js +29 -1
  19. package/dist/core/tool-audit.d.ts +3 -1
  20. package/dist/core/tool-audit.js +35 -22
  21. package/dist/core/tool-gaps.d.ts +60 -0
  22. package/dist/core/tool-gaps.js +199 -0
  23. package/dist/delegated-result-correlation.d.ts +26 -0
  24. package/dist/delegated-result-correlation.js +68 -0
  25. package/dist/delegated-tool-bridge.d.ts +6 -14
  26. package/dist/delegated-tool-bridge.js +155 -155
  27. package/dist/event-log.d.ts +45 -2
  28. package/dist/event-log.js +83 -4
  29. package/dist/http/attachments.d.ts +26 -1
  30. package/dist/http/attachments.js +129 -65
  31. package/dist/http/connectors.d.ts +1 -32
  32. package/dist/http/connectors.js +13 -68
  33. package/dist/http/delegated-tools.d.ts +8 -1
  34. package/dist/http/delegated-tools.js +30 -30
  35. package/dist/http/host-tools.d.ts +1 -0
  36. package/dist/http/host-tools.js +64 -59
  37. package/dist/http/index.d.ts +2 -2
  38. package/dist/http/index.js +1 -1
  39. package/dist/http/media.js +48 -69
  40. package/dist/http/memory.js +45 -36
  41. package/dist/http/model-proxy-fields.d.ts +23 -0
  42. package/dist/http/model-proxy-fields.js +29 -0
  43. package/dist/http/model-proxy.js +193 -395
  44. package/dist/http/research-tavily.adapter.js +72 -49
  45. package/dist/http/research.js +33 -23
  46. package/dist/http/resource-request.d.ts +37 -0
  47. package/dist/http/resource-request.js +59 -0
  48. package/dist/http/routines.js +49 -90
  49. package/dist/http/runs.d.ts +7 -5
  50. package/dist/http/runs.js +21 -7
  51. package/dist/http/terminals.js +23 -48
  52. package/dist/http/tool-catalog.d.ts +5 -0
  53. package/dist/http/xai.d.ts +11 -1
  54. package/dist/http/xai.js +125 -124
  55. package/dist/index.d.ts +2 -0
  56. package/dist/index.js +1 -0
  57. package/dist/legacy-data-migration.js +41 -24
  58. package/dist/run/core/retry.js +51 -35
  59. package/dist/run/diagnostics/diagnostics.js +5 -17
  60. package/dist/run-credentials.js +49 -27
  61. package/dist/run-lifecycle.d.ts +31 -2
  62. package/dist/run-lifecycle.js +49 -42
  63. package/dist/store/agent-sessions/sqlite.d.ts +6 -0
  64. package/dist/store/agent-sessions/sqlite.js +6 -4
  65. package/dist/surface-exchanges.js +2 -1
  66. package/dist/terminal-session.d.ts +1 -1
  67. package/dist/terminal-session.js +26 -33
  68. package/dist/tool-audit.d.ts +1 -0
  69. package/dist/tool-audit.js +1 -0
  70. package/dist/tool-executor.d.ts +24 -3
  71. package/dist/tool-executor.js +276 -139
  72. package/dist/tool-recovery/recovery.js +64 -35
  73. package/dist/tool-recovery/stack.d.ts +2 -1
  74. package/dist/tool-recovery/stack.js +1 -1
  75. package/dist/tool-result-diagnostics.d.ts +27 -0
  76. package/dist/tool-result-diagnostics.js +77 -0
  77. package/dist/tool-result-media.d.ts +27 -14
  78. package/dist/tool-result-media.js +51 -38
  79. package/package.json +14 -14
@@ -27,11 +27,9 @@
27
27
  */
28
28
  import { randomUUID } from 'node:crypto';
29
29
  import { createApiError } from '@jini-ai/protocol';
30
- import { ToolInputError } from '@jini-ai/core';
30
+ import { batchOutcomeFor, ToolInputError } from '@jini-ai/core';
31
31
  import { createDelegatedToolBridge } from '../index.js';
32
- import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
33
- import { validationError } from '@jini-ai/http-kit';
34
- import { err, ok } from '@jini-ai/http-kit';
32
+ import { defineJsonRoute, err, mountJsonRoute, ok, validationError } from '@jini-ai/http-kit';
35
33
  import * as readOnlyTools from '../core/read-only-tools.js';
36
34
  /** Route preflight shares the same registration decision as nested dispatch. */
37
35
  function checkReadOnlyConstraint(deps, input) {
@@ -68,20 +66,14 @@ function describeSafely(deps, context) {
68
66
  return undefined;
69
67
  }
70
68
  }
71
- function reportInternalError(deps, source, error, runId, toolId, status) {
69
+ function reportInternalError(deps, report) {
72
70
  const correlationId = randomUUID();
73
- const context = {
74
- source,
75
- runId,
76
- toolId,
77
- correlationId,
78
- error,
79
- ...(status === undefined ? {} : { status }),
80
- };
71
+ // Every caller either omits `status` or sets it, so spreading keeps "absent" absent.
72
+ const context = { ...report, correlationId };
81
73
  const sink = deps.onInternalError ?? defaultInternalErrorSink;
82
74
  sink(context);
83
75
  const message = describeSafely(deps, context) ?? 'an internal error occurred';
84
- return createApiError({ code: 'INTERNAL_ERROR', message: message }, { requestId: correlationId });
76
+ return createApiError({ code: 'INTERNAL_ERROR', message }, { requestId: correlationId });
85
77
  }
86
78
  function isRecord(value) {
87
79
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -118,7 +110,8 @@ function parseDelegatedToolExecute(input) {
118
110
  /**
119
111
  * Maps a settled `ToolExecutionResult` to the wire `Result` — the same status mapping
120
112
  * `db-ops.ts`'s `toolResultToApiResult` establishes for this identical `ToolExecutionResult`
121
- * union, kept consistent here rather than reinvented: `completed` → `200 {result}`,
113
+ * union: `completed` → `200 {result}`; a batch with failed items carries
114
+ * `partial_failure` or `error` plus its per-item output and isError. Other states map as follows:
122
115
  * `denied`/`confirmation-denied` → `403 TOOL_OPERATION_DENIED`, `timed-out`/`cancelled` → a
123
116
  * SEC-005-redacted `500 INTERNAL_ERROR` (the real status/error goes to `onInternalError`, never the
124
117
  * wire — unless the host supplies `deps.describeInternalError`, whose text then replaces the generic
@@ -135,26 +128,33 @@ function parseDelegatedToolExecute(input) {
135
128
  * vouches for this exact result via `deps.isModelSafeToolFailure` (it has already redacted the text
136
129
  * itself), in which case it is a `422 TOOL_EXECUTION_FAILED` carrying the host's text verbatim.
137
130
  */
138
- function toolExecutionResultToApiResult(deps, runId, toolId, result) {
131
+ function toolExecutionResultToApiResult(deps, { runId, toolId, result }) {
139
132
  switch (result.status) {
140
- case 'completed':
141
- return ok({ value: { result } });
133
+ case 'completed': {
134
+ const outcome = batchOutcomeFor({ output: result.output });
135
+ return ok({ value: { result: outcome === undefined || outcome === 'completed' ? result
136
+ : { ...result, status: outcome, outcome, isError: true } } });
137
+ }
142
138
  case 'denied':
143
139
  return err({ error: createApiError({ code: 'TOOL_OPERATION_DENIED', message: 'this operation was denied by policy' }) });
144
140
  case 'confirmation-denied':
145
141
  return err({ error: createApiError({ code: 'TOOL_OPERATION_DENIED', message: 'this operation was denied during confirmation' }) });
146
142
  case 'timed-out':
147
143
  case 'cancelled':
148
- return err({ error: reportInternalError(deps, 'delegated-tool-execute', result.status, runId, toolId, result.status) });
144
+ return err({ error: reportInternalError(deps, { source: 'delegated-tool-execute', error: result.status, runId, toolId, status: result.status }) });
149
145
  case 'failed':
150
- if (result.errorKind === 'validation') {
151
- return err({ error: createApiError({ code: 'BAD_REQUEST', message: result.error ?? 'invalid tool input' }) });
152
- }
153
- if (result.error && deps.isModelSafeToolFailure?.({ result }) === true) {
154
- return err({ error: createApiError({ code: 'TOOL_EXECUTION_FAILED', message: result.error }) });
155
- }
156
- return err({ error: reportInternalError(deps, 'delegated-tool-execute', result.error ?? result.status, runId, toolId, 'failed') });
146
+ return err({ error: failedResultToApiError(deps, { runId, toolId, result }) });
147
+ }
148
+ }
149
+ /** The `failed` arm of {@link toolExecutionResultToApiResult} — see its doc for the three-way split. */
150
+ function failedResultToApiError(deps, { runId, toolId, result }) {
151
+ if (result.errorKind === 'validation') {
152
+ return createApiError({ code: 'BAD_REQUEST', message: result.error ?? 'invalid tool input' });
153
+ }
154
+ if (result.error && deps.isModelSafeToolFailure?.({ result }) === true) {
155
+ return createApiError({ code: 'TOOL_EXECUTION_FAILED', message: result.error });
157
156
  }
157
+ return reportInternalError(deps, { source: 'delegated-tool-execute', error: result.error ?? result.status, runId, toolId, status: 'failed' });
158
158
  }
159
159
  /**
160
160
  * `POST /api/delegated-tool-calls` — executes one delegated tool call against an already-started
@@ -183,7 +183,7 @@ export const delegatedToolExecuteRoute = defineJsonRoute({
183
183
  principal = await deps.resolvePrincipal({ request: input });
184
184
  }
185
185
  catch (error) {
186
- return err({ error: reportInternalError(deps, 'resolve-principal', error, input.runId, input.toolId) });
186
+ return err({ error: reportInternalError(deps, { source: 'resolve-principal', error, runId: input.runId, toolId: input.toolId }) });
187
187
  }
188
188
  // Attenuates the resolved principal itself for a `requireReadOnly` call, so the constraint
189
189
  // survives past this route: a consumer's own `ToolExecutor` composition may dispatch a tool id
@@ -203,7 +203,7 @@ export const delegatedToolExecuteRoute = defineJsonRoute({
203
203
  const toolExecutor = input.requireReadOnly === true
204
204
  ? readOnlyTools.withReadOnlyToolConstraint({ inner: deps.toolExecutor, registry: deps.toolRegistry, messages: readOnlyTools.defaultDaemonMessages.readOnly, idGenerator: { newId: randomUUID } })
205
205
  : deps.toolExecutor;
206
- const bridge = createDelegatedToolBridge({ lifecycle: deps.lifecycle, toolExecutor });
206
+ const bridge = createDelegatedToolBridge({ lifecycle: deps.lifecycle, toolExecutor }, deps);
207
207
  try {
208
208
  // `signal` carries the caller's HTTP connection dropping — see `mountJsonRoute`
209
209
  // (`adapter.ts`). The bridge already combines it with the run's own cancellation
@@ -217,13 +217,13 @@ export const delegatedToolExecuteRoute = defineJsonRoute({
217
217
  input: input.input,
218
218
  ...(signal !== undefined ? { signal } : {}),
219
219
  });
220
- return toolExecutionResultToApiResult(deps, input.runId, input.toolId, result);
220
+ return toolExecutionResultToApiResult(deps, { runId: input.runId, toolId: input.toolId, result });
221
221
  }
222
222
  catch (error) {
223
223
  if (error instanceof ToolInputError) {
224
224
  return err({ error: createApiError({ code: 'BAD_REQUEST', message: error.message }) });
225
225
  }
226
- return err({ error: reportInternalError(deps, 'delegated-tool-execute', error, input.runId, input.toolId) });
226
+ return err({ error: reportInternalError(deps, { source: 'delegated-tool-execute', error, runId: input.runId, toolId: input.toolId }) });
227
227
  }
228
228
  }
229
229
  }, { requireSameOrigin: true });
@@ -102,6 +102,7 @@ type ResolvedEntry = {
102
102
  } | {
103
103
  readonly available: false;
104
104
  };
105
+ /** Resolves how to launch `entry`: its CLI shim first, then (darwin only) its app bundle, else unavailable. */
105
106
  export declare function resolveEntry({ entry }: {
106
107
  readonly entry: CatalogueEntry;
107
108
  }, { probeEnv }?: {
@@ -24,7 +24,8 @@ import { createApiError } from '@jini-ai/protocol';
24
24
  import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
25
25
  import { validationError } from '@jini-ai/http-kit';
26
26
  import { err, ok } from '@jini-ai/http-kit';
27
- import { denyAllWorkspaceRoots, resolveWorkspaceRoot, WorkspaceRootDeniedError } from './workspace-root.js';
27
+ import { denyAllWorkspaceRoots } from './workspace-root.js';
28
+ import { isRecord, readOptionalNonEmptyString, resolveResourceRoot, withoutUndefined } from './resource-request.js';
28
29
  const MAC_OPEN_COMMAND = '/usr/bin/open';
29
30
  /** The cross-platform-staple + macOS-bundle editor/tool catalogue, unchanged from the origin. */
30
31
  export const CATALOGUE = [
@@ -148,33 +149,38 @@ export async function probeMacBundle({ name, probeEnv }, _optional = {}) {
148
149
  }
149
150
  return null;
150
151
  }
152
+ /** The CLI-shim half of {@link resolveEntry}: `null` when the entry has no `command` or it is not found. */
153
+ async function resolveViaCommand({ entry, probeEnv }) {
154
+ if (!entry.command)
155
+ return null;
156
+ const resolved = await probeCommandOnPath({ command: entry.command, probeEnv });
157
+ if (!resolved)
158
+ return null;
159
+ const commandArgs = entry.commandArgs ?? (({ workingDir }) => [workingDir]);
160
+ return { available: true, resolvedPath: resolved, launch: { command: resolved, argsForDir: ({ workingDir }) => commandArgs({ workingDir }) } };
161
+ }
162
+ /**
163
+ * The macOS app-bundle half of {@link resolveEntry} (launched via `open -a`): `null` without a
164
+ * `macOpenBundle` or when no bundle is found. Off darwin it is `null` too: {@link probeMacBundle}
165
+ * itself returns `null` before any filesystem access on every non-darwin platform, so it is the
166
+ * single darwin gate (this function used to repeat that check).
167
+ */
168
+ async function resolveViaMacBundle({ entry, probeEnv }) {
169
+ if (!entry.macOpenBundle)
170
+ return null;
171
+ const bundle = await probeMacBundle({ name: entry.macOpenBundle, probeEnv });
172
+ if (!bundle)
173
+ return null;
174
+ const macOpenArgs = entry.macOpenArgs ?? (({ bundleName, workingDir }) => ['-a', bundleName, workingDir]);
175
+ return {
176
+ available: true,
177
+ resolvedPath: bundle.path,
178
+ launch: { command: await resolveMacOpenCommand(probeEnv), argsForDir: ({ workingDir }) => macOpenArgs({ bundleName: bundle.name, workingDir }) },
179
+ };
180
+ }
181
+ /** Resolves how to launch `entry`: its CLI shim first, then (darwin only) its app bundle, else unavailable. */
151
182
  export async function resolveEntry({ entry }, { probeEnv = defaultProbeEnv({}) } = {}) {
152
- if (entry.command) {
153
- const resolved = await probeCommandOnPath({ command: entry.command, probeEnv });
154
- if (resolved) {
155
- return {
156
- available: true,
157
- resolvedPath: resolved,
158
- launch: { command: resolved, argsForDir: entry.commandArgs ? ({ workingDir }) => entry.commandArgs({ workingDir }) : ({ workingDir }) => [workingDir] },
159
- };
160
- }
161
- }
162
- if (entry.macOpenBundle && probeEnv.platform === 'darwin') {
163
- const bundle = await probeMacBundle({ name: entry.macOpenBundle, probeEnv });
164
- if (bundle) {
165
- return {
166
- available: true,
167
- resolvedPath: bundle.path,
168
- launch: {
169
- command: await resolveMacOpenCommand(probeEnv),
170
- argsForDir: entry.macOpenArgs
171
- ? ({ workingDir }) => entry.macOpenArgs({ bundleName: bundle.name, workingDir })
172
- : ({ workingDir }) => ['-a', bundle.name, workingDir],
173
- },
174
- };
175
- }
176
- }
177
- return { available: false };
183
+ return (await resolveViaCommand({ entry, probeEnv })) ?? (await resolveViaMacBundle({ entry, probeEnv })) ?? { available: false };
178
184
  }
179
185
  /** Resolves the concrete `{command, args}` a caller would spawn to open `workingDir` in `editorId`, or `available: false` if that tool isn't installed/known. */
180
186
  export async function resolveHostToolLaunchPlan({ editorId, workingDir }, { probeEnv = defaultProbeEnv({}) } = {}) {
@@ -249,9 +255,6 @@ export async function listAvailableEditors(_required, { probeEnv = defaultProbeE
249
255
  }
250
256
  /** `GET /api/editors` — lists the host-tool catalogue applicable to this platform with live availability. */
251
257
  export const hostEditorsRoute = defineJsonRoute({ method: 'get', path: '/api/editors', parse: () => ok({ value: undefined }), handle: async () => ok({ value: await listAvailableEditors({}) }) });
252
- function isRecord(value) {
253
- return typeof value === 'object' && value !== null && !Array.isArray(value);
254
- }
255
258
  function parseOpenResourceInEditor(input) {
256
259
  const resourceRef = input.params.resourceRef;
257
260
  if (typeof resourceRef !== 'string' || resourceRef.length === 0) {
@@ -263,11 +266,31 @@ function parseOpenResourceInEditor(input) {
263
266
  if (typeof editorId !== 'string' || editorId.length === 0) {
264
267
  return err({ error: validationError({ message: 'editorId is required' }, { issues: [{ path: 'editorId', message: 'required non-empty string' }] }) });
265
268
  }
266
- const detail = input.body.detail;
267
- if (detail !== undefined && (typeof detail !== 'string' || detail.length === 0)) {
268
- return err({ error: validationError({ message: 'detail must be a non-empty string when provided' }) });
269
+ const detail = readOptionalNonEmptyString({ body: input.body, field: 'detail' });
270
+ if (!detail.ok)
271
+ return detail;
272
+ return ok({ value: { resourceRef, editorId, ...withoutUndefined({ detail: detail.value }) } });
273
+ }
274
+ /** Looks `editorId` up in {@link CATALOGUE} and checks it applies to `platform`; both refusals are `BAD_REQUEST`. */
275
+ function selectEditor({ editorId, platform }) {
276
+ const entry = CATALOGUE.find((c) => c.id === editorId);
277
+ if (!entry)
278
+ return err({ error: createApiError({ code: 'BAD_REQUEST', message: `unknown editor: ${editorId}` }) });
279
+ if (!applicableForPlatform({ entry, platform })) {
280
+ return err({ error: createApiError({ code: 'BAD_REQUEST', message: `${entry.label} is not available on ${platform}` }) });
281
+ }
282
+ return ok({ value: entry });
283
+ }
284
+ /** Probes `entry` and launches it at `workingDir`: `CONFLICT` when it is not installed, `INTERNAL_ERROR` (with the OS's own reason) when the launch is refused. */
285
+ async function launchEditorAt({ entry, workingDir }, { probeEnv, spawnImpl }) {
286
+ const probe = await resolveEntry({ entry }, { probeEnv });
287
+ if (!probe.available)
288
+ return err({ error: createApiError({ code: 'CONFLICT', message: `${entry.label} is not installed` }) });
289
+ const launch = await launchHostTool({ command: probe.launch.command, args: probe.launch.argsForDir({ workingDir }) }, { spawnImpl });
290
+ if (!launch.ok) {
291
+ return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: `failed to launch ${entry.label}: ${launch.error}` }) });
269
292
  }
270
- return ok({ value: { resourceRef, editorId, ...(detail === undefined ? {} : { detail }) } });
293
+ return ok({ value: { ok: true, editorId: entry.id, path: workingDir } });
271
294
  }
272
295
  /**
273
296
  * `POST /api/resources/:resourceRef/open-in` — launches `editorId` (from
@@ -293,32 +316,14 @@ function parseOpenResourceInEditor(input) {
293
316
  */
294
317
  export const openResourceInEditorRoute = defineJsonRoute({
295
318
  method: 'post', path: '/api/resources/:resourceRef/open-in', parse: parseOpenResourceInEditor, handle: async ({ input, deps }) => {
296
- const entry = CATALOGUE.find((c) => c.id === input.editorId);
297
- if (!entry)
298
- return err({ error: createApiError({ code: 'BAD_REQUEST', message: `unknown editor: ${input.editorId}` }) });
299
319
  const probeEnv = deps.probeEnv ?? defaultProbeEnv({});
300
- if (!applicableForPlatform({ entry, platform: probeEnv.platform })) {
301
- return err({ error: createApiError({ code: 'BAD_REQUEST', message: `${entry.label} is not available on ${probeEnv.platform}` }) });
302
- }
303
- let resolvedDir;
304
- try {
305
- resolvedDir = await resolveWorkspaceRoot({ request: { resourceRef: input.resourceRef, ...(input.detail === undefined ? {} : { detail: input.detail }) } }, { resolver: deps.resolveRoot ?? denyAllWorkspaceRoots });
306
- }
307
- catch (error) {
308
- if (error instanceof WorkspaceRootDeniedError) {
309
- return err({ error: createApiError({ code: 'NOT_FOUND', message: `resource "${input.resourceRef}" was not found` }) });
310
- }
311
- throw error;
312
- }
313
- const launchPlan = await resolveHostToolLaunchPlan({ editorId: input.editorId, workingDir: resolvedDir }, { probeEnv });
314
- if (!launchPlan.available || !launchPlan.command || !launchPlan.args) {
315
- return err({ error: createApiError({ code: 'CONFLICT', message: `${entry.label} is not installed` }) });
316
- }
317
- const launch = await launchHostTool({ command: launchPlan.command, args: launchPlan.args }, { spawnImpl: deps.spawnImpl ?? nodeSpawn });
318
- if (!launch.ok) {
319
- return err({ error: createApiError({ code: 'INTERNAL_ERROR', message: `failed to launch ${entry.label}: ${launch.error}` }) });
320
- }
321
- return ok({ value: { ok: true, editorId: input.editorId, path: resolvedDir } });
320
+ const editor = selectEditor({ editorId: input.editorId, platform: probeEnv.platform });
321
+ if (!editor.ok)
322
+ return editor;
323
+ const root = await resolveResourceRoot({ resourceRef: input.resourceRef, detail: input.detail }, { resolver: deps.resolveRoot ?? denyAllWorkspaceRoots });
324
+ if (!root.ok)
325
+ return root;
326
+ return launchEditorAt({ entry: editor.value, workingDir: root.value }, { probeEnv, spawnImpl: deps.spawnImpl ?? nodeSpawn });
322
327
  }
323
328
  }, { requireSameOrigin: true });
324
329
  /** Mounts `GET /api/editors` and `POST /api/resources/:resourceRef/open-in` on `app`. A pack's `http(app, services)` calls this directly. */
@@ -38,8 +38,8 @@ export type { ModelProxyHttpDeps, ModelProxyInternalErrorContext } from './model
38
38
  export { registerModelProxyRoutes } from './model-proxy.js';
39
39
  export type { HealthHttpDeps, HealthReadinessResult, LivenessResponse, ReadinessResponse, VersionResponse } from './health.js';
40
40
  export { apiHealthRoute, apiReadyRoute, apiVersionInfoRoute, healthRoute, readyRoute, registerHealthRoutes, versionInfoRoute, } from './health.js';
41
- export type { AuthCredentials, AuthProvider, AuthSession, AuthUser, Charge, ChargeInput, ChargeStatus, ConnectorsAuthSessionResponse, ConnectorsAuthUserResponse, ConnectorsAuthVerifyResponse, ConnectorsChargeResponse, ConnectorsDbQueryResponse, ConnectorsDbRecordResponse, ConnectorsHttpDeps, ConnectorsInternalErrorContext, ConnectorsOkResponse, ConnectorsStorageGetResponse, ConnectorsStorageListResponse, ConnectorsStorageMetaResponse, DbProvider, DbQuery, DbRecord, PaymentsProvider, RealtimeProvider, StorageObjectMeta, StorageProvider, StoragePutOptions, } from './connectors.js';
42
- export { connectorsAuthSessionRoute, connectorsAuthSignInRoute, connectorsAuthSignOutRoute, connectorsAuthSignUpRoute, connectorsDbDeleteRoute, connectorsDbGetRoute, connectorsDbInsertRoute, connectorsDbQueryRoute, connectorsDbUpdateRoute, connectorsPaymentsChargeRoute, connectorsPaymentsGetRoute, connectorsPaymentsRefundRoute, connectorsRealtimePublishRoute, connectorsStorageDeleteRoute, connectorsStorageGetRoute, connectorsStorageListRoute, connectorsStoragePutRoute, registerConnectorsRoutes, } from './connectors.js';
41
+ export type { AuthCredentials, AuthProvider, AuthSession, AuthUser, ConnectorsAuthSessionResponse, ConnectorsAuthUserResponse, ConnectorsAuthVerifyResponse, ConnectorsDbQueryResponse, ConnectorsDbRecordResponse, ConnectorsHttpDeps, ConnectorsInternalErrorContext, ConnectorsOkResponse, ConnectorsStorageGetResponse, ConnectorsStorageListResponse, ConnectorsStorageMetaResponse, DbProvider, DbQuery, DbRecord, RealtimeProvider, StorageObjectMeta, StorageProvider, StoragePutOptions, } from './connectors.js';
42
+ export { connectorsAuthSessionRoute, connectorsAuthSignInRoute, connectorsAuthSignOutRoute, connectorsAuthSignUpRoute, connectorsDbDeleteRoute, connectorsDbGetRoute, connectorsDbInsertRoute, connectorsDbQueryRoute, connectorsDbUpdateRoute, connectorsRealtimePublishRoute, connectorsStorageDeleteRoute, connectorsStorageGetRoute, connectorsStorageListRoute, connectorsStoragePutRoute, registerConnectorsRoutes, } from './connectors.js';
43
43
  export type { ResearchProvider, ResearchSearchRequest, ResearchHttpDeps, ResearchInternalErrorContext, ResearchProviderCredentials, ResearchSearchResponse, ResearchSource, } from './research.js';
44
44
  export { createResearchSearchRoute, registerResearchRoutes, researchSearchRoute } from './research.js';
45
45
  export type { MediaGenerateResponse, MediaHttpDeps, MediaInternalErrorContext, MediaTaskDeleteResponse, MediaTaskListResponse, MediaTaskResponse, } from './media.js';
@@ -18,7 +18,7 @@ export { createFrontendControl } from './frontend-control.js';
18
18
  export { registerTerminalEventStream, registerTerminalRoutes, terminalCreateRoute, terminalDeleteRoute, terminalKillRoute, terminalListRoute, terminalResizeRoute, terminalStdinRoute, } from './terminals.js';
19
19
  export { registerModelProxyRoutes } from './model-proxy.js';
20
20
  export { apiHealthRoute, apiReadyRoute, apiVersionInfoRoute, healthRoute, readyRoute, registerHealthRoutes, versionInfoRoute, } from './health.js';
21
- export { connectorsAuthSessionRoute, connectorsAuthSignInRoute, connectorsAuthSignOutRoute, connectorsAuthSignUpRoute, connectorsDbDeleteRoute, connectorsDbGetRoute, connectorsDbInsertRoute, connectorsDbQueryRoute, connectorsDbUpdateRoute, connectorsPaymentsChargeRoute, connectorsPaymentsGetRoute, connectorsPaymentsRefundRoute, connectorsRealtimePublishRoute, connectorsStorageDeleteRoute, connectorsStorageGetRoute, connectorsStorageListRoute, connectorsStoragePutRoute, registerConnectorsRoutes, } from './connectors.js';
21
+ export { connectorsAuthSessionRoute, connectorsAuthSignInRoute, connectorsAuthSignOutRoute, connectorsAuthSignUpRoute, connectorsDbDeleteRoute, connectorsDbGetRoute, connectorsDbInsertRoute, connectorsDbQueryRoute, connectorsDbUpdateRoute, connectorsRealtimePublishRoute, connectorsStorageDeleteRoute, connectorsStorageGetRoute, connectorsStorageListRoute, connectorsStoragePutRoute, registerConnectorsRoutes, } from './connectors.js';
22
22
  export { createResearchSearchRoute, registerResearchRoutes, researchSearchRoute } from './research.js';
23
23
  export { mediaGenerateRoute, mediaTaskDeleteRoute, mediaTaskGetRoute, mediaTaskListRoute, registerMediaRoutes, } from './media.js';
24
24
  export { ATTACHMENTS_ROUTE_PATH, AttachmentRejectedError, createDiskAttachmentStore, detectAttachmentKind, handleAttachmentCleanup, handleAttachmentUpload, isUnchangedAttachment, registerAttachmentRoutes, sanitizeAttachmentName, writeBoundedAttachmentBody, } from './attachments.js';
@@ -115,85 +115,64 @@ function parseOptionalImageRefs(body) {
115
115
  }
116
116
  return ok({ value: refs });
117
117
  }
118
+ function parseOptionalMember(body, key, allowed) {
119
+ const value = body[key];
120
+ if (value === undefined)
121
+ return ok({ value: undefined });
122
+ if (typeof value !== 'string' || !allowed.has(value)) {
123
+ return err({ error: validationError({ message: `${key} must be one of: ${[...allowed].join(', ')} when provided` }) });
124
+ }
125
+ return ok({ value });
126
+ }
127
+ /** Table order is the validation order: the first failing field is the one a caller sees. */
128
+ const OPTIONAL_FIELD_PARSERS = [
129
+ ['audioKind', (body) => parseOptionalMember(body, 'audioKind', AUDIO_KINDS)],
130
+ ['speechFormat', (body) => parseOptionalMember(body, 'speechFormat', SPEECH_FORMATS)],
131
+ ['prompt', (body) => parseOptionalString(body, 'prompt')],
132
+ ['aspect', (body) => parseOptionalString(body, 'aspect')],
133
+ ['voice', (body) => parseOptionalString(body, 'voice')],
134
+ ['language', (body) => parseOptionalString(body, 'language')],
135
+ ['wireModel', (body) => parseOptionalString(body, 'wireModel')],
136
+ ['length', (body) => parseOptionalNumber(body, 'length')],
137
+ ['duration', (body) => parseOptionalNumber(body, 'duration')],
138
+ ['promptInfluence', (body) => parseOptionalNumber(body, 'promptInfluence')],
139
+ ['loop', (body) => parseOptionalBoolean(body, 'loop')],
140
+ ['imageRef', parseOptionalImageRef],
141
+ ['imageRefs', parseOptionalImageRefs],
142
+ ];
143
+ /** Collects every provided optional field; an absent field is omitted, never written as `undefined`. */
144
+ function parseOptionalFields(body) {
145
+ const fields = {};
146
+ for (const [key, parse] of OPTIONAL_FIELD_PARSERS) {
147
+ const parsed = parse(body);
148
+ if (!parsed.ok)
149
+ return parsed;
150
+ if (parsed.value !== undefined)
151
+ fields[key] = parsed.value;
152
+ }
153
+ return ok({ value: fields });
154
+ }
155
+ function requiredStringError(key) {
156
+ return err({ error: validationError({ message: `${key} must be a non-empty string` }, { issues: [{ path: key, message: 'required non-empty string' }] }) });
157
+ }
118
158
  function parseMediaGenerate(input) {
119
159
  if (!isRecord(input.body))
120
160
  return err({ error: validationError({ message: 'body must be a JSON object' }) });
121
161
  const body = input.body;
122
162
  const ownerRef = requireNonEmptyString(body, 'ownerRef');
123
- if (ownerRef === undefined) {
124
- return err({ error: validationError({ message: 'ownerRef must be a non-empty string' }, { issues: [{ path: 'ownerRef', message: 'required non-empty string' }] }) });
125
- }
163
+ if (ownerRef === undefined)
164
+ return requiredStringError('ownerRef');
126
165
  const surface = body.surface;
127
166
  if (typeof surface !== 'string' || !SURFACES.has(surface)) {
128
167
  return err({ error: validationError({ message: 'surface must be one of: image, video, audio' }) });
129
168
  }
130
169
  const model = requireNonEmptyString(body, 'model');
131
- if (model === undefined) {
132
- return err({ error: validationError({ message: 'model must be a non-empty string' }, { issues: [{ path: 'model', message: 'required non-empty string' }] }) });
133
- }
134
- const audioKind = body.audioKind;
135
- if (audioKind !== undefined && (typeof audioKind !== 'string' || !AUDIO_KINDS.has(audioKind))) {
136
- return err({ error: validationError({ message: 'audioKind must be one of: music, speech, sfx when provided' }) });
137
- }
138
- const speechFormat = body.speechFormat;
139
- if (speechFormat !== undefined && (typeof speechFormat !== 'string' || !SPEECH_FORMATS.has(speechFormat))) {
140
- return err({ error: validationError({ message: 'speechFormat must be one of: mp3, wav, flac, aac, opus when provided' }) });
141
- }
142
- const prompt = parseOptionalString(body, 'prompt');
143
- if (!prompt.ok)
144
- return prompt;
145
- const aspect = parseOptionalString(body, 'aspect');
146
- if (!aspect.ok)
147
- return aspect;
148
- const voice = parseOptionalString(body, 'voice');
149
- if (!voice.ok)
150
- return voice;
151
- const language = parseOptionalString(body, 'language');
152
- if (!language.ok)
153
- return language;
154
- const wireModel = parseOptionalString(body, 'wireModel');
155
- if (!wireModel.ok)
156
- return wireModel;
157
- const length = parseOptionalNumber(body, 'length');
158
- if (!length.ok)
159
- return length;
160
- const duration = parseOptionalNumber(body, 'duration');
161
- if (!duration.ok)
162
- return duration;
163
- const promptInfluence = parseOptionalNumber(body, 'promptInfluence');
164
- if (!promptInfluence.ok)
165
- return promptInfluence;
166
- const loop = parseOptionalBoolean(body, 'loop');
167
- if (!loop.ok)
168
- return loop;
169
- const imageRef = parseOptionalImageRef(body);
170
- if (!imageRef.ok)
171
- return imageRef;
172
- const imageRefs = parseOptionalImageRefs(body);
173
- if (!imageRefs.ok)
174
- return imageRefs;
175
- return ok({
176
- value: {
177
- ownerRef,
178
- request: {
179
- surface: surface,
180
- model,
181
- ...(prompt.value !== undefined ? { prompt: prompt.value } : {}),
182
- ...(aspect.value !== undefined ? { aspect: aspect.value } : {}),
183
- ...(length.value !== undefined ? { length: length.value } : {}),
184
- ...(duration.value !== undefined ? { duration: duration.value } : {}),
185
- ...(voice.value !== undefined ? { voice: voice.value } : {}),
186
- ...(audioKind !== undefined ? { audioKind: audioKind } : {}),
187
- ...(language.value !== undefined ? { language: language.value } : {}),
188
- ...(loop.value !== undefined ? { loop: loop.value } : {}),
189
- ...(promptInfluence.value !== undefined ? { promptInfluence: promptInfluence.value } : {}),
190
- ...(imageRef.value !== undefined ? { imageRef: imageRef.value } : {}),
191
- ...(imageRefs.value !== undefined ? { imageRefs: imageRefs.value } : {}),
192
- ...(wireModel.value !== undefined ? { wireModel: wireModel.value } : {}),
193
- ...(speechFormat !== undefined ? { speechFormat: speechFormat } : {}),
194
- },
195
- }
196
- });
170
+ if (model === undefined)
171
+ return requiredStringError('model');
172
+ const options = parseOptionalFields(body);
173
+ if (!options.ok)
174
+ return options;
175
+ return ok({ value: { ownerRef, request: { surface: surface, model, ...options.value } } });
197
176
  }
198
177
  function parseTaskId(input) {
199
178
  const id = input.params.id;
@@ -1,16 +1,29 @@
1
1
  import { createApiError } from '@jini-ai/protocol';
2
- import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
3
- import { guardSameOrigin } from '@jini-ai/http-kit';
4
- import { validationError } from '@jini-ai/http-kit';
5
- import { sendApiError, statusForError } from '@jini-ai/http-kit';
6
- import { createSseChannel } from '@jini-ai/http-kit';
7
- import { err, ok } from '@jini-ai/http-kit';
2
+ import { createSseChannel, defineJsonRoute, err, guardSameOrigin, mountJsonRoute, ok, sendApiError, statusForError, validationError, } from '@jini-ai/http-kit';
8
3
  function isRecord(value) {
9
4
  return typeof value === 'object' && value !== null && !Array.isArray(value);
10
5
  }
11
6
  function errorMessage(error) {
12
7
  return error instanceof Error ? error.message : String(error);
13
8
  }
9
+ /** Reads an optional string field: absent (`undefined`) passes through, anything else that is not a string — `null` included — is refused. */
10
+ function readOptionalString(body, key) {
11
+ const value = body[key];
12
+ if (value === undefined || typeof value === 'string')
13
+ return ok({ value });
14
+ return err({ error: validationError({ message: `${key} must be a string when provided` }) });
15
+ }
16
+ /** Reads a required, non-empty string field, refusing with a per-field validation issue. */
17
+ function readRequiredString(body, key) {
18
+ const value = body[key];
19
+ if (typeof value === 'string' && value.length > 0)
20
+ return ok({ value });
21
+ return err({ error: validationError({ message: `${key} is required` }, { issues: [{ path: key, message: 'required non-empty string' }] }) });
22
+ }
23
+ /** The `BAD_REQUEST` every mutating route answers when its store call throws, carrying the store's own message. */
24
+ function storeFailure(error) {
25
+ return err({ error: createApiError({ code: 'BAD_REQUEST', message: errorMessage(error) }) });
26
+ }
14
27
  function parseIdParam(input) {
15
28
  const id = input.params.id;
16
29
  return typeof id === 'string' && id.length > 0 ? ok({ value: id }) : err({ error: validationError({ message: 'id must be a non-empty path parameter' }) });
@@ -45,12 +58,11 @@ function parseUpdateTreeNode(input) {
45
58
  return err({ error: validationError({ message: 'body must be a JSON object' }) });
46
59
  const patch = {};
47
60
  for (const key of ['name', 'description', 'type', 'body']) {
48
- const value = input.body[key];
49
- if (value === undefined)
50
- continue;
51
- if (typeof value !== 'string')
52
- return err({ error: validationError({ message: `${key} must be a string when provided` }) });
53
- patch[key] = value;
61
+ const value = readOptionalString(input.body, key);
62
+ if (!value.ok)
63
+ return value;
64
+ if (value.value !== undefined)
65
+ patch[key] = value.value;
54
66
  }
55
67
  return ok({ value: { id: parsedId.value, patch } });
56
68
  }
@@ -105,7 +117,7 @@ export const memoryWriteIndexRoute = defineJsonRoute({
105
117
  return ok({ value: { index } });
106
118
  }
107
119
  catch (error) {
108
- return err({ error: createApiError({ code: 'BAD_REQUEST', message: errorMessage(error) }) });
120
+ return storeFailure(error);
109
121
  }
110
122
  },
111
123
  }, { requireSameOrigin: true });
@@ -128,7 +140,7 @@ export const memoryWriteConfigRoute = defineJsonRoute({
128
140
  return ok({ value: { enabled: next.enabled } });
129
141
  }
130
142
  catch (error) {
131
- return err({ error: createApiError({ code: 'BAD_REQUEST', message: errorMessage(error) }) });
143
+ return storeFailure(error);
132
144
  }
133
145
  },
134
146
  }, { requireSameOrigin: true });
@@ -171,27 +183,24 @@ export const memoryRemoveVerificationRoute = defineJsonRoute({
171
183
  function parseEntryInput(input) {
172
184
  if (!isRecord(input.body))
173
185
  return err({ error: validationError({ message: 'body must be a JSON object' }) });
174
- const { name, type } = input.body;
175
- if (typeof name !== 'string' || name.length === 0) {
176
- return err({ error: validationError({ message: 'name is required' }, { issues: [{ path: 'name', message: 'required non-empty string' }] }) });
177
- }
178
- if (typeof type !== 'string' || type.length === 0) {
179
- return err({ error: validationError({ message: 'type is required' }, { issues: [{ path: 'type', message: 'required non-empty string' }] }) });
180
- }
181
- const description = input.body.description;
182
- if (description !== undefined && typeof description !== 'string') {
183
- return err({ error: validationError({ message: 'description must be a string when provided' }) });
184
- }
185
- const body = input.body.body;
186
- if (body !== undefined && typeof body !== 'string') {
187
- return err({ error: validationError({ message: 'body must be a string when provided' }) });
188
- }
186
+ const name = readRequiredString(input.body, 'name');
187
+ if (!name.ok)
188
+ return name;
189
+ const type = readRequiredString(input.body, 'type');
190
+ if (!type.ok)
191
+ return type;
192
+ const description = readOptionalString(input.body, 'description');
193
+ if (!description.ok)
194
+ return description;
195
+ const body = readOptionalString(input.body, 'body');
196
+ if (!body.ok)
197
+ return body;
189
198
  return ok({
190
199
  value: {
191
- name,
192
- type,
193
- ...(description === undefined ? {} : { description }),
194
- ...(body === undefined ? {} : { body }),
200
+ name: name.value,
201
+ type: type.value,
202
+ ...(description.value === undefined ? {} : { description: description.value }),
203
+ ...(body.value === undefined ? {} : { body: body.value }),
195
204
  }
196
205
  });
197
206
  }
@@ -205,7 +214,7 @@ export const memoryCreateEntryRoute = defineJsonRoute({
205
214
  return ok({ value: { entry } });
206
215
  }
207
216
  catch (error) {
208
- return err({ error: createApiError({ code: 'BAD_REQUEST', message: errorMessage(error) }) });
217
+ return storeFailure(error);
209
218
  }
210
219
  },
211
220
  }, { requireSameOrigin: true, successStatus: 201 });
@@ -237,7 +246,7 @@ export const memoryUpdateEntryRoute = defineJsonRoute({
237
246
  return ok({ value: { entry } });
238
247
  }
239
248
  catch (error) {
240
- return err({ error: createApiError({ code: 'BAD_REQUEST', message: errorMessage(error) }) });
249
+ return storeFailure(error);
241
250
  }
242
251
  },
243
252
  }, { requireSameOrigin: true });
@@ -251,7 +260,7 @@ export const memoryDeleteEntryRoute = defineJsonRoute({
251
260
  return ok({ value: { ok: true } });
252
261
  }
253
262
  catch (error) {
254
- return err({ error: createApiError({ code: 'BAD_REQUEST', message: errorMessage(error) }) });
263
+ return storeFailure(error);
255
264
  }
256
265
  },
257
266
  }, { requireSameOrigin: true });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @module model-proxy-fields
3
+ *
4
+ * Request-body field helpers for `model-proxy.ts`'s provider parsers: primitive type tests, the
5
+ * table-driven "optional field present but badly typed" check, the required-field 400 builder, and
6
+ * the "forward an optional field only when present" copy. Transport-level shape only — nothing here
7
+ * knows any provider's wire protocol (see `model-proxy.ts`'s module doc).
8
+ */
9
+ import type { ApiError } from '@jini-ai/protocol';
10
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
11
+ export declare function nonEmptyString(value: unknown): string | null;
12
+ export declare const isString: (value: unknown) => boolean;
13
+ export declare const isNumber: (value: unknown) => boolean;
14
+ export declare const isArray: (value: unknown) => boolean;
15
+ /** One optional body field: its name, the primitive-type test it must pass when present, and the 400 message when it does not. */
16
+ export type OptionalFieldCheck = readonly [field: string, isValid: (value: unknown) => boolean, message: string];
17
+ /** The first optional field that is present (not `undefined`) but fails its type test, as a 400 error; `undefined` when every present field is well-typed. */
18
+ export declare function firstOptionalFieldError(record: Record<string, unknown>, checks: readonly OptionalFieldCheck[]): ApiError | undefined;
19
+ /** A 400 for a required field, carrying the single-issue `details` the client can point at. */
20
+ export declare function requiredFieldError(path: string, message: string, issue: string): ApiError;
21
+ /** Copies only the listed keys whose value is not `undefined` — the "forward an optional field only when present" rule every parser and turn-runner call shares. */
22
+ export declare function pickDefined<T extends object, K extends keyof T>(source: T, keys: readonly K[]): Partial<Pick<T, K>>;
23
+ //# sourceMappingURL=model-proxy-fields.d.ts.map