praxis-agent 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -4
- package/dist/application/session-service.d.ts +5 -2
- package/dist/application/session-service.js +23 -0
- package/dist/application/subagent-service.d.ts +4 -2
- package/dist/application/subagent-service.js +4 -0
- package/dist/cli/interactive.d.ts +4 -3
- package/dist/cli/interactive.js +760 -195
- package/dist/cli/tui/claude-command-inventory.d.ts +368 -0
- package/dist/cli/tui/claude-command-inventory.js +220 -0
- package/dist/cli/tui/claude-style.d.ts +12 -14
- package/dist/cli/tui/claude-style.js +45 -67
- package/dist/cli/tui/config-dashboard.d.ts +4 -8
- package/dist/cli/tui/config-dashboard.js +4 -18
- package/dist/cli/tui/config-settings.js +1 -1
- package/dist/cli/tui/conversation-export.d.ts +1 -1
- package/dist/cli/tui/conversation-export.js +2 -11
- package/dist/cli/tui/mcp-elicitation.d.ts +75 -0
- package/dist/cli/tui/mcp-elicitation.js +497 -0
- package/dist/cli/tui/open-url.d.ts +2 -0
- package/dist/cli/tui/open-url.js +13 -0
- package/dist/cli/tui/permission-settings.d.ts +6 -0
- package/dist/cli/tui/permission-settings.js +84 -0
- package/dist/cli/tui/runtime-interactions.d.ts +2 -0
- package/dist/cli/tui/runtime-interactions.js +44 -17
- package/dist/cli/tui/slash-commands.js +3 -28
- package/dist/cli/tui/tool-permission.d.ts +36 -0
- package/dist/cli/tui/tool-permission.js +386 -0
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +28 -13
- package/dist/core/runtime.d.ts +50 -2
- package/dist/core/runtime.js +30 -3
- package/dist/extensions/claude-extension-tools.d.ts +3 -2
- package/dist/extensions/claude-extension-tools.js +15 -5
- package/dist/extensions/claude-extensions.d.ts +1 -0
- package/dist/extensions/claude-extensions.js +10 -0
- package/dist/hooks/claude-hook-tools.js +1 -1
- package/dist/mcp/claude-mcp-tools.js +12 -6
- package/dist/permissions/bash-ast.d.ts +39 -0
- package/dist/permissions/bash-ast.js +1046 -0
- package/dist/permissions/bash-normalization.d.ts +5 -0
- package/dist/permissions/bash-normalization.js +241 -0
- package/dist/permissions/bash-path-safety.d.ts +28 -0
- package/dist/permissions/bash-path-safety.js +575 -0
- package/dist/permissions/claude-permission-resolver.d.ts +7 -0
- package/dist/permissions/claude-permission-resolver.js +351 -40
- package/dist/permissions/claude-shell-permission.d.ts +4 -0
- package/dist/permissions/claude-shell-permission.js +108 -0
- package/dist/permissions/permission-updates.d.ts +16 -0
- package/dist/permissions/permission-updates.js +320 -0
- package/dist/permissions/sed-safety.d.ts +9 -0
- package/dist/permissions/sed-safety.js +172 -0
- package/dist/permissions/shell-rule-matching.d.ts +14 -0
- package/dist/permissions/shell-rule-matching.js +76 -0
- package/dist/tools/claude-interactive-tools.d.ts +10 -1
- package/dist/tools/claude-interactive-tools.js +10 -4
- package/dist/tools/local-tools.js +22 -2
- package/package.json +3 -1
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
function record(value) {
|
|
4
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function finite(value) {
|
|
7
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
function stringValue(value) {
|
|
10
|
+
return typeof value === 'string' ? value : undefined;
|
|
11
|
+
}
|
|
12
|
+
function options(schema) {
|
|
13
|
+
const titledOptions = Array.isArray(schema.oneOf)
|
|
14
|
+
? schema.oneOf
|
|
15
|
+
: Array.isArray(schema.anyOf)
|
|
16
|
+
? schema.anyOf
|
|
17
|
+
: undefined;
|
|
18
|
+
if (titledOptions) {
|
|
19
|
+
return titledOptions.flatMap((candidate) => {
|
|
20
|
+
if (!record(candidate) || typeof candidate.const !== 'string')
|
|
21
|
+
return [];
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
value: candidate.const,
|
|
25
|
+
label: typeof candidate.title === 'string'
|
|
26
|
+
? candidate.title
|
|
27
|
+
: candidate.const,
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const enumValues = Array.isArray(schema.enum)
|
|
33
|
+
? schema.enum.filter((value) => typeof value === 'string')
|
|
34
|
+
: [];
|
|
35
|
+
const enumNames = Array.isArray(schema.enumNames)
|
|
36
|
+
? schema.enumNames.filter((value) => typeof value === 'string')
|
|
37
|
+
: [];
|
|
38
|
+
return enumValues.map((value, index) => ({
|
|
39
|
+
value,
|
|
40
|
+
label: enumNames[index] ?? value,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
function fieldKind(schema) {
|
|
44
|
+
if (schema.type === 'array' && record(schema.items)) {
|
|
45
|
+
return options(schema.items).length > 0 ? 'multi-enum' : null;
|
|
46
|
+
}
|
|
47
|
+
if (options(schema).length > 0)
|
|
48
|
+
return 'enum';
|
|
49
|
+
if (schema.type === 'string')
|
|
50
|
+
return 'text';
|
|
51
|
+
if (schema.type === 'number')
|
|
52
|
+
return 'number';
|
|
53
|
+
if (schema.type === 'integer')
|
|
54
|
+
return 'integer';
|
|
55
|
+
if (schema.type === 'boolean')
|
|
56
|
+
return 'boolean';
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function validDefault(field, value) {
|
|
60
|
+
if (field.kind === 'boolean')
|
|
61
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
62
|
+
if (field.kind === 'number' || field.kind === 'integer')
|
|
63
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
64
|
+
? value
|
|
65
|
+
: undefined;
|
|
66
|
+
if (field.kind === 'multi-enum') {
|
|
67
|
+
if (!Array.isArray(value))
|
|
68
|
+
return undefined;
|
|
69
|
+
const allowed = new Set(field.options.map((option) => option.value));
|
|
70
|
+
const selected = value.filter((item) => typeof item === 'string' && allowed.has(item));
|
|
71
|
+
return selected;
|
|
72
|
+
}
|
|
73
|
+
return typeof value === 'string' ? value : undefined;
|
|
74
|
+
}
|
|
75
|
+
export function createTuiElicitationForm(requestedSchema) {
|
|
76
|
+
const properties = record(requestedSchema?.properties)
|
|
77
|
+
? requestedSchema.properties
|
|
78
|
+
: {};
|
|
79
|
+
const required = new Set(Array.isArray(requestedSchema?.required)
|
|
80
|
+
? requestedSchema.required.filter((value) => typeof value === 'string')
|
|
81
|
+
: []);
|
|
82
|
+
const fields = Object.entries(properties).flatMap(([name, value]) => {
|
|
83
|
+
if (!record(value))
|
|
84
|
+
return [];
|
|
85
|
+
const schema = value;
|
|
86
|
+
const kind = fieldKind(schema);
|
|
87
|
+
if (!kind)
|
|
88
|
+
return [];
|
|
89
|
+
const optionSchema = kind === 'multi-enum' && record(schema.items)
|
|
90
|
+
? schema.items
|
|
91
|
+
: schema;
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
name,
|
|
95
|
+
title: stringValue(schema.title) ?? name,
|
|
96
|
+
description: stringValue(schema.description),
|
|
97
|
+
required: required.has(name),
|
|
98
|
+
schema,
|
|
99
|
+
kind,
|
|
100
|
+
options: options(optionSchema),
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
});
|
|
104
|
+
const values = {};
|
|
105
|
+
const errors = {};
|
|
106
|
+
for (const field of fields) {
|
|
107
|
+
const value = validDefault(field, field.schema.default);
|
|
108
|
+
if (value !== undefined) {
|
|
109
|
+
values[field.name] = value;
|
|
110
|
+
if (['text', 'number', 'integer'].includes(field.kind)) {
|
|
111
|
+
const error = validateText(field, String(value)).error;
|
|
112
|
+
if (error)
|
|
113
|
+
errors[field.name] = error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
fields,
|
|
119
|
+
values,
|
|
120
|
+
errors,
|
|
121
|
+
focusIndex: fields.length > 0 ? 0 : fields.length,
|
|
122
|
+
expandedField: undefined,
|
|
123
|
+
optionIndex: 0,
|
|
124
|
+
typeahead: '',
|
|
125
|
+
typeaheadAt: 0,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export function focusedElicitationField(state) {
|
|
129
|
+
return state.fields[state.focusIndex];
|
|
130
|
+
}
|
|
131
|
+
export function elicitationTextValue(state) {
|
|
132
|
+
const field = focusedElicitationField(state);
|
|
133
|
+
if (!field || !['text', 'number', 'integer'].includes(field.kind))
|
|
134
|
+
return '';
|
|
135
|
+
const value = state.values[field.name];
|
|
136
|
+
return value === undefined ? '' : String(value);
|
|
137
|
+
}
|
|
138
|
+
function withError(state, name, error) {
|
|
139
|
+
const errors = { ...state.errors };
|
|
140
|
+
if (error)
|
|
141
|
+
errors[name] = error;
|
|
142
|
+
else
|
|
143
|
+
delete errors[name];
|
|
144
|
+
return { ...state, errors };
|
|
145
|
+
}
|
|
146
|
+
function validateText(field, raw) {
|
|
147
|
+
const text = raw.trim();
|
|
148
|
+
if (!text) {
|
|
149
|
+
if (field.required)
|
|
150
|
+
return { error: 'This field is required' };
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
if (field.kind === 'number' || field.kind === 'integer') {
|
|
154
|
+
const value = Number(text);
|
|
155
|
+
if (!Number.isFinite(value))
|
|
156
|
+
return { value: raw, error: 'Enter a valid number' };
|
|
157
|
+
if (field.kind === 'integer' && !Number.isInteger(value))
|
|
158
|
+
return { value: raw, error: 'Enter a whole number' };
|
|
159
|
+
const minimum = finite(field.schema.minimum);
|
|
160
|
+
const maximum = finite(field.schema.maximum);
|
|
161
|
+
const exclusiveMinimum = finite(field.schema.exclusiveMinimum);
|
|
162
|
+
const exclusiveMaximum = finite(field.schema.exclusiveMaximum);
|
|
163
|
+
if (minimum !== undefined && value < minimum)
|
|
164
|
+
return { value, error: `Must be at least ${minimum}` };
|
|
165
|
+
if (maximum !== undefined && value > maximum)
|
|
166
|
+
return { value, error: `Must be at most ${maximum}` };
|
|
167
|
+
if (exclusiveMinimum !== undefined && value <= exclusiveMinimum)
|
|
168
|
+
return { value, error: `Must be greater than ${exclusiveMinimum}` };
|
|
169
|
+
if (exclusiveMaximum !== undefined && value >= exclusiveMaximum)
|
|
170
|
+
return { value, error: `Must be less than ${exclusiveMaximum}` };
|
|
171
|
+
return { value };
|
|
172
|
+
}
|
|
173
|
+
const minLength = finite(field.schema.minLength);
|
|
174
|
+
const maxLength = finite(field.schema.maxLength);
|
|
175
|
+
if (minLength !== undefined && raw.length < minLength)
|
|
176
|
+
return { value: raw, error: `Must be at least ${minLength} characters` };
|
|
177
|
+
if (maxLength !== undefined && raw.length > maxLength)
|
|
178
|
+
return { value: raw, error: `Must be at most ${maxLength} characters` };
|
|
179
|
+
const pattern = stringValue(field.schema.pattern);
|
|
180
|
+
if (pattern) {
|
|
181
|
+
try {
|
|
182
|
+
if (!new RegExp(pattern, 'u').test(raw))
|
|
183
|
+
return { value: raw, error: 'Does not match the required format' };
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return { value: raw, error: 'Invalid schema pattern' };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const format = stringValue(field.schema.format);
|
|
190
|
+
if (format === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(text))
|
|
191
|
+
return {
|
|
192
|
+
value: raw,
|
|
193
|
+
error: 'Must be a valid email address, e.g. user@example.com',
|
|
194
|
+
};
|
|
195
|
+
if (format === 'uri') {
|
|
196
|
+
try {
|
|
197
|
+
new URL(text);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return {
|
|
201
|
+
value: raw,
|
|
202
|
+
error: 'Must be a valid URI, e.g. https://example.com',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (format === 'date' && !validIsoDate(text))
|
|
207
|
+
return { value: raw, error: 'Enter a date as YYYY-MM-DD' };
|
|
208
|
+
if (format === 'date-time' &&
|
|
209
|
+
(!/^\d{4}-\d{2}-\d{2}T/u.test(text) || Number.isNaN(Date.parse(text))))
|
|
210
|
+
return { value: raw, error: 'Enter a valid date and time' };
|
|
211
|
+
return { value: raw };
|
|
212
|
+
}
|
|
213
|
+
function validIsoDate(value) {
|
|
214
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/u.exec(value);
|
|
215
|
+
if (!match)
|
|
216
|
+
return false;
|
|
217
|
+
const year = Number(match[1]);
|
|
218
|
+
const month = Number(match[2]);
|
|
219
|
+
const day = Number(match[3]);
|
|
220
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
221
|
+
return (date.getUTCFullYear() === year &&
|
|
222
|
+
date.getUTCMonth() === month - 1 &&
|
|
223
|
+
date.getUTCDate() === day);
|
|
224
|
+
}
|
|
225
|
+
export function commitElicitationText(state, raw) {
|
|
226
|
+
const field = focusedElicitationField(state);
|
|
227
|
+
if (!field || !['text', 'number', 'integer'].includes(field.kind))
|
|
228
|
+
return state;
|
|
229
|
+
const preserveEmptyString = field.kind === 'text' &&
|
|
230
|
+
field.schema.format === undefined &&
|
|
231
|
+
raw.trim() === '' &&
|
|
232
|
+
state.values[field.name] !== undefined;
|
|
233
|
+
const validation = preserveEmptyString
|
|
234
|
+
? { value: '' }
|
|
235
|
+
: validateText(field, raw);
|
|
236
|
+
const values = { ...state.values };
|
|
237
|
+
if (validation.value === undefined)
|
|
238
|
+
delete values[field.name];
|
|
239
|
+
else
|
|
240
|
+
values[field.name] = validation.value;
|
|
241
|
+
return withError({ ...state, values }, field.name, validation.error);
|
|
242
|
+
}
|
|
243
|
+
export function moveElicitationFocus(state, direction) {
|
|
244
|
+
const count = state.fields.length + 2;
|
|
245
|
+
return {
|
|
246
|
+
...state,
|
|
247
|
+
focusIndex: (state.focusIndex + count + direction) % count,
|
|
248
|
+
expandedField: undefined,
|
|
249
|
+
optionIndex: 0,
|
|
250
|
+
typeahead: '',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
export function unsetElicitationField(state) {
|
|
254
|
+
const field = focusedElicitationField(state);
|
|
255
|
+
if (!field)
|
|
256
|
+
return state;
|
|
257
|
+
const values = { ...state.values };
|
|
258
|
+
const errors = { ...state.errors };
|
|
259
|
+
delete values[field.name];
|
|
260
|
+
delete errors[field.name];
|
|
261
|
+
return { ...state, values, errors };
|
|
262
|
+
}
|
|
263
|
+
export function toggleElicitationBoolean(state) {
|
|
264
|
+
const field = focusedElicitationField(state);
|
|
265
|
+
if (!field || field.kind !== 'boolean')
|
|
266
|
+
return state;
|
|
267
|
+
return withError({
|
|
268
|
+
...state,
|
|
269
|
+
values: {
|
|
270
|
+
...state.values,
|
|
271
|
+
[field.name]: state.values[field.name] !== true,
|
|
272
|
+
},
|
|
273
|
+
}, field.name);
|
|
274
|
+
}
|
|
275
|
+
export function expandElicitationOptions(state) {
|
|
276
|
+
const field = focusedElicitationField(state);
|
|
277
|
+
if (!field || !['enum', 'multi-enum'].includes(field.kind))
|
|
278
|
+
return state;
|
|
279
|
+
const current = state.values[field.name];
|
|
280
|
+
const optionIndex = field.kind === 'enum' && typeof current === 'string'
|
|
281
|
+
? Math.max(0, field.options.findIndex((option) => option.value === current))
|
|
282
|
+
: 0;
|
|
283
|
+
return { ...state, expandedField: field.name, optionIndex };
|
|
284
|
+
}
|
|
285
|
+
export function moveElicitationOption(state, direction) {
|
|
286
|
+
const field = focusedElicitationField(state);
|
|
287
|
+
if (!field ||
|
|
288
|
+
state.expandedField !== field.name ||
|
|
289
|
+
field.options.length === 0)
|
|
290
|
+
return state;
|
|
291
|
+
const nextIndex = state.optionIndex + direction;
|
|
292
|
+
if (nextIndex < 0)
|
|
293
|
+
return validateSelectedField({ ...state, expandedField: undefined }, field);
|
|
294
|
+
if (nextIndex >= field.options.length) {
|
|
295
|
+
return moveElicitationFocus(validateSelectedField({ ...state, expandedField: undefined }, field), 1);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
...state,
|
|
299
|
+
optionIndex: nextIndex,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
export function selectElicitationOption(state, collapse, ensureSelected = false) {
|
|
303
|
+
const field = focusedElicitationField(state);
|
|
304
|
+
const option = field?.options[state.optionIndex];
|
|
305
|
+
if (!field || !option || state.expandedField !== field.name)
|
|
306
|
+
return state;
|
|
307
|
+
if (field.kind === 'multi-enum') {
|
|
308
|
+
const selected = Array.isArray(state.values[field.name])
|
|
309
|
+
? state.values[field.name]
|
|
310
|
+
: [];
|
|
311
|
+
const next = selected.includes(option.value) && !ensureSelected
|
|
312
|
+
? selected.filter((value) => value !== option.value)
|
|
313
|
+
: selected.includes(option.value)
|
|
314
|
+
? selected
|
|
315
|
+
: [...selected, option.value];
|
|
316
|
+
const values = { ...state.values };
|
|
317
|
+
if (next.length > 0)
|
|
318
|
+
values[field.name] = next;
|
|
319
|
+
else
|
|
320
|
+
delete values[field.name];
|
|
321
|
+
return validateSelectedField({
|
|
322
|
+
...state,
|
|
323
|
+
values,
|
|
324
|
+
...(collapse ? { expandedField: undefined } : {}),
|
|
325
|
+
}, field);
|
|
326
|
+
}
|
|
327
|
+
return withError({
|
|
328
|
+
...state,
|
|
329
|
+
values: { ...state.values, [field.name]: option.value },
|
|
330
|
+
...(collapse ? { expandedField: undefined } : {}),
|
|
331
|
+
}, field.name);
|
|
332
|
+
}
|
|
333
|
+
function validateSelectedField(state, field) {
|
|
334
|
+
if (field.kind !== 'multi-enum')
|
|
335
|
+
return state;
|
|
336
|
+
const value = state.values[field.name];
|
|
337
|
+
const selected = Array.isArray(value) ? value : [];
|
|
338
|
+
const minItems = finite(field.schema.minItems);
|
|
339
|
+
const maxItems = finite(field.schema.maxItems);
|
|
340
|
+
let error;
|
|
341
|
+
if (field.required && selected.length === 0) {
|
|
342
|
+
error = 'This field is required';
|
|
343
|
+
}
|
|
344
|
+
else if (minItems !== undefined &&
|
|
345
|
+
selected.length < minItems &&
|
|
346
|
+
(selected.length > 0 || field.required)) {
|
|
347
|
+
error = `Select at least ${minItems} item${minItems === 1 ? '' : 's'}`;
|
|
348
|
+
}
|
|
349
|
+
else if (maxItems !== undefined && selected.length > maxItems) {
|
|
350
|
+
error = `Select at most ${maxItems} item${maxItems === 1 ? '' : 's'}`;
|
|
351
|
+
}
|
|
352
|
+
return withError(state, field.name, error);
|
|
353
|
+
}
|
|
354
|
+
export function typeaheadElicitationOption(state, input, now = Date.now()) {
|
|
355
|
+
const field = focusedElicitationField(state);
|
|
356
|
+
if (!field || !['enum', 'multi-enum'].includes(field.kind))
|
|
357
|
+
return state;
|
|
358
|
+
const buffer = `${now - state.typeaheadAt > 2_000 ? '' : state.typeahead}${input}`.toLowerCase();
|
|
359
|
+
const optionIndex = field.options.findIndex((option) => option.label.toLowerCase().startsWith(buffer));
|
|
360
|
+
return {
|
|
361
|
+
...state,
|
|
362
|
+
expandedField: field.name,
|
|
363
|
+
typeahead: buffer,
|
|
364
|
+
typeaheadAt: now,
|
|
365
|
+
...(optionIndex < 0 ? {} : { optionIndex }),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
export function validateTuiElicitationForm(state) {
|
|
369
|
+
let next = state;
|
|
370
|
+
for (const field of state.fields) {
|
|
371
|
+
const value = next.values[field.name];
|
|
372
|
+
let error = ['text', 'number', 'integer'].includes(field.kind)
|
|
373
|
+
? next.errors[field.name]
|
|
374
|
+
: undefined;
|
|
375
|
+
if (field.required &&
|
|
376
|
+
(value === undefined ||
|
|
377
|
+
value === '' ||
|
|
378
|
+
(Array.isArray(value) && value.length === 0))) {
|
|
379
|
+
error = 'This field is required';
|
|
380
|
+
}
|
|
381
|
+
if (field.kind === 'multi-enum' && Array.isArray(value)) {
|
|
382
|
+
const minItems = finite(field.schema.minItems);
|
|
383
|
+
const maxItems = finite(field.schema.maxItems);
|
|
384
|
+
if (minItems !== undefined && value.length < minItems)
|
|
385
|
+
error = `Select at least ${minItems} item${minItems === 1 ? '' : 's'}`;
|
|
386
|
+
else if (maxItems !== undefined && value.length > maxItems)
|
|
387
|
+
error = `Select at most ${maxItems} item${maxItems === 1 ? '' : 's'}`;
|
|
388
|
+
}
|
|
389
|
+
next = withError(next, field.name, error);
|
|
390
|
+
}
|
|
391
|
+
const firstInvalid = next.fields.findIndex((field) => next.errors[field.name]);
|
|
392
|
+
return firstInvalid < 0 ? next : { ...next, focusIndex: firstInvalid };
|
|
393
|
+
}
|
|
394
|
+
export function elicitationFormIsValid(state) {
|
|
395
|
+
return (Object.keys(state.errors).length === 0 &&
|
|
396
|
+
state.fields.every((field) => {
|
|
397
|
+
if (!field.required)
|
|
398
|
+
return true;
|
|
399
|
+
const value = state.values[field.name];
|
|
400
|
+
return (value !== undefined &&
|
|
401
|
+
value !== '' &&
|
|
402
|
+
(!Array.isArray(value) || value.length > 0));
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
function displayValue(field, value) {
|
|
406
|
+
if (value === undefined)
|
|
407
|
+
return 'not set';
|
|
408
|
+
if (Array.isArray(value))
|
|
409
|
+
return value
|
|
410
|
+
.map((item) => field.options.find((option) => option.value === item)?.label ?? item)
|
|
411
|
+
.join(', ');
|
|
412
|
+
if (typeof value === 'boolean')
|
|
413
|
+
return value ? '☒' : '☐';
|
|
414
|
+
if (field.kind === 'enum')
|
|
415
|
+
return (field.options.find((option) => option.value === value)?.label ??
|
|
416
|
+
String(value));
|
|
417
|
+
if (field.schema.format === 'date') {
|
|
418
|
+
const parts = String(value).split('-');
|
|
419
|
+
if (parts.length === 3) {
|
|
420
|
+
const date = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
|
|
421
|
+
if (!Number.isNaN(date.getTime()))
|
|
422
|
+
return date.toLocaleDateString('en-US', {
|
|
423
|
+
weekday: 'short',
|
|
424
|
+
year: 'numeric',
|
|
425
|
+
month: 'short',
|
|
426
|
+
day: 'numeric',
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (field.schema.format === 'date-time') {
|
|
431
|
+
const date = new Date(String(value));
|
|
432
|
+
if (!Number.isNaN(date.getTime()))
|
|
433
|
+
return date.toLocaleString('en-US');
|
|
434
|
+
}
|
|
435
|
+
return String(value);
|
|
436
|
+
}
|
|
437
|
+
export function McpElicitationUrl({ serverName, message, url, waiting, actionLabel, selection, screenReader, }) {
|
|
438
|
+
const title = waiting
|
|
439
|
+
? `MCP server “${serverName}” — waiting for completion`
|
|
440
|
+
: `MCP server “${serverName}” wants to open a URL`;
|
|
441
|
+
let beforeDomain = '';
|
|
442
|
+
let domain = url;
|
|
443
|
+
let afterDomain = '';
|
|
444
|
+
try {
|
|
445
|
+
const parsed = new URL(url);
|
|
446
|
+
const start = url.indexOf(parsed.hostname);
|
|
447
|
+
if (start >= 0) {
|
|
448
|
+
beforeDomain = url.slice(0, start);
|
|
449
|
+
domain = parsed.hostname;
|
|
450
|
+
afterDomain = url.slice(start + parsed.hostname.length);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
// Render malformed server input verbatim; the opener remains user-gated.
|
|
455
|
+
}
|
|
456
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: screenReader ? undefined : 'round', borderColor: "yellow", paddingX: screenReader ? 0 : 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: title }), _jsx(Text, { children: message }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [beforeDomain, _jsx(Text, { bold: true, children: domain }), afterDomain] }), waiting ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, italic: true, children: "Waiting for the server to confirm completion\u2026" }), _jsxs(Text, { children: [selection === 0 ? '›' : ' ', ' ', _jsx(Text, { bold: selection === 0, children: "Reopen URL" }), ' ', selection === 1 ? '›' : ' ', ' ', _jsx(Text, { bold: selection === 1, children: actionLabel })] })] })) : (_jsxs(Text, { children: [selection === 0 ? '›' : ' ', ' ', _jsx(Text, { bold: selection === 0, children: "Accept" }), ' ', selection === 1 ? '›' : ' ', ' ', _jsx(Text, { bold: selection === 1, children: "Decline" })] })), _jsx(Text, { dimColor: true, children: "Esc cancel \u00B7 \u2190\u2192 switch" })] }));
|
|
457
|
+
}
|
|
458
|
+
export function McpElicitationForm({ serverName, message, state, input, screenReader, }) {
|
|
459
|
+
const field = focusedElicitationField(state);
|
|
460
|
+
const maxVisibleFields = Math.max(2, Math.floor(((process.stdout.rows ?? 24) - 14) / 3));
|
|
461
|
+
const focusForWindow = Math.min(state.focusIndex, Math.max(0, state.fields.length - 1));
|
|
462
|
+
let windowStart = Math.max(0, focusForWindow - Math.floor(maxVisibleFields / 2));
|
|
463
|
+
const windowEnd = Math.min(state.fields.length, windowStart + maxVisibleFields);
|
|
464
|
+
windowStart = Math.max(0, windowEnd - maxVisibleFields);
|
|
465
|
+
const visibleFields = state.fields.slice(windowStart, windowEnd);
|
|
466
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: screenReader ? undefined : 'round', borderColor: "yellow", paddingX: screenReader ? 0 : 1, marginTop: 1, children: [_jsxs(Text, { bold: true, children: ["MCP server \u201C", serverName, "\u201D requests your input"] }), _jsx(Text, { children: message }), _jsx(Text, { children: " " }), windowStart > 0 ? (_jsxs(Text, { dimColor: true, children: [" \u2191 ", windowStart, " more above"] })) : null, visibleFields.map((candidate, visibleIndex) => {
|
|
467
|
+
const index = windowStart + visibleIndex;
|
|
468
|
+
const active = index === state.focusIndex;
|
|
469
|
+
const value = state.values[candidate.name];
|
|
470
|
+
const expanded = state.expandedField === candidate.name;
|
|
471
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: active, ...(state.errors[candidate.name]
|
|
472
|
+
? { color: 'red' }
|
|
473
|
+
: active
|
|
474
|
+
? { color: 'yellow' }
|
|
475
|
+
: {}), children: [active ? '›' : ' ', ' ', value !== undefined ? '✓' : candidate.required ? '*' : ' ', ' ', candidate.title, ":", ' ', active && ['text', 'number', 'integer'].includes(candidate.kind)
|
|
476
|
+
? input || 'Type something…'
|
|
477
|
+
: displayValue(candidate, value)] }), expanded
|
|
478
|
+
? candidate.options.map((option, optionIndex) => {
|
|
479
|
+
const selected = Array.isArray(value)
|
|
480
|
+
? value.includes(option.value)
|
|
481
|
+
: value === option.value;
|
|
482
|
+
return (_jsxs(Text, { children: [' ', optionIndex === state.optionIndex ? '›' : ' ', ' ', candidate.kind === 'multi-enum'
|
|
483
|
+
? selected
|
|
484
|
+
? '☒'
|
|
485
|
+
: '☐'
|
|
486
|
+
: selected
|
|
487
|
+
? '◉'
|
|
488
|
+
: '○', ' ', option.label] }, option.value));
|
|
489
|
+
})
|
|
490
|
+
: null, candidate.description ? (_jsxs(Text, { dimColor: true, children: [" ", candidate.description] })) : null, state.errors[candidate.name] ? (_jsxs(Text, { color: "red", children: [" ", state.errors[candidate.name]] })) : null] }, candidate.name));
|
|
491
|
+
}), windowEnd < state.fields.length ? (_jsxs(Text, { dimColor: true, children: [' ', "\u2193 ", state.fields.length - windowEnd, " more below"] })) : null, _jsxs(Text, { bold: state.focusIndex === state.fields.length, children: [state.focusIndex === state.fields.length ? '›' : ' ', " Accept"] }), _jsxs(Text, { bold: state.focusIndex === state.fields.length + 1, children: [state.focusIndex === state.fields.length + 1 ? '›' : ' ', " Decline"] }), _jsxs(Text, { dimColor: true, children: ["Esc cancel \u00B7 \u2191\u2193 navigate", field?.kind === 'boolean' ? ' · Space toggle' : '', field && ['enum', 'multi-enum'].includes(field.kind)
|
|
492
|
+
? state.expandedField
|
|
493
|
+
? ' · Space select'
|
|
494
|
+
: ' · → expand'
|
|
495
|
+
: ''] })] }));
|
|
496
|
+
}
|
|
497
|
+
//# sourceMappingURL=mcp-elicitation.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
export async function openTuiUrl(url) {
|
|
3
|
+
const command = process.platform === 'darwin'
|
|
4
|
+
? 'open'
|
|
5
|
+
: process.platform === 'win32'
|
|
6
|
+
? 'cmd'
|
|
7
|
+
: 'xdg-open';
|
|
8
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
9
|
+
await new Promise((resolvePromise) => {
|
|
10
|
+
execFile(command, args, { timeout: 10_000 }, () => resolvePromise());
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=open-url.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ClaudeResourceScope } from '../../compatibility/claude/shared-resources.js';
|
|
2
|
+
import type { PermissionUpdate } from '../../core/runtime.js';
|
|
2
3
|
export type TuiPermissionBehavior = 'allow' | 'ask' | 'deny';
|
|
3
4
|
export interface TuiPermissionRule {
|
|
4
5
|
behavior: TuiPermissionBehavior;
|
|
@@ -15,4 +16,9 @@ export declare function addTuiPermissionRule({ cwd, behavior, rule, scope, confi
|
|
|
15
16
|
configRoot?: string;
|
|
16
17
|
}): Promise<void>;
|
|
17
18
|
export declare function removeTuiPermissionRule(rule: TuiPermissionRule): Promise<void>;
|
|
19
|
+
export declare function persistTuiPermissionUpdates({ cwd, updates, configRoot, }: {
|
|
20
|
+
cwd: string;
|
|
21
|
+
updates: readonly PermissionUpdate[];
|
|
22
|
+
configRoot?: string;
|
|
23
|
+
}): Promise<void>;
|
|
18
24
|
//# sourceMappingURL=permission-settings.d.ts.map
|
|
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { loadClaudeSettings, } from '../../compatibility/claude/shared-resources.js';
|
|
5
5
|
import { writeFileAtomically } from '../../platform/atomic-write.js';
|
|
6
|
+
import { permissionRuleValueFromString, permissionRuleValueToString, } from '../../permissions/permission-updates.js';
|
|
6
7
|
function isRecord(value) {
|
|
7
8
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
9
|
}
|
|
@@ -106,4 +107,87 @@ export async function removeTuiPermissionRule(rule) {
|
|
|
106
107
|
}
|
|
107
108
|
throw new Error(`Settings changed concurrently: ${rule.path}`);
|
|
108
109
|
}
|
|
110
|
+
function destinationScope(destination) {
|
|
111
|
+
return destination === 'userSettings'
|
|
112
|
+
? 'user'
|
|
113
|
+
: destination === 'projectSettings'
|
|
114
|
+
? 'project'
|
|
115
|
+
: destination === 'localSettings'
|
|
116
|
+
? 'local'
|
|
117
|
+
: undefined;
|
|
118
|
+
}
|
|
119
|
+
export async function persistTuiPermissionUpdates({ cwd, updates, configRoot = configRootPath(), }) {
|
|
120
|
+
for (const update of updates) {
|
|
121
|
+
const scope = destinationScope(update.destination);
|
|
122
|
+
if (!scope)
|
|
123
|
+
continue;
|
|
124
|
+
if (update.type === 'addRules') {
|
|
125
|
+
for (const rule of update.rules) {
|
|
126
|
+
await addTuiPermissionRule({
|
|
127
|
+
cwd,
|
|
128
|
+
behavior: update.behavior,
|
|
129
|
+
rule: permissionRuleValueToString(rule),
|
|
130
|
+
scope,
|
|
131
|
+
configRoot,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const path = settingsPath(configRoot, cwd, scope);
|
|
137
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
138
|
+
const { value: settings, source } = await readSettings(path);
|
|
139
|
+
const permissions = isRecord(settings.permissions)
|
|
140
|
+
? { ...settings.permissions }
|
|
141
|
+
: {};
|
|
142
|
+
if (update.type === 'setMode') {
|
|
143
|
+
permissions.defaultMode = update.mode;
|
|
144
|
+
}
|
|
145
|
+
else if (update.type === 'addDirectories' ||
|
|
146
|
+
update.type === 'removeDirectories') {
|
|
147
|
+
const existing = permissions.additionalDirectories;
|
|
148
|
+
if (existing !== undefined && !Array.isArray(existing)) {
|
|
149
|
+
throw new Error(`permissions.additionalDirectories must be an array: ${path}`);
|
|
150
|
+
}
|
|
151
|
+
const directories = Array.isArray(existing)
|
|
152
|
+
? existing.filter((directory) => typeof directory === 'string')
|
|
153
|
+
: [];
|
|
154
|
+
const changed = new Set(directories);
|
|
155
|
+
for (const directory of update.directories) {
|
|
156
|
+
if (update.type === 'addDirectories')
|
|
157
|
+
changed.add(directory);
|
|
158
|
+
else
|
|
159
|
+
changed.delete(directory);
|
|
160
|
+
}
|
|
161
|
+
permissions.additionalDirectories = [...changed];
|
|
162
|
+
}
|
|
163
|
+
else if (update.type === 'replaceRules' ||
|
|
164
|
+
update.type === 'removeRules') {
|
|
165
|
+
const existing = permissions[update.behavior];
|
|
166
|
+
if (existing !== undefined && !Array.isArray(existing)) {
|
|
167
|
+
throw new Error(`permissions.${update.behavior} must be an array: ${path}`);
|
|
168
|
+
}
|
|
169
|
+
const rules = Array.isArray(existing)
|
|
170
|
+
? existing.filter((rule) => typeof rule === 'string')
|
|
171
|
+
: [];
|
|
172
|
+
const changed = update.rules.map(permissionRuleValueToString);
|
|
173
|
+
if (update.type === 'replaceRules') {
|
|
174
|
+
permissions[update.behavior] = changed;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const removed = new Set(changed);
|
|
178
|
+
permissions[update.behavior] = rules.filter((rule) => !removed.has(permissionRuleValueToString(permissionRuleValueFromString(rule))));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const unreachable = update;
|
|
183
|
+
throw new Error(`Unsupported permission update: ${JSON.stringify(unreachable)}`);
|
|
184
|
+
}
|
|
185
|
+
const committed = await writeFileAtomically(path, `${JSON.stringify({ ...settings, permissions }, null, 2)}\n`, { beforeCommit: () => sourceUnchanged(path, source) });
|
|
186
|
+
if (committed)
|
|
187
|
+
break;
|
|
188
|
+
if (attempt === 2)
|
|
189
|
+
throw new Error(`Settings changed concurrently: ${path}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
109
193
|
//# sourceMappingURL=permission-settings.js.map
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { TranscriptItem } from './claude-style.js';
|
|
2
2
|
import type { PraxisRuntimeSettings } from './runtime-settings.js';
|
|
3
3
|
export type CopyCandidate = {
|
|
4
|
+
kind: 'full' | 'code' | 'always';
|
|
4
5
|
label: string;
|
|
5
6
|
description: string;
|
|
6
7
|
text: string;
|
|
8
|
+
filename: string;
|
|
7
9
|
};
|
|
8
10
|
export declare function spinnerTip(settings: Pick<PraxisRuntimeSettings, 'tips'>): string | undefined;
|
|
9
11
|
export declare function questionTimeoutMilliseconds(value: PraxisRuntimeSettings['askUserQuestionTimeout']): number | undefined;
|