@opencoven/coven-code 0.0.1 → 0.0.3

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 (47) hide show
  1. package/README.md +2 -1
  2. package/docs/CLI.md +65 -1
  3. package/docs/DEMO.md +453 -0
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/README.md +1 -0
  6. package/package.json +7 -6
  7. package/src/agent/{local.mjs → fixture.mjs} +1 -1
  8. package/src/cli/execute.mjs +6 -4
  9. package/src/cli/interactive-core.mjs +5 -279
  10. package/src/cli/interactive-io.mjs +101 -0
  11. package/src/cli/interactive-slash.mjs +184 -0
  12. package/src/cli/repl.mjs +1 -2
  13. package/src/cli/slash-commands.mjs +20 -2
  14. package/src/cli/tui-actions.mjs +72 -0
  15. package/src/cli/tui-blessed.mjs +198 -0
  16. package/src/cli/tui-keys.mjs +80 -0
  17. package/src/cli/tui-lane.mjs +73 -0
  18. package/src/cli/tui-render.mjs +169 -0
  19. package/src/cli/tui-submit.mjs +82 -0
  20. package/src/cli/tui.mjs +30 -613
  21. package/src/commands/permissions-eval.mjs +122 -0
  22. package/src/commands/permissions-rules.mjs +53 -0
  23. package/src/commands/permissions-text.mjs +112 -0
  24. package/src/commands/permissions.mjs +15 -281
  25. package/src/commands/usage.mjs +1 -1
  26. package/src/constants.mjs +7 -1
  27. package/src/mcp/local.mjs +55 -0
  28. package/src/mcp/parsers.mjs +46 -0
  29. package/src/mcp/probe.mjs +12 -351
  30. package/src/mcp/remote-oauth.mjs +55 -0
  31. package/src/mcp/remote-session.mjs +54 -0
  32. package/src/mcp/remote-sse.mjs +82 -0
  33. package/src/mcp/remote.mjs +74 -0
  34. package/src/plugins/api.mjs +187 -0
  35. package/src/plugins/configuration.mjs +124 -0
  36. package/src/plugins/discover.mjs +8 -804
  37. package/src/plugins/helpers.mjs +187 -0
  38. package/src/plugins/subsystems.mjs +198 -0
  39. package/src/plugins/validators.mjs +142 -0
  40. package/src/sdk-execute.mjs +82 -0
  41. package/src/sdk-settings.mjs +88 -0
  42. package/src/sdk.mjs +13 -164
  43. package/src/tools/builtin/oracle.mjs +2 -2
  44. package/src/tools/builtin/runtime-content.mjs +31 -0
  45. package/src/tools/builtin/runtime-decisions.mjs +115 -0
  46. package/src/tools/builtin/runtime.mjs +18 -148
  47. package/src/tools/builtin/task.mjs +2 -2
@@ -1,20 +1,15 @@
1
- import { spawnSync } from 'node:child_process';
2
1
  import { existsSync, readdirSync } from 'node:fs';
3
2
  import path from 'node:path';
4
- import { fileURLToPath, pathToFileURL } from 'node:url';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { configDir, findProjectRoot } from '../settings/paths.mjs';
5
5
  import {
6
- SETTINGS_PREFIX,
7
- readEffectiveSettings,
8
- readSettings,
9
- readSettingsFile,
10
- writeSettings,
11
- writeSettingsFile,
12
- } from '../settings/load.mjs';
13
- import { configDir, findProjectRoot, workspaceSettingsFile } from '../settings/paths.mjs';
14
- import { latestActiveThread, readThread, writeThread } from '../threads/store.mjs';
15
- import { shellQuote } from '../util/shell.mjs';
6
+ createPluginApi,
7
+ createPluginCommandContext,
8
+ createPluginContext,
9
+ createPluginToolContext,
10
+ } from './api.mjs';
16
11
 
17
- const PLUGIN_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
12
+ export { createPluginCommandContext, createPluginToolContext };
18
13
 
19
14
  export function discoverPluginFiles(cwd) {
20
15
  const projectRoot = findProjectRoot(cwd);
@@ -87,794 +82,3 @@ export async function runPluginEventHandlers(handlers = [], event = {}, validate
87
82
  }
88
83
  return decision ?? { action: 'allow' };
89
84
  }
90
-
91
- export function createPluginCommandContext(cwd = process.cwd()) {
92
- const notifications = [];
93
- const context = {
94
- ui: {
95
- async notify(message) {
96
- notifications.push(validatePluginNotifyMessage(message));
97
- },
98
- confirm: async (options) => pluginEnvConfirm(options),
99
- input: async (options) => pluginEnvInput(options),
100
- select: async (options) => pluginEnvSelection(options),
101
- },
102
- system: createPluginSystem((target) => {
103
- notifications.push(String(target));
104
- }),
105
- ai: createPluginAI(),
106
- $: (strings, ...values) => runPluginShell(cwd, strings, values),
107
- thread: createPluginThreadContext(),
108
- };
109
- Object.defineProperty(context, 'notifications', {
110
- value: notifications,
111
- enumerable: false,
112
- });
113
- return context;
114
- }
115
-
116
- export function createPluginToolContext() {
117
- return {
118
- ui: {
119
- notify: async (message) => {
120
- validatePluginNotifyMessage(message);
121
- },
122
- confirm: async (options) => pluginEnvConfirm(options),
123
- input: async (options) => pluginEnvInput(options),
124
- select: async (options) => pluginEnvSelection(options),
125
- },
126
- logger: createPluginLogger(),
127
- };
128
- }
129
-
130
- function createPluginApi({ cwd, runtime }) {
131
- return {
132
- registerTool(tool) {
133
- const summary = runtime.currentPlugin;
134
- validatePluginToolName(tool?.name);
135
- validatePluginToolDefinition(tool);
136
- const registeredTool = { ...tool };
137
- if (tool?.name) summary?.tools.push(tool.name);
138
- runtime.tools.push(registeredTool);
139
- return createSubscription(() => {
140
- runtime.tools = runtime.tools.filter((entry) => entry !== registeredTool);
141
- if (tool?.name && summary) removeFirst(summary.tools, tool.name);
142
- });
143
- },
144
- registerCommand(name, metadata = {}, handler = async () => undefined) {
145
- const summary = runtime.currentPlugin;
146
- validatePluginCommand(name, metadata);
147
- summary?.commands.push(name);
148
- const commandMetadata = {
149
- ...metadata,
150
- category: metadata.category ?? summary?.name,
151
- };
152
- const command = {
153
- name,
154
- metadata: commandMetadata,
155
- handler,
156
- availability: validateCommandAvailability(name, commandMetadata.availability ?? { type: 'enabled' }),
157
- };
158
- runtime.commands.push(command);
159
- return {
160
- setAvailability(availability) {
161
- command.availability = validateCommandAvailability(name, availability);
162
- },
163
- ...createSubscription(() => {
164
- runtime.commands = runtime.commands.filter((entry) => entry !== command);
165
- if (summary) removeFirst(summary.commands, name);
166
- }),
167
- };
168
- },
169
- on(eventName, handler) {
170
- const summary = runtime.currentPlugin;
171
- summary?.events.push(eventName);
172
- if (!runtime.handlers[eventName]) runtime.handlers[eventName] = [];
173
- runtime.handlers[eventName].push(handler);
174
- return createSubscription(() => {
175
- runtime.handlers[eventName] = (runtime.handlers[eventName] ?? []).filter((entry) => entry !== handler);
176
- if (summary) removeFirst(summary.events, eventName);
177
- });
178
- },
179
- configuration: createPluginConfigurationApi(cwd, runtime),
180
- ai: createPluginAI(),
181
- helpers: {
182
- shellCommandFromToolCall(event = {}) {
183
- if (event.tool !== 'Bash' && event.tool !== 'shell_command') return null;
184
- const command = event.input?.command ?? event.input?.cmd;
185
- if (typeof command !== 'string' || !command) return null;
186
- const dir = typeof event.input?.dir === 'string' && event.input.dir ? event.input.dir : undefined;
187
- return dir ? { command, dir } : { command };
188
- },
189
- toolCallsInMessages(messages = []) {
190
- return toolCallsInMessages(messages);
191
- },
192
- filesModifiedByToolCall(event = {}) {
193
- const files = filesModifiedByToolCall(event);
194
- return files ? files.map((filePath) => pathToFileURL(filePath)) : null;
195
- },
196
- filePathFromURI(uri) {
197
- return filePathFromURI(uri);
198
- },
199
- isPluginUINotAvailableError(error) {
200
- return isPluginUINotAvailableError(error);
201
- },
202
- },
203
- logger: createPluginLogger(),
204
- system: createPluginSystem((target) => {
205
- runtime.notifications.push(String(target));
206
- }),
207
- ui: {
208
- async notify(message) {
209
- runtime.notifications.push(validatePluginNotifyMessage(message));
210
- },
211
- confirm: async (options) => pluginEnvConfirm(options),
212
- input: async (options) => pluginEnvInput(options),
213
- select: async (options) => pluginEnvSelection(options),
214
- },
215
- $: (strings, ...values) => runPluginShell(cwd, strings, values),
216
- experimental: createPluginExperimentalApi(runtime),
217
- };
218
- }
219
-
220
- function createSubscription(dispose) {
221
- return {
222
- unsubscribe: dispose,
223
- };
224
- }
225
-
226
- function createPluginLogger() {
227
- return {
228
- log() {},
229
- };
230
- }
231
-
232
- function removeFirst(entries, value) {
233
- const index = entries.indexOf(value);
234
- if (index >= 0) entries.splice(index, 1);
235
- }
236
-
237
- function validatePluginToolName(name) {
238
- if (typeof name !== 'string' || !PLUGIN_TOOL_NAME_PATTERN.test(name)) {
239
- throw new Error(`plugin tool name must match ^[a-zA-Z0-9_-]+$: ${String(name ?? '')}`);
240
- }
241
- }
242
-
243
- function validatePluginToolDefinition(tool) {
244
- const name = String(tool?.name ?? '');
245
- if (typeof tool?.description !== 'string' || tool.description.trim() === '') {
246
- throw new Error(`plugin tool description is required: ${name}`);
247
- }
248
- if (tool?.inputSchema?.type !== 'object') {
249
- throw new Error(`plugin tool inputSchema.type must be object: ${name}`);
250
- }
251
- if (
252
- tool.inputSchema.properties !== undefined &&
253
- (!isPlainObject(tool.inputSchema.properties) ||
254
- Object.values(tool.inputSchema.properties).some((property) => !isPlainObject(property)))
255
- ) {
256
- throw new Error(`plugin tool inputSchema.properties values must be objects: ${name}`);
257
- }
258
- if (
259
- tool.inputSchema.required !== undefined &&
260
- (!Array.isArray(tool.inputSchema.required) ||
261
- tool.inputSchema.required.some((propertyName) => typeof propertyName !== 'string'))
262
- ) {
263
- throw new Error(`plugin tool inputSchema.required must be strings: ${name}`);
264
- }
265
- if (typeof tool?.execute !== 'function') {
266
- throw new Error(`plugin tool execute handler is required: ${name}`);
267
- }
268
- }
269
-
270
- function isPlainObject(value) {
271
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
272
- }
273
-
274
- function validatePluginCommand(name, metadata) {
275
- if (typeof metadata?.title !== 'string' || metadata.title.trim() === '') {
276
- throw new Error(`plugin command title is required: ${String(name ?? '')}`);
277
- }
278
- if (metadata.category !== undefined && typeof metadata.category !== 'string') {
279
- throw new Error(`plugin command category must be a string: ${String(name ?? '')}`);
280
- }
281
- if (metadata.description !== undefined && typeof metadata.description !== 'string') {
282
- throw new Error(`plugin command description must be a string: ${String(name ?? '')}`);
283
- }
284
- }
285
-
286
- function validateCommandAvailability(commandName, availability) {
287
- const type = availability?.type;
288
- if (type !== 'enabled' && type !== 'disabled' && type !== 'hidden') {
289
- throw new Error(`plugin command availability must be enabled, disabled, or hidden: ${String(commandName ?? '')}`);
290
- }
291
- if (type === 'disabled' && typeof availability.reason !== 'string') {
292
- throw new Error(`plugin command disabled availability reason is required: ${String(commandName ?? '')}`);
293
- }
294
- const keys = Object.keys(availability);
295
- const allowedKeys = type === 'disabled' ? ['reason', 'type'] : ['type'];
296
- if (keys.some((key) => !allowedKeys.includes(key))) {
297
- throw new Error(`plugin command availability fields must match the documented union: ${String(commandName ?? '')}`);
298
- }
299
- return availability;
300
- }
301
-
302
- function createPluginSystem(recordOpen = () => {}) {
303
- const covenCodeURL = pluginCovenCodeURL();
304
- return {
305
- open: async (target) => {
306
- validatePluginSystemOpenTarget(target);
307
- recordOpen(target);
308
- },
309
- covenCodeURL,
310
- executor: { kind: 'local' },
311
- };
312
- }
313
-
314
- function validatePluginSystemOpenTarget(target) {
315
- if (typeof target !== 'string' && !(target instanceof URL)) {
316
- throw new Error('plugin system open target must be a string or URL');
317
- }
318
- }
319
-
320
- function pluginCovenCodeURL() {
321
- try {
322
- return new URL(process.env.COVEN_CODE_URL || 'https://coven-code.local');
323
- } catch {
324
- return new URL('https://coven-code.local');
325
- }
326
- }
327
-
328
- function createPluginExperimentalApi(runtime) {
329
- return {
330
- createStatusItem(initial) {
331
- const item = { value: validatePluginStatusItemValue(initial) };
332
- runtime.statusItems.push(item);
333
- return {
334
- update(value) {
335
- item.value = validatePluginStatusItemValue(value);
336
- },
337
- ...createSubscription(() => {
338
- runtime.statusItems = runtime.statusItems.filter((entry) => entry !== item);
339
- }),
340
- };
341
- },
342
- activeThread: createActiveThreadObservable(),
343
- };
344
- }
345
-
346
- function validatePluginStatusItemValue(value) {
347
- if (value === undefined) return undefined;
348
- for (const key of Object.keys(value ?? {})) {
349
- if (key !== 'text' && key !== 'url') {
350
- throw new Error('plugin status item fields must be text or url');
351
- }
352
- }
353
- if (typeof value?.text !== 'string' || value.text.trim() === '') {
354
- throw new Error('plugin status item text is required');
355
- }
356
- if (value.url !== undefined && typeof value.url !== 'string') {
357
- throw new Error('plugin status item url must be a string');
358
- }
359
- return value;
360
- }
361
-
362
- function createActiveThreadObservable() {
363
- const subscribers = [];
364
- const observableSymbol = Symbol.observable ?? Symbol.for('observable');
365
- const observable = {
366
- get current() {
367
- const thread = latestActiveThread();
368
- return thread ? { id: thread.id } : null;
369
- },
370
- subscribe(observer) {
371
- validateObservableSubscriber(observer);
372
- subscribers.push(observer);
373
- return createSubscription(() => {
374
- const index = subscribers.indexOf(observer);
375
- if (index >= 0) subscribers.splice(index, 1);
376
- });
377
- },
378
- pipe(op) {
379
- return op(observable);
380
- },
381
- [observableSymbol]() {
382
- return observable;
383
- },
384
- };
385
- return observable;
386
- }
387
-
388
- function createPluginConfigurationApi(cwd, runtime) {
389
- const observableSymbol = Symbol.observable ?? Symbol.for('observable');
390
- const api = {
391
- async get() {
392
- return pluginConfiguration(readEffectiveSettings());
393
- },
394
- async update(patch = {}, scope = 'workspace') {
395
- const normalizedPatch = normalizePluginConfigurationPatch(patch);
396
- const target = normalizePluginConfigurationTarget(scope);
397
- if (target === 'workspace') {
398
- const filePath = workspaceSettingsFile(findProjectRoot(cwd));
399
- await writeSettingsFile(filePath, { ...readSettingsFile(filePath), ...normalizedPatch });
400
- } else {
401
- await writeSettings({ ...readSettings(), ...normalizedPatch });
402
- }
403
- await notifyConfigurationSubscribers(runtime);
404
- },
405
- async delete(key, scope = 'workspace') {
406
- const normalizedKey = normalizePluginConfigurationKey(key);
407
- const alternateKey = alternatePluginConfigurationKey(normalizedKey);
408
- const target = normalizePluginConfigurationTarget(scope);
409
- if (target === 'workspace') {
410
- const filePath = workspaceSettingsFile(findProjectRoot(cwd));
411
- const settings = readSettingsFile(filePath);
412
- delete settings[normalizedKey];
413
- delete settings[alternateKey];
414
- await writeSettingsFile(filePath, settings);
415
- } else {
416
- const settings = readSettings();
417
- delete settings[normalizedKey];
418
- delete settings[alternateKey];
419
- await writeSettings(settings);
420
- }
421
- await notifyConfigurationSubscribers(runtime);
422
- },
423
- subscribe(handler) {
424
- validateObservableSubscriber(handler);
425
- runtime.configurationSubscribers.push(handler);
426
- return createSubscription(() => {
427
- runtime.configurationSubscribers = runtime.configurationSubscribers.filter((entry) => entry !== handler);
428
- });
429
- },
430
- pipe(op) {
431
- return op(api);
432
- },
433
- [observableSymbol]() {
434
- return api;
435
- },
436
- };
437
- return api;
438
- }
439
-
440
- function pluginConfiguration(settings = {}) {
441
- const config = { ...settings };
442
- for (const [key, value] of Object.entries(settings)) {
443
- if (key.startsWith(SETTINGS_PREFIX) && !Object.hasOwn(config, key.slice(SETTINGS_PREFIX.length))) {
444
- config[key.slice(SETTINGS_PREFIX.length)] = value;
445
- }
446
- }
447
- return config;
448
- }
449
-
450
- function normalizePluginConfigurationPatch(patch = {}) {
451
- if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
452
- throw new Error('plugin configuration update patch must be an object');
453
- }
454
- return Object.fromEntries(Object.entries(patch).map(([key, value]) => [
455
- normalizePluginConfigurationKey(key),
456
- value,
457
- ]));
458
- }
459
-
460
- function normalizePluginConfigurationKey(key) {
461
- if (typeof key !== 'string') {
462
- throw new Error('plugin configuration key must be a string');
463
- }
464
- const text = String(key);
465
- if (text.startsWith(SETTINGS_PREFIX)) return text;
466
- return `${SETTINGS_PREFIX}${text}`;
467
- }
468
-
469
- function alternatePluginConfigurationKey(key) {
470
- return key;
471
- }
472
-
473
- function normalizePluginConfigurationTarget(target = 'workspace') {
474
- if (target !== 'workspace' && target !== 'global') {
475
- throw new Error(`plugin configuration target must be workspace or global: ${String(target)}`);
476
- }
477
- return target;
478
- }
479
-
480
- async function notifyConfigurationSubscribers(runtime) {
481
- if (runtime.configurationSubscribers.length === 0) return;
482
- const config = pluginConfiguration(readEffectiveSettings());
483
- for (const subscriber of runtime.configurationSubscribers) {
484
- await notifyObservableSubscriber(subscriber, config);
485
- }
486
- }
487
-
488
- async function notifyObservableSubscriber(subscriber, value) {
489
- if (typeof subscriber === 'function') {
490
- await subscriber(value);
491
- return;
492
- }
493
- if (typeof subscriber?.next === 'function') {
494
- await subscriber.next(value);
495
- }
496
- }
497
-
498
- function validateObservableSubscriber(subscriber) {
499
- if (typeof subscriber === 'function') return;
500
- if (subscriber && typeof subscriber === 'object' && !Array.isArray(subscriber)) return;
501
- throw new Error('plugin observable subscriber must be a function or observer object');
502
- }
503
-
504
- function createPluginContext(event = {}) {
505
- return {
506
- logger: createPluginLogger(),
507
- $: (strings, ...values) => runPluginShell(process.cwd(), strings, values),
508
- ui: {
509
- async notify(message) {
510
- validatePluginNotifyMessage(message);
511
- },
512
- confirm: async (options) => {
513
- validatePluginConfirmOptions(options);
514
- return false;
515
- },
516
- input: async (options) => {
517
- validatePluginInputOptions(options);
518
- return undefined;
519
- },
520
- select: async (options) => {
521
- validatePluginSelectOptions(options);
522
- return undefined;
523
- },
524
- },
525
- ai: createPluginAI(),
526
- system: createPluginSystem(),
527
- thread: createPluginThreadContext(event.thread?.id),
528
- };
529
- }
530
-
531
- function createPluginAI() {
532
- return {
533
- ask: async (question) => {
534
- if (typeof question !== 'string') {
535
- throw new Error('plugin ai.ask question must be a string');
536
- }
537
- return classifyPluginQuestion(question);
538
- },
539
- };
540
- }
541
-
542
- function classifyPluginQuestion(question = '') {
543
- const text = String(question).toLowerCase();
544
- const matches = [
545
- ['deploy', /\bdeploy(?:ment)?\b/],
546
- ['production', /\b(?:prod|production)\b/],
547
- ['destructive', /\b(?:destroy|delete|remove|reset|force push|drop)\b/],
548
- ['risky', /\b(?:risky|danger|unsafe|secret|password|token)\b/],
549
- ].filter(([, pattern]) => pattern.test(text)).map(([label]) => label);
550
-
551
- if (matches.length > 0) {
552
- return {
553
- result: 'yes',
554
- probability: 0.9,
555
- reason: `local keyword classifier matched: ${matches.join(', ')}`,
556
- };
557
- }
558
-
559
- if (/\b(?:maybe|unclear|uncertain|unknown|not sure)\b/.test(text)) {
560
- return {
561
- result: 'uncertain',
562
- probability: 0.5,
563
- reason: 'local keyword classifier found ambiguity markers',
564
- };
565
- }
566
-
567
- return {
568
- result: 'no',
569
- probability: 0.1,
570
- reason: 'local keyword classifier found no risky keywords',
571
- };
572
- }
573
-
574
- function createPluginThreadContext(threadId) {
575
- const thread = threadId ? readThread(threadId) ?? createPendingThread(threadId) : latestActiveThread();
576
- if (!thread) return undefined;
577
- return {
578
- id: thread.id,
579
- append: async (entries = []) => {
580
- const messages = pluginThreadEntriesToMessages(entries);
581
- if (messages.length === 0) return;
582
- thread.messages.push(...messages);
583
- thread.updatedAt = new Date().toISOString();
584
- await writeThread(thread);
585
- },
586
- };
587
- }
588
-
589
- function createPendingThread(threadId) {
590
- const now = new Date().toISOString();
591
- return {
592
- id: threadId,
593
- title: '(pending thread)',
594
- cwd: process.cwd(),
595
- mode: 'smart',
596
- visibility: 'private',
597
- labels: [],
598
- archived: false,
599
- createdAt: now,
600
- updatedAt: now,
601
- messages: [],
602
- };
603
- }
604
-
605
- function pluginThreadEntriesToMessages(entries) {
606
- if (!Array.isArray(entries)) {
607
- throw new Error('plugin thread append expects an array of user-message entries');
608
- }
609
- return entries.map((entry = {}) => {
610
- if (entry.type !== 'user-message') {
611
- throw new Error('plugin thread append only supports user-message entries');
612
- }
613
- if (typeof entry.content !== 'string') {
614
- throw new Error('plugin thread append content must be a string');
615
- }
616
- return { role: 'user', content: entry.content };
617
- });
618
- }
619
-
620
- function pluginEnvBoolean(name) {
621
- const value = process.env[name];
622
- if (!value) return false;
623
- return ['1', 'true', 'yes', 'y', 'allow', 'confirm', 'ok'].includes(value.toLowerCase());
624
- }
625
-
626
- function pluginEnvConfirm(options) {
627
- validatePluginConfirmOptions(options);
628
- return pluginEnvBoolean('COVEN_CODE_PLUGIN_CONFIRM');
629
- }
630
-
631
- function pluginEnvInput(inputOptions) {
632
- validatePluginInputOptions(inputOptions);
633
- const value = process.env.COVEN_CODE_PLUGIN_INPUT;
634
- if (value !== undefined && value !== '') return value;
635
- if (inputOptions.initialValue !== undefined) return inputOptions.initialValue;
636
- return value;
637
- }
638
-
639
- function pluginEnvSelection(selectOptions) {
640
- validatePluginSelectOptions(selectOptions);
641
- const requested = process.env.COVEN_CODE_PLUGIN_SELECT;
642
- if (!requested) return undefined;
643
- const options = selectOptions.options;
644
- for (const option of options) {
645
- if (typeof option === 'string' && option === requested) return option;
646
- }
647
- return requested;
648
- }
649
-
650
- function validatePluginConfirmOptions(options) {
651
- if (!options || typeof options !== 'object' || Array.isArray(options)) {
652
- throw new Error('plugin confirm options must be an object');
653
- }
654
- if (typeof options?.title !== 'string' || options.title.trim() === '') {
655
- throw new Error('plugin confirm title is required');
656
- }
657
- for (const key of ['message', 'confirmButtonText']) {
658
- if (options[key] !== undefined && typeof options[key] !== 'string') {
659
- throw new Error(`plugin confirm ${key} must be a string`);
660
- }
661
- }
662
- }
663
-
664
- function validatePluginNotifyMessage(message) {
665
- if (typeof message !== 'string') {
666
- throw new Error('plugin notify message must be a string');
667
- }
668
- return message;
669
- }
670
-
671
- function validatePluginInputOptions(options) {
672
- if (!options || typeof options !== 'object' || Array.isArray(options)) {
673
- throw new Error('plugin input options must be an object');
674
- }
675
- for (const key of ['title', 'helpText', 'initialValue', 'submitButtonText']) {
676
- if (options[key] !== undefined && typeof options[key] !== 'string') {
677
- throw new Error(`plugin input ${key} must be a string`);
678
- }
679
- }
680
- }
681
-
682
- function validatePluginSelectOptions(options) {
683
- if (!options || typeof options !== 'object' || Array.isArray(options)) {
684
- throw new Error('plugin select options must be an object');
685
- }
686
- if (typeof options?.title !== 'string' || options.title.trim() === '') {
687
- throw new Error('plugin select title is required');
688
- }
689
- for (const key of ['message', 'initialValue']) {
690
- if (options[key] !== undefined && typeof options[key] !== 'string') {
691
- throw new Error(`plugin select ${key} must be a string`);
692
- }
693
- }
694
- if (!Array.isArray(options.options) || options.options.some((option) => typeof option !== 'string')) {
695
- throw new Error('plugin select options must be strings');
696
- }
697
- }
698
-
699
- function filesModifiedByToolCall(event = {}) {
700
- const input = event.input ?? {};
701
- if ((event.tool === 'edit_file' || event.tool === 'create_file' || event.tool === 'undo_edit') && input.path) {
702
- return [path.resolve(process.cwd(), String(input.path))];
703
- }
704
- if (event.tool === 'apply_patch' && typeof input.patch === 'string') {
705
- return applyPatchModifiedFiles(input.patch);
706
- }
707
- const shellCommand = event.tool === 'Bash' || event.tool === 'shell_command'
708
- ? input.command ?? input.cmd
709
- : undefined;
710
- if (typeof shellCommand === 'string') {
711
- const files = sedInPlaceModifiedFiles(shellCommand);
712
- return files.length > 0 ? files : null;
713
- }
714
- return null;
715
- }
716
-
717
- function applyPatchModifiedFiles(patch) {
718
- return [...patch.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)]
719
- .map((match) => path.resolve(process.cwd(), match[1].trim()))
720
- .filter(Boolean);
721
- }
722
-
723
- function sedInPlaceModifiedFiles(command) {
724
- const tokens = shellWords(command);
725
- if (tokens[0] !== 'sed') return [];
726
- const files = [];
727
- let sawInPlace = false;
728
- for (let index = 1; index < tokens.length; index += 1) {
729
- const token = tokens[index];
730
- if (token === '-i') {
731
- sawInPlace = true;
732
- if (tokens[index + 1] === '' || (tokens[index + 1] && !tokens[index + 1].startsWith('-') && !looksLikeSedScript(tokens[index + 1]))) {
733
- index += 1;
734
- }
735
- continue;
736
- }
737
- if (token.startsWith('-i')) {
738
- sawInPlace = true;
739
- continue;
740
- }
741
- if (!sawInPlace || token.startsWith('-') || looksLikeSedScript(token)) continue;
742
- files.push(path.resolve(process.cwd(), token));
743
- }
744
- return files;
745
- }
746
-
747
- function looksLikeSedScript(token = '') {
748
- return /^[a-zA-Z][^/|,;]*[\/|,;]/.test(token);
749
- }
750
-
751
- function shellWords(command) {
752
- const words = [];
753
- let current = '';
754
- let quote = '';
755
- let escaping = false;
756
- let tokenStarted = false;
757
- for (const char of command) {
758
- if (escaping) {
759
- current += char;
760
- escaping = false;
761
- tokenStarted = true;
762
- continue;
763
- }
764
- if (char === '\\' && quote !== "'") {
765
- escaping = true;
766
- tokenStarted = true;
767
- continue;
768
- }
769
- if (quote) {
770
- if (char === quote) quote = '';
771
- else {
772
- current += char;
773
- tokenStarted = true;
774
- }
775
- continue;
776
- }
777
- if (char === '"' || char === "'") {
778
- quote = char;
779
- tokenStarted = true;
780
- continue;
781
- }
782
- if (/\s/.test(char)) {
783
- if (tokenStarted) {
784
- words.push(current);
785
- current = '';
786
- tokenStarted = false;
787
- }
788
- continue;
789
- }
790
- current += char;
791
- tokenStarted = true;
792
- }
793
- if (tokenStarted) words.push(current);
794
- return words;
795
- }
796
-
797
- function toolCallsInMessages(messages = []) {
798
- const calls = [];
799
- const resultsById = new Map();
800
- for (const message of messages) {
801
- const blocks = Array.isArray(message.content) ? message.content : [];
802
- for (const block of blocks) {
803
- if (block?.type === 'tool_use') {
804
- calls.push({
805
- toolUseID: block.id,
806
- tool: block.name,
807
- input: block.input ?? {},
808
- });
809
- continue;
810
- }
811
- if (block?.type !== 'tool_result') continue;
812
- const toolUseID = block.toolUseID ?? block.tool_use_id;
813
- if (typeof toolUseID !== 'string' || !toolUseID) continue;
814
- const status = normalizeToolResultStatus(block);
815
- if (!status) continue;
816
- resultsById.set(toolUseID, {
817
- toolUseID,
818
- status,
819
- output: normalizeToolResultOutput(block),
820
- ...(status === 'error' && typeof block.content === 'string' ? { error: block.content } : {}),
821
- });
822
- }
823
- }
824
-
825
- return calls.flatMap((call) => {
826
- const result = resultsById.get(call.toolUseID);
827
- if (!result) return [];
828
- return [{
829
- call,
830
- result: {
831
- ...result,
832
- tool: call.tool,
833
- input: call.input,
834
- },
835
- }];
836
- });
837
- }
838
-
839
- function normalizeToolResultStatus(block = {}) {
840
- if (block.status === 'done' || block.status === 'error' || block.status === 'cancelled') return block.status;
841
- if (typeof block.is_error === 'boolean') return block.is_error ? 'error' : 'done';
842
- if (typeof block.isError === 'boolean') return block.isError ? 'error' : 'done';
843
- return undefined;
844
- }
845
-
846
- function normalizeToolResultOutput(block = {}) {
847
- if (Object.hasOwn(block, 'output')) return block.output;
848
- if (Object.hasOwn(block, 'content')) return block.content;
849
- return undefined;
850
- }
851
-
852
- function filePathFromURI(uri) {
853
- if (!uri) return undefined;
854
- try {
855
- return fileURLToPath(String(uri));
856
- } catch {
857
- return undefined;
858
- }
859
- }
860
-
861
- function isPluginUINotAvailableError(error) {
862
- if (!error || typeof error !== 'object') return false;
863
- if (error.name === 'PluginUINotAvailableError') return true;
864
- if (error.code === 'PLUGIN_UI_NOT_AVAILABLE') return true;
865
- return /\b(?:no|plugin)\s+plugin\s+ui\s+(?:is\s+)?available\b/i.test(String(error.message ?? ''))
866
- || /\bplugin\s+ui\s+(?:is\s+)?not\s+available\b/i.test(String(error.message ?? ''));
867
- }
868
-
869
- function runPluginShell(cwd, strings, values) {
870
- const command = strings.reduce((text, part, index) => {
871
- return `${text}${part}${index < values.length ? shellQuote(values[index]) : ''}`;
872
- }, '');
873
- const result = spawnSync(command, { cwd, shell: true, encoding: 'utf8' });
874
- return {
875
- exitCode: result.status ?? 0,
876
- stdout: result.stdout ?? '',
877
- stderr: result.stderr ?? '',
878
- status: result.status ?? 0,
879
- };
880
- }