@loicngr/kobo 2.1.29 → 2.1.30

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 (160) hide show
  1. package/AGENTS.md +59 -10
  2. package/CHANGELOG.md +16 -1
  3. package/CONFIGURATION.md +114 -14
  4. package/CONTRIBUTING.md +1 -1
  5. package/DESIGN.md +1 -1
  6. package/README.md +12 -9
  7. package/SECURITY.md +7 -2
  8. package/dist/server/db/memory-schema.js +5 -5
  9. package/dist/server/db/migrations.js +2 -0
  10. package/dist/server/db/mistral-memory-migration.js +40 -0
  11. package/dist/server/routes/engines.js +14 -0
  12. package/dist/server/routes/environment.js +1 -1
  13. package/dist/server/routes/health.js +14 -3
  14. package/dist/server/routes/settings.js +7 -1
  15. package/dist/server/routes/workspaces.js +116 -9
  16. package/dist/server/services/agent/engines/claude-code/capabilities.js +2 -0
  17. package/dist/server/services/agent/engines/claude-code/engine.js +3 -0
  18. package/dist/server/services/agent/engines/codex/capabilities.js +2 -0
  19. package/dist/server/services/agent/engines/codex/engine.js +3 -0
  20. package/dist/server/services/agent/engines/configuration.js +14 -0
  21. package/dist/server/services/agent/engines/mistral-vibe/capabilities.js +13 -0
  22. package/dist/server/services/agent/engines/mistral-vibe/client.js +64 -0
  23. package/dist/server/services/agent/engines/mistral-vibe/configuration.js +76 -0
  24. package/dist/server/services/agent/engines/mistral-vibe/engine.js +361 -0
  25. package/dist/server/services/agent/engines/mistral-vibe/event-mapper.js +297 -0
  26. package/dist/server/services/agent/engines/mistral-vibe/options-builder.js +35 -0
  27. package/dist/server/services/agent/engines/mistral-vibe/server-requests.js +278 -0
  28. package/dist/server/services/agent/engines/mistral-vibe/spawn.js +176 -0
  29. package/dist/server/services/agent/engines/registry.js +2 -0
  30. package/dist/server/services/agent/engines/types.js +6 -0
  31. package/dist/server/services/agent/orchestrator.js +88 -25
  32. package/dist/server/services/agent/session-controller.js +5 -0
  33. package/dist/server/services/auto-loop-final-review-service.js +10 -3
  34. package/dist/server/services/environment-check-service.js +6 -1
  35. package/dist/server/services/memory-service.js +1 -1
  36. package/dist/server/services/mistral-options-service.js +145 -0
  37. package/dist/server/services/pr-watcher-service.js +40 -11
  38. package/dist/server/services/review-service.js +33 -9
  39. package/dist/server/services/session-handoff-service.js +28 -2
  40. package/dist/server/services/settings-defaults.js +1 -0
  41. package/dist/server/services/settings-service.js +19 -1
  42. package/dist/server/services/workspace-creation-mcp-service.js +50 -8
  43. package/dist/server/services/workspace-dialogue-service.js +2 -0
  44. package/dist/server/services/workspace-message-service.js +19 -1
  45. package/dist/server/services/workspace-permission-policy-service.js +24 -0
  46. package/dist/server/utils/bounded-process.js +1 -1
  47. package/dist/shared/launch-instructions.js +18 -7
  48. package/dist/shared/memory.js +1 -1
  49. package/dist/shared/mistral-models.js +9 -0
  50. package/dist/shared/mistral-options.js +1 -0
  51. package/dist/shared/question-selections.js +16 -0
  52. package/dist/shared/workspace-creation-tools.js +8 -2
  53. package/dist/shared/workspace-dialogue-tools.js +15 -4
  54. package/docs/deployment-vps.md +10 -3
  55. package/docs/getting-started.md +3 -2
  56. package/docs/troubleshooting.md +1 -1
  57. package/package.json +7 -1
  58. package/src/client/dist/pwa/assets/ActivityFeed-C5stmHXP.js +2 -0
  59. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-CIDSX32d.css +1 -0
  60. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-DG32W_0G.js +1 -0
  61. package/src/client/dist/pwa/assets/{ChangelogPage-C5KjAOMG.js → ChangelogPage-_vItY8vA.js} +1 -1
  62. package/src/client/dist/pwa/assets/{ComparisonPanel-XXyzPEzW.js → ComparisonPanel-B3BMzQH0.js} +1 -1
  63. package/src/client/dist/pwa/assets/CreatePage-FlGrfmjX.js +2 -0
  64. package/src/client/dist/pwa/assets/CreatePage-oh8ihVC4.css +1 -0
  65. package/src/client/dist/pwa/assets/{DashboardPage-DyhUu26C.js → DashboardPage-lJGwWQYQ.js} +1 -1
  66. package/src/client/dist/pwa/assets/DiffViewer-DIaQH3lA.js +8 -0
  67. package/src/client/dist/pwa/assets/{DocumentsPanel-ItndGxeB.js → DocumentsPanel-Ct-nm4fX.js} +1 -1
  68. package/src/client/dist/pwa/assets/{DrawerToggleButton-DfzS_CD8.js → DrawerToggleButton-M5qv4Io-.js} +1 -1
  69. package/src/client/dist/pwa/assets/GitPanel-BaHCgb3p.js +3 -0
  70. package/src/client/dist/pwa/assets/GitPanel-C8vw2qvW.css +1 -0
  71. package/src/client/dist/pwa/assets/HealthPage-DR3wa6iD.js +1 -0
  72. package/src/client/dist/pwa/assets/HealthPage-Dbn4J_DT.css +1 -0
  73. package/src/client/dist/pwa/assets/{MainLayout-DoAEgANU.css → MainLayout-CHi8qP1D.css} +1 -1
  74. package/src/client/dist/pwa/assets/MainLayout-WJabICsA.js +2 -0
  75. package/src/client/dist/pwa/assets/{MemoryPanel-CZzI7_nY.js → MemoryPanel-CRpWdfc0.js} +1 -1
  76. package/src/client/dist/pwa/assets/{MemoryProposalList-DqNTycY1.js → MemoryProposalList-DGz3lDbL.js} +1 -1
  77. package/src/client/dist/pwa/assets/{QBtnDropdown-D7ml7zHo.js → QBtnDropdown-tt8rUNNR.js} +1 -1
  78. package/src/client/dist/pwa/assets/{QExpansionItem-BVb067oc.js → QExpansionItem-BkrI3UPL.js} +1 -1
  79. package/src/client/dist/pwa/assets/{QScrollArea-CUDWkEHh.js → QScrollArea-BvgnY-oA.js} +1 -1
  80. package/src/client/dist/pwa/assets/QSelect-BtZB1wrB.js +1 -0
  81. package/src/client/dist/pwa/assets/{QTooltip-B5QJIU3p.js → QTooltip-CDH6p745.js} +1 -1
  82. package/src/client/dist/pwa/assets/{SchedulePanel-IJOcrHgk.js → SchedulePanel-C2rWlh7l.js} +1 -1
  83. package/src/client/dist/pwa/assets/{SearchPage-BpTRWtne.js → SearchPage-BFd4NQwJ.js} +1 -1
  84. package/src/client/dist/pwa/assets/{SettingsPage-B4rmd2WX.css → SettingsPage-Bm0TcarK.css} +1 -1
  85. package/src/client/dist/pwa/assets/SettingsPage-DDQCgnjL.js +9 -0
  86. package/src/client/dist/pwa/assets/{SplitWorkspacePage-BZ8wN4ii.js → SplitWorkspacePage-DKCtOZO4.js} +1 -1
  87. package/src/client/dist/pwa/assets/{TerminalPanel-pY-RGBb6.js → TerminalPanel-BE_MSZF0.js} +4 -4
  88. package/src/client/dist/pwa/assets/{ToolCallItem-C9ksVseJ.js → ToolCallItem-BEYaOtgh.js} +3 -3
  89. package/src/client/dist/pwa/assets/{TouchPan-DRynDIve.js → TouchPan-DeYoOjX5.js} +1 -1
  90. package/src/client/dist/pwa/assets/{TourReplayButton-D4h1QR6C.js → TourReplayButton-D5lMwIKt.js} +1 -1
  91. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-BHEAsIoo.js +1 -0
  92. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-v7ein__H.css +1 -0
  93. package/src/client/dist/pwa/assets/WorkspacePage-5_wsJPaJ.js +4 -0
  94. package/src/client/dist/pwa/assets/WorkspacePage-CtUXz0Cq.css +1 -0
  95. package/src/client/dist/pwa/assets/{browser-BcOi2ES2.js → browser-ogy_9LVv.js} +1 -1
  96. package/src/client/dist/pwa/assets/{build-path-tree-mDco3pi7.js → build-path-tree-CRXxaYkA.js} +1 -1
  97. package/src/client/dist/pwa/assets/custom-sounds-iLTkCd_i.js +1 -0
  98. package/src/client/dist/pwa/assets/de-BqDU7iZH.js +17 -0
  99. package/src/client/dist/pwa/assets/{editor.api-CRrihXg2.js → editor.api-Dx6TbfZG.js} +1 -1
  100. package/src/client/dist/pwa/assets/{efforts-C-p9DzkL.js → efforts-DnJh2fW_.js} +2 -2
  101. package/src/client/dist/pwa/assets/es-D4VMXQRk.js +17 -0
  102. package/src/client/dist/pwa/assets/{expand-template-gIwrPbeS.js → expand-template-PUEVWny3.js} +1 -1
  103. package/src/client/dist/pwa/assets/{format-DeBnwmgW.js → format-yf9XM3dV.js} +1 -1
  104. package/src/client/dist/pwa/assets/{formatters-CxZZ_WTb.js → formatters-D0sXu9Oe.js} +1 -1
  105. package/src/client/dist/pwa/assets/fr-DIyaCFB3.js +17 -0
  106. package/src/client/dist/pwa/assets/{freemarker2-h7OLlqTn.js → freemarker2-l_xIHxRf.js} +1 -1
  107. package/src/client/dist/pwa/assets/{handlebars-BCWmi1-q.js → handlebars-CYgq99yQ.js} +1 -1
  108. package/src/client/dist/pwa/assets/{html-MUMpfUBd.js → html-C28GatN_.js} +1 -1
  109. package/src/client/dist/pwa/assets/i18n-2AmHIUy0.js +1 -0
  110. package/src/client/dist/pwa/assets/index-MMYMztJ_.js +20 -0
  111. package/src/client/dist/pwa/assets/it-CF6_4OS0.js +17 -0
  112. package/src/client/dist/pwa/assets/{javascript-CHDwijEy.js → javascript-YCSueyyk.js} +1 -1
  113. package/src/client/dist/pwa/assets/{liquid-dFrnLevt.js → liquid-CUBn90Gs.js} +1 -1
  114. package/src/client/dist/pwa/assets/{mdx-BHS7JVD4.js → mdx-C5WOHShJ.js} +1 -1
  115. package/src/client/dist/pwa/assets/{monaco-contributions-DCWZe7nX.js → monaco-contributions-DYwqbh7m.js} +1 -1
  116. package/src/client/dist/pwa/assets/{monaco.contribution-Dx0gcAn4.js → monaco.contribution-CoFzrPQl.js} +2 -2
  117. package/src/client/dist/pwa/assets/network-auth-BpJ6Noax.js +1 -0
  118. package/src/client/dist/pwa/assets/notify-theme-NPq4-Tup.js +1 -0
  119. package/src/client/dist/pwa/assets/permissionModes-Bbl901Hx.js +36 -0
  120. package/src/client/dist/pwa/assets/{project-color-DihsE4yN.js → project-color-gNyhKJlV.js} +1 -1
  121. package/src/client/dist/pwa/assets/{python-BtjZnK9y.js → python-lhKUZdPj.js} +1 -1
  122. package/src/client/dist/pwa/assets/{razor-DBcOGz0a.js → razor-Chi44rEL.js} +1 -1
  123. package/src/client/dist/pwa/assets/{typescript-B320c7wU.js → typescript-CU2TXVm6.js} +1 -1
  124. package/src/client/dist/pwa/assets/{use-search-index-status-B95GPaJP.js → use-search-index-status-CQ9S05G7.js} +1 -1
  125. package/src/client/dist/pwa/assets/{use-tours-C_5R1Y5D.js → use-tours-BmrZbHjz.js} +2 -2
  126. package/src/client/dist/pwa/assets/use-worktree-restore-DcucRlLh.js +1 -0
  127. package/src/client/dist/pwa/assets/{workspace-templates-B9UETKER.js → workspace-templates-DaG42EEt.js} +1 -1
  128. package/src/client/dist/pwa/assets/{xml-uK8LK4OB.js → xml-CPO5LI6F.js} +1 -1
  129. package/src/client/dist/pwa/assets/{yaml-B2D1l_tz.js → yaml-CoYs82jI.js} +1 -1
  130. package/src/client/dist/pwa/index.html +1 -1
  131. package/src/client/dist/pwa/sw.js +1 -1
  132. package/src/mcp-server/README.md +18 -6
  133. package/src/client/dist/pwa/assets/ActivityFeed-CiTe2VUQ.js +0 -2
  134. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-Q-OlSQ1c.css +0 -1
  135. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-v3jXoH_Q.js +0 -1
  136. package/src/client/dist/pwa/assets/CreatePage-DxOes_cF.css +0 -1
  137. package/src/client/dist/pwa/assets/CreatePage-MYCexRJu.js +0 -2
  138. package/src/client/dist/pwa/assets/DiffViewer-CvsmC9d5.js +0 -8
  139. package/src/client/dist/pwa/assets/GitPanel-C1ybi9bZ.js +0 -3
  140. package/src/client/dist/pwa/assets/GitPanel-yHvU38dp.css +0 -1
  141. package/src/client/dist/pwa/assets/HealthPage-C1u35ANL.js +0 -1
  142. package/src/client/dist/pwa/assets/HealthPage-Cnlhrn--.css +0 -1
  143. package/src/client/dist/pwa/assets/MainLayout-BEAGPqei.js +0 -2
  144. package/src/client/dist/pwa/assets/QSelect-DOnv_ry0.js +0 -1
  145. package/src/client/dist/pwa/assets/SettingsPage-CFaRN9Pk.js +0 -9
  146. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-B9LE8HtU.css +0 -1
  147. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-Bkj2DhOn.js +0 -1
  148. package/src/client/dist/pwa/assets/WorkspacePage-BEBwwr81.css +0 -1
  149. package/src/client/dist/pwa/assets/WorkspacePage-BTJnap9a.js +0 -4
  150. package/src/client/dist/pwa/assets/custom-sounds-T5HKzrWh.js +0 -1
  151. package/src/client/dist/pwa/assets/de-suqWUSXa.js +0 -17
  152. package/src/client/dist/pwa/assets/es-Dcptbz5U.js +0 -17
  153. package/src/client/dist/pwa/assets/fr-Db70bqVq.js +0 -17
  154. package/src/client/dist/pwa/assets/i18n-DLprfNLs.js +0 -1
  155. package/src/client/dist/pwa/assets/index-DLoUeovT.js +0 -20
  156. package/src/client/dist/pwa/assets/it-Cloo58Nn.js +0 -17
  157. package/src/client/dist/pwa/assets/network-auth-DDMAzwYH.js +0 -1
  158. package/src/client/dist/pwa/assets/notify-theme-JuVT96rq.js +0 -1
  159. package/src/client/dist/pwa/assets/permissionModes-D-Nexnoo.js +0 -36
  160. package/src/client/dist/pwa/assets/use-worktree-restore-Cj8eGE9s.js +0 -1
@@ -0,0 +1,35 @@
1
+ import { resolve } from 'node:path';
2
+ import { buildKoboLaunchInstructions, getKoboPromptCapabilities, } from '../../../../../shared/launch-instructions.js';
3
+ import { assertMistralSelection, MISTRAL_MODES } from './configuration.js';
4
+ export function buildMistralOptions(options) {
5
+ if (options.resumeFromEngineSessionId !== undefined && !options.resumeFromEngineSessionId.trim())
6
+ throw new Error('Mistral Vibe requires a nonempty saved session identity');
7
+ const permissionMode = options.agentPermissionMode ?? 'interactive';
8
+ const capabilities = getKoboPromptCapabilities(options.mcpServers?.find((server) => server.name === 'kobo-tasks')?.env);
9
+ if (permissionMode === 'plan' || options.readOnly || capabilities.handoff)
10
+ throw new Error('Mistral Vibe does not support Plan or read-only launches');
11
+ assertMistralSelection(options.model, 'model');
12
+ assertMistralSelection(options.effort, 'thinking');
13
+ if (!Object.hasOwn(MISTRAL_MODES, permissionMode))
14
+ throw new Error('Unsupported Mistral Vibe permission mode');
15
+ const modeId = MISTRAL_MODES[permissionMode];
16
+ const koboInstructions = buildKoboLaunchInstructions({
17
+ engine: 'mistral-vibe',
18
+ permissionMode,
19
+ autoLoop: options.autoLoop,
20
+ ...capabilities,
21
+ });
22
+ return {
23
+ cwd: resolve(options.workingDir),
24
+ modeId,
25
+ koboInstructions,
26
+ mcpServers: (options.mcpServers ?? []).map((server) => ({
27
+ name: server.name,
28
+ command: server.command,
29
+ args: [...server.args],
30
+ env: Object.entries(server.env).map(([name, value]) => ({ name, value })),
31
+ })),
32
+ prompt: [{ type: 'text', text: `${koboInstructions.promptPrefix}\n\n${options.prompt}` }],
33
+ env: { ...process.env, ...options.env },
34
+ };
35
+ }
@@ -0,0 +1,278 @@
1
+ import { isOperationApprovalAvailable } from '../../../workspace-permission-policy-service.js';
2
+ import { PendingUserInputValidationError } from '../types.js';
3
+ function record(value) {
4
+ return !!value && typeof value === 'object' && !Array.isArray(value);
5
+ }
6
+ function onlyKeys(value, keys) {
7
+ return Object.keys(value).every((key) => keys.includes(key));
8
+ }
9
+ function text(value, max = 4000) {
10
+ return typeof value === 'string' && value.length <= max;
11
+ }
12
+ function bound(value, fallback, ceiling) {
13
+ if (value == null)
14
+ return fallback;
15
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= ceiling ? value : undefined;
16
+ }
17
+ function choices(schema, titledKey) {
18
+ if (schema.enum != null && schema[titledKey] != null)
19
+ return undefined;
20
+ let options;
21
+ if (Array.isArray(schema.enum) && schema.enum.every((value) => text(value, 2000))) {
22
+ options = schema.enum.map((label) => ({ label }));
23
+ }
24
+ else if (Array.isArray(schema[titledKey])) {
25
+ const values = schema[titledKey];
26
+ if (!values.every((v) => record(v) &&
27
+ onlyKeys(v, ['const', 'title', 'description', '_meta']) &&
28
+ text(v.const, 2000) &&
29
+ text(v.title, 2000) &&
30
+ (v.description == null || text(v.description))))
31
+ return undefined;
32
+ options = values.map((v) => ({ label: v.const, description: (v.description ?? v.title) }));
33
+ }
34
+ else
35
+ return undefined;
36
+ if (!options.length || options.length > 32 || new Set(options.map((o) => o.label)).size !== options.length)
37
+ return undefined;
38
+ return options;
39
+ }
40
+ function formFields(schema) {
41
+ if (!record(schema) ||
42
+ !onlyKeys(schema, ['type', 'title', 'description', 'properties', 'required', '_meta']) ||
43
+ (schema.type != null && schema.type !== 'object') ||
44
+ !record(schema.properties))
45
+ return undefined;
46
+ const entries = Object.entries(schema.properties);
47
+ if (!entries.length || entries.length > 20)
48
+ return undefined;
49
+ const required = schema.required ?? [];
50
+ if (!Array.isArray(required) ||
51
+ required.some((key) => typeof key !== 'string' || !Object.hasOwn(schema.properties, key)) ||
52
+ new Set(required).size !== required.length)
53
+ return undefined;
54
+ const fields = [];
55
+ for (const [id, property] of entries) {
56
+ if (!id ||
57
+ !text(id, 200) ||
58
+ !record(property) ||
59
+ (property.title != null && !text(property.title)) ||
60
+ (property.description != null && !text(property.description)))
61
+ return undefined;
62
+ const multiSelect = property.type === 'array';
63
+ if (property.type !== 'string' && !multiSelect)
64
+ return undefined;
65
+ if (!onlyKeys(property, multiSelect
66
+ ? ['type', 'title', 'description', 'items', 'minItems', 'maxItems', 'default', '_meta']
67
+ : [
68
+ 'type',
69
+ 'title',
70
+ 'description',
71
+ 'minLength',
72
+ 'maxLength',
73
+ 'enum',
74
+ 'oneOf',
75
+ 'default',
76
+ 'pattern',
77
+ 'format',
78
+ '_meta',
79
+ ]))
80
+ return undefined;
81
+ // Pattern/format constraints need native validation support before they can be rendered safely.
82
+ if (property.pattern != null || property.format != null)
83
+ return undefined;
84
+ let options = [];
85
+ if (multiSelect) {
86
+ if (!record(property.items) ||
87
+ !onlyKeys(property.items, ['type', 'enum', 'anyOf', '_meta']) ||
88
+ (property.items.type != null && property.items.type !== 'string'))
89
+ return undefined;
90
+ const parsed = choices(property.items, 'anyOf');
91
+ if (!parsed)
92
+ return undefined;
93
+ options = parsed;
94
+ }
95
+ else if (property.enum != null || property.oneOf != null) {
96
+ const parsed = choices(property, 'oneOf');
97
+ if (!parsed)
98
+ return undefined;
99
+ options = parsed;
100
+ }
101
+ const min = bound(multiSelect ? property.minItems : property.minLength, 0, multiSelect ? options.length : 10000);
102
+ const max = bound(multiSelect ? property.maxItems : property.maxLength, multiSelect ? options.length : 10000, multiSelect ? options.length : 10000);
103
+ if (min === undefined || max === undefined || min > max)
104
+ return undefined;
105
+ fields.push({
106
+ id,
107
+ question: (property.description ?? property.title ?? id),
108
+ ...(property.title ? { header: property.title } : {}),
109
+ options,
110
+ required: required.includes(id),
111
+ freeText: !multiSelect && !options.length,
112
+ multiSelect,
113
+ isOther: false,
114
+ min,
115
+ max,
116
+ });
117
+ }
118
+ return fields;
119
+ }
120
+ function formAnswer(fields, response) {
121
+ if (!record(response.answers) || (response.selections !== undefined && !record(response.selections)))
122
+ return undefined;
123
+ if (Object.keys(response.answers).some((id) => !fields.some((field) => field.id === id)) ||
124
+ Object.keys(response.selections ?? {}).some((id) => !fields.some((field) => field.id === id && field.multiSelect)))
125
+ return undefined;
126
+ const content = [];
127
+ for (const field of fields) {
128
+ const source = field.multiSelect ? response.selections : response.answers;
129
+ if (!source || !Object.hasOwn(source, field.id)) {
130
+ if (field.required || Object.hasOwn(response.answers, field.id))
131
+ return undefined;
132
+ continue;
133
+ }
134
+ const value = source[field.id];
135
+ if (field.multiSelect) {
136
+ if (!Array.isArray(value) ||
137
+ value.length < field.min ||
138
+ value.length > field.max ||
139
+ new Set(value).size !== value.length ||
140
+ value.some((v) => !text(v, 2000) || !field.options.some((o) => o.label === v)))
141
+ return undefined;
142
+ content.push([field.id, [...value]]);
143
+ }
144
+ else {
145
+ if (!text(value, field.max) ||
146
+ value.length < field.min ||
147
+ (!field.freeText && !field.options.some((o) => o.label === value)))
148
+ return undefined;
149
+ content.push([field.id, value]);
150
+ }
151
+ }
152
+ return { action: 'accept', content: Object.fromEntries(content) };
153
+ }
154
+ export function createMistralServerRequests(args) {
155
+ const pending = new Map();
156
+ const seen = new Set();
157
+ let closed = false;
158
+ function wait(key, signal, cancelled, answer, event) {
159
+ return new Promise((resolve) => {
160
+ function settle(result) {
161
+ if (!pending.delete(key))
162
+ return;
163
+ signal.removeEventListener('abort', cancel);
164
+ resolve(result);
165
+ args.onPendingChange?.(pending.size);
166
+ }
167
+ const cancel = () => settle(cancelled);
168
+ pending.set(key, {
169
+ cancel,
170
+ answer: (response) => {
171
+ const result = answer(response);
172
+ if (result === undefined)
173
+ throw new PendingUserInputValidationError();
174
+ settle(result);
175
+ return true;
176
+ },
177
+ });
178
+ seen.add(key);
179
+ signal.addEventListener('abort', cancel, { once: true });
180
+ args.onPendingChange?.(pending.size);
181
+ if (pending.has(key))
182
+ args.emit(event);
183
+ });
184
+ }
185
+ function unavailable(key, signal) {
186
+ return closed || signal.aborted || seen.has(key) || pending.size >= 32 || seen.size >= 4096;
187
+ }
188
+ return {
189
+ requestPermission({ params, signal }) {
190
+ const cancelled = { outcome: { outcome: 'cancelled' } };
191
+ if (closed || signal.aborted || args.readOnly || params.sessionId !== args.sessionId)
192
+ return cancelled;
193
+ const tool = args.getTool(params.toolCall.toolCallId);
194
+ const toolName = tool?._meta?.tool_name;
195
+ if (!tool || typeof toolName !== 'string' || !toolName.trim() || toolName.length > 200)
196
+ return cancelled;
197
+ const key = args.canonicalToolCallId(params.toolCall.toolCallId);
198
+ if (unavailable(key, signal))
199
+ return cancelled;
200
+ const offered = params.options;
201
+ if (!offered.length ||
202
+ offered.some((option) => !option.optionId) ||
203
+ new Set(offered.map((option) => option.optionId)).size !== offered.length)
204
+ return cancelled;
205
+ const allow = offered.find((option) => option.kind === 'allow_once');
206
+ const deny = offered.find((option) => option.kind === 'reject_once');
207
+ if (!allow || !deny)
208
+ return cancelled;
209
+ const payload = {
210
+ rawInput: tool.rawInput ?? null,
211
+ effectKind: tool._meta?.effect_kind ?? null,
212
+ kind: tool.kind ?? null,
213
+ operationApprovalAvailable: false,
214
+ };
215
+ payload.operationApprovalAvailable = isOperationApprovalAvailable({
216
+ engine: 'mistral-vibe',
217
+ toolName,
218
+ payload: { ...payload, operationApprovalAvailable: true },
219
+ });
220
+ const selected = (optionId) => ({ outcome: { outcome: 'selected', optionId } });
221
+ const approved = args.autoApprove?.(toolName, payload);
222
+ if (closed || signal.aborted)
223
+ return cancelled;
224
+ if (approved) {
225
+ seen.add(key);
226
+ return selected(allow.optionId);
227
+ }
228
+ return wait(key, signal, cancelled, (response) => response.kind === 'permission-allow'
229
+ ? selected(allow.optionId)
230
+ : response.kind === 'permission-deny'
231
+ ? selected(deny.optionId)
232
+ : undefined, { kind: 'session:user-input-requested', requestKind: 'permission', toolCallId: key, toolName, payload });
233
+ },
234
+ createElicitation({ params, signal, requestId }) {
235
+ if (closed || signal.aborted || args.readOnly)
236
+ return { action: 'cancel' };
237
+ if (params.mode !== 'form' ||
238
+ !('sessionId' in params) ||
239
+ params.sessionId !== args.sessionId ||
240
+ !text(params.message))
241
+ return { action: 'decline' };
242
+ const fields = formFields(params.requestedSchema);
243
+ if (!fields)
244
+ return { action: 'decline' };
245
+ const nativeId = typeof params.toolCallId === 'string' ? params.toolCallId : '';
246
+ const key = `${args.canonicalToolCallId(nativeId)}:elicitation:${typeof requestId}:${encodeURIComponent(String(requestId))}`;
247
+ if (unavailable(key, signal))
248
+ return { action: 'decline' };
249
+ return wait(key, signal, { action: 'cancel' }, (response) => response.kind === 'question-cancel'
250
+ ? { action: 'cancel' }
251
+ : response.kind === 'question'
252
+ ? formAnswer(fields, response)
253
+ : undefined, {
254
+ kind: 'session:user-input-requested',
255
+ requestKind: 'question',
256
+ toolCallId: key,
257
+ toolName: 'AskUserQuestion',
258
+ payload: {
259
+ questions: fields.map(({ min, max, ...field }) => ({
260
+ ...field,
261
+ ...(field.multiSelect ? { minItems: min, maxItems: max } : { minLength: min, maxLength: max }),
262
+ })),
263
+ },
264
+ });
265
+ },
266
+ hasPendingUserInput(toolCallId) {
267
+ return pending.has(toolCallId);
268
+ },
269
+ resolvePendingUserInput(toolCallId, response) {
270
+ return pending.get(toolCallId)?.answer(response) ?? false;
271
+ },
272
+ cancelAll() {
273
+ closed = true;
274
+ for (const entry of pending.values())
275
+ entry.cancel();
276
+ },
277
+ };
278
+ }
@@ -0,0 +1,176 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { groupIsLive } from '../../../../utils/bounded-process.js';
3
+ export function spawnVibeAcp(options) {
4
+ const child = spawn('vibe-acp', [], { ...options, detached: true, stdio: ['pipe', 'pipe', 'pipe'] });
5
+ let resolveClosed;
6
+ const closed = new Promise((resolve) => {
7
+ resolveClosed = resolve;
8
+ });
9
+ let exited = false;
10
+ let confirmed = false;
11
+ let abnormal = false;
12
+ let groupGone = false;
13
+ let checking = false;
14
+ let warned = false;
15
+ let spawnFailure;
16
+ let stopPromise;
17
+ let rejectStop;
18
+ let stderr = Buffer.alloc(0);
19
+ let poll;
20
+ const timers = new Set();
21
+ const diagnostic = (reason) => {
22
+ if (warned)
23
+ return;
24
+ warned = true;
25
+ // Native stderr may contain credentials. Only fixed classifications leave this module.
26
+ const hint = /missing api key|authentication failed/i.test(stderr.toString('utf8'))
27
+ ? '; check the native Vibe authentication configuration'
28
+ : '';
29
+ console.error(`[mistral-vibe] ${reason}${hint}; retaining runtime ownership`);
30
+ };
31
+ const schedule = (callback, ms) => {
32
+ const timer = setTimeout(() => {
33
+ timers.delete(timer);
34
+ callback();
35
+ }, ms);
36
+ timers.add(timer);
37
+ return timer;
38
+ };
39
+ const finish = () => {
40
+ confirmed = true;
41
+ clearTimeout(poll);
42
+ for (const timer of timers)
43
+ clearTimeout(timer);
44
+ timers.clear();
45
+ stderr = Buffer.alloc(0);
46
+ resolveClosed();
47
+ };
48
+ const confirm = async () => {
49
+ if (confirmed || checking || (!exited && !abnormal))
50
+ return;
51
+ checking = true;
52
+ try {
53
+ if (!child.pid || !(await groupIsLive(child.pid))) {
54
+ groupGone = true;
55
+ if (!exited)
56
+ return;
57
+ if (abnormal) {
58
+ diagnostic('Native process did not close cleanly');
59
+ rejectStop?.(new Error('Vibe ACP closed abnormally; retaining runtime ownership'));
60
+ }
61
+ else
62
+ finish();
63
+ return;
64
+ }
65
+ }
66
+ catch {
67
+ diagnostic('Cannot inspect the native process group');
68
+ }
69
+ finally {
70
+ checking = false;
71
+ }
72
+ if (!confirmed)
73
+ poll = setTimeout(() => {
74
+ void confirm();
75
+ }, 250).unref();
76
+ };
77
+ const signal = (requested) => {
78
+ if (groupGone)
79
+ return;
80
+ try {
81
+ if (child.pid) {
82
+ process.kill(-child.pid, requested);
83
+ abnormal = true;
84
+ }
85
+ }
86
+ catch (error) {
87
+ if (error.code !== 'ESRCH') {
88
+ abnormal = true;
89
+ diagnostic('Cannot signal the native process group');
90
+ }
91
+ }
92
+ void confirm();
93
+ };
94
+ child.stderr?.on('data', (chunk) => {
95
+ stderr = Buffer.from(Buffer.concat([stderr, chunk.subarray(-8192)]).subarray(-8192));
96
+ });
97
+ // A failed write is also reported by the SDK; never let a pipe error crash the server.
98
+ child.stdin?.on('error', () => { });
99
+ child.once('error', (error) => {
100
+ spawnFailure = new Error(error.code === 'ENOENT'
101
+ ? 'vibe-acp not found on PATH'
102
+ : 'Cannot start vibe-acp; check its executable and working directory');
103
+ if (!child.pid)
104
+ finish();
105
+ else {
106
+ abnormal = true;
107
+ diagnostic('Native process error');
108
+ }
109
+ });
110
+ child.once('close', (code, exitSignal) => {
111
+ exited = true;
112
+ abnormal ||= code !== 0 || exitSignal !== null;
113
+ void confirm();
114
+ });
115
+ const stop = (cooperate) => {
116
+ if (stopPromise)
117
+ return stopPromise;
118
+ stopPromise = (async () => {
119
+ if (spawnFailure)
120
+ throw spawnFailure;
121
+ if (confirmed)
122
+ return;
123
+ if (groupGone && abnormal)
124
+ throw new Error('Vibe ACP closed abnormally; retaining runtime ownership');
125
+ if (cooperate && !exited) {
126
+ let closeTimer;
127
+ try {
128
+ // A stalled close ACK must not consume the subsequent EOF cleanup allowance.
129
+ await Promise.race([
130
+ cooperate(),
131
+ closed,
132
+ new Promise((resolve) => {
133
+ closeTimer = schedule(resolve, 5000);
134
+ }),
135
+ ]);
136
+ }
137
+ catch {
138
+ // EOF is the native shared-host cleanup path even if cancel/close failed.
139
+ }
140
+ finally {
141
+ clearTimeout(closeTimer);
142
+ if (closeTimer)
143
+ timers.delete(closeTimer);
144
+ }
145
+ }
146
+ if (spawnFailure)
147
+ throw spawnFailure;
148
+ if (confirmed)
149
+ return;
150
+ child.stdin?.end();
151
+ child.stdout?.resume();
152
+ schedule(() => {
153
+ if (!confirmed)
154
+ signal('SIGTERM');
155
+ }, 15000);
156
+ schedule(() => {
157
+ if (!confirmed)
158
+ signal('SIGKILL');
159
+ }, 20000);
160
+ await Promise.race([
161
+ closed,
162
+ new Promise((_resolve, reject) => {
163
+ rejectStop = reject;
164
+ schedule(() => {
165
+ diagnostic('Native shutdown could not be confirmed');
166
+ reject(new Error('Vibe ACP shutdown unconfirmed; retaining runtime ownership'));
167
+ }, 25000);
168
+ }),
169
+ ]);
170
+ if (spawnFailure)
171
+ throw spawnFailure;
172
+ })();
173
+ return stopPromise;
174
+ };
175
+ return { child, closed, stop };
176
+ }
@@ -1,8 +1,10 @@
1
1
  import { createClaudeCodeEngine } from './claude-code/engine.js';
2
2
  import { createCodexEngine } from './codex/engine.js';
3
+ import { createMistralVibeEngine } from './mistral-vibe/engine.js';
3
4
  const ENGINES = {
4
5
  'claude-code': createClaudeCodeEngine(),
5
6
  codex: createCodexEngine(),
7
+ 'mistral-vibe': createMistralVibeEngine(),
6
8
  };
7
9
  export function listEngines() {
8
10
  return Object.values(ENGINES);
@@ -11,6 +11,12 @@
11
11
  * anchor; it is not itself part of the `AgentEngine` interface.
12
12
  */
13
13
  export const AGENT_NO_LONGER_RUNNING_TEXT = 'agent is no longer running';
14
+ export class PendingUserInputValidationError extends Error {
15
+ constructor() {
16
+ super('Invalid response for pending user input');
17
+ this.name = 'PendingUserInputValidationError';
18
+ }
19
+ }
14
20
  /** Every AgentEvent kind, as a const for exhaustive iteration in tests. */
15
21
  export const ALL_AGENT_EVENT_KINDS = [
16
22
  'session:started',