clay-server 4.7.1-beta.1 → 4.7.1-beta.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.
@@ -370,7 +370,7 @@ function onElicitation(request, opts) {
370
370
  if (opts.signal) {
371
371
  opts.signal.addEventListener("abort", function() {
372
372
  delete pendingElicitations[requestId];
373
- resolve({ action: "reject" });
373
+ resolve({ action: "cancel" });
374
374
  });
375
375
  }
376
376
  });
@@ -1079,10 +1079,13 @@ function createCodexQueryHandle(appServer, queryOpts) {
1079
1079
  onElicitation(request, {
1080
1080
  signal: { addEventListener: function() {} },
1081
1081
  }).then(function(result) {
1082
- appServer.respond(msg.id, result || { action: "reject" });
1082
+ var resultAction = result && result.action === "reject" ? "decline" : result && result.action;
1083
+ appServer.respond(msg.id, resultAction === "accept" || resultAction === "decline"
1084
+ ? Object.assign({}, result, { action: resultAction })
1085
+ : { action: "cancel" });
1083
1086
  }).catch(function(err) {
1084
1087
  console.error("[yoke/codex] elicitation_response send failed:", err.message || err);
1085
- appServer.respond(msg.id, { action: "reject" });
1088
+ appServer.respond(msg.id, { action: "cancel" });
1086
1089
  });
1087
1090
  } else if (canUseTool) {
1088
1091
  canUseTool("mcp__" + (mcpParams.serverName || "unknown") + "__" + (mcpMeta.tool || "call"), mcpParams, {}).then(function(decision) {
@@ -204,12 +204,12 @@ function flattenUpdate(update, state) {
204
204
  // Tool call announced
205
205
  if (type === "tool_call") {
206
206
  var callId = update.toolCallId;
207
- var toolName = toolNameForKind(update.kind, update.title);
207
+ var toolName = update.name || toolNameForKind(update.kind, update.title);
208
208
  // Cache kind/rawInput so the permission handler (whose request payload only
209
209
  // carries { toolCallId, title }) can pass a canonical tool name + input to
210
210
  // canUseTool. Clay's permission whitelist keys on canonical names.
211
211
  if (callId) {
212
- state.toolMeta[callId] = { kind: update.kind, title: update.title, rawInput: update.rawInput || {} };
212
+ state.toolMeta[callId] = { kind: update.kind, name: update.name, title: update.title, rawInput: update.rawInput || {} };
213
213
  }
214
214
  if (callId && !state.toolBlocks[callId]) {
215
215
  state.blockCounter++;
@@ -501,8 +501,13 @@ function createKiroQueryHandle(acp, queryOpts) {
501
501
  }
502
502
  return null;
503
503
  }
504
- var allowId = pickOption(["allow_once", "allow_always"]) || (options[0] && options[0].optionId);
505
- var rejectId = pickOption(["reject_once", "reject_always"]) || (options[options.length - 1] && options[options.length - 1].optionId);
504
+ var allowId = pickOption(["allow_once", "allow_always"]);
505
+ var rejectId = pickOption(["reject_once", "reject_always"]);
506
+ function permissionResponse(optionId) {
507
+ return optionId
508
+ ? { outcome: { outcome: "selected", optionId: optionId } }
509
+ : { outcome: { outcome: "cancelled" } };
510
+ }
506
511
 
507
512
  if (isCancelled()) {
508
513
  acp.respond(msg.id, { outcome: { outcome: "cancelled" } });
@@ -512,21 +517,21 @@ function createKiroQueryHandle(acp, queryOpts) {
512
517
  // The permission request payload carries only { toolCallId, title }.
513
518
  // Recover kind + rawInput from the tool_call notification we cached.
514
519
  var meta = (tc.toolCallId && state.toolMeta[tc.toolCallId]) || {};
515
- var toolName = toolNameForKind(tc.kind || meta.kind, tc.title || meta.title);
520
+ var toolName = tc.name || meta.name || toolNameForKind(tc.kind || meta.kind, tc.title || meta.title);
516
521
  var toolInput = tc.rawInput || meta.rawInput || { title: tc.title || meta.title };
517
522
  cancellation.wait(Promise.resolve().then(function () { cancellation.check(); return canUseTool(toolName, toolInput, { signal: cancellation.signal }); })).then(function(decision) {
518
- acp.respond(msg.id, isCancelled() ? { outcome: { outcome: "cancelled" } } : { outcome: { outcome: "selected", optionId: isApproved(decision) ? allowId : rejectId } });
523
+ acp.respond(msg.id, isCancelled() ? { outcome: { outcome: "cancelled" } } : permissionResponse(isApproved(decision) ? allowId : rejectId));
519
524
  }).catch(function(err) {
520
525
  if (isCancelled()) { acp.respond(msg.id, { outcome: { outcome: "cancelled" } }); return; }
521
526
  console.error("[yoke/kiro] canUseTool error:", err.message);
522
- acp.respond(msg.id, { outcome: { outcome: "selected", optionId: rejectId } });
527
+ acp.respond(msg.id, permissionResponse(rejectId));
523
528
  });
524
529
  } else {
525
530
  // No approver wired up. Deny: Clay is the only thing standing between
526
531
  // the agent and the user's filesystem, so absence of an approver must
527
532
  // never mean approval.
528
533
  console.warn("[yoke/kiro] permission request with no canUseTool callback, denying");
529
- acp.respond(msg.id, { outcome: { outcome: "selected", optionId: rejectId } });
534
+ acp.respond(msg.id, permissionResponse(rejectId));
530
535
  }
531
536
  return;
532
537
  }
@@ -652,12 +657,6 @@ function createKiroQueryHandle(acp, queryOpts) {
652
657
  cwd: queryOpts.cwd,
653
658
  mcpServers: queryOpts.mcpServers || [],
654
659
  }, 60000);
655
- } catch (e) {
656
- if (isCancelled()) throw e;
657
- // If load fails (unknown session), fall back to a fresh session.
658
- console.warn("[yoke/kiro] session/load failed, starting fresh:", e.message);
659
- state.sessionId = null;
660
- handlerEntry.sessionId = null;
661
660
  } finally {
662
661
  state.loadingSession = false;
663
662
  }
@@ -681,12 +680,12 @@ function createKiroQueryHandle(acp, queryOpts) {
681
680
  }, 15000);
682
681
  }
683
682
 
684
- // Select model + mode for the session (best-effort; failures are non-fatal).
683
+ // Select the requested model and mode before the prompt is sent.
685
684
  if (queryOpts.model) {
686
- await setSessionModel(queryOpts.model).catch(function() {});
685
+ await setSessionModel(queryOpts.model);
687
686
  }
688
687
  if (queryOpts.mode) {
689
- await send("session/set_mode", { sessionId: state.sessionId, modeId: queryOpts.mode }, 15000).catch(function() {});
688
+ await send("session/set_mode", { sessionId: state.sessionId, modeId: queryOpts.mode }, 15000);
690
689
  }
691
690
 
692
691
  while (!isCancelled()) {
@@ -14,15 +14,13 @@ function ask(handler, input, opts) {
14
14
 
15
15
  function elicitation(handler, request, opts) {
16
16
  opts = opts || {};
17
- return userInput.dispatchUserInput(handler, userInput.questionsFromElicitation(request), {
17
+ return userInput.dispatchElicitation(handler, request, {
18
18
  requestId: opts.requestId || request.elicitationId,
19
19
  signal: opts.signal,
20
20
  source: "claude_elicitation",
21
21
  native: true,
22
22
  provider: "claude",
23
- presentation: "elicitation",
24
- diagnostics: { elicitation: request },
25
- }).then(userInput.elicitationResponse);
23
+ }).then(function (result) { return userInput.elicitationResponse(result, request.requestedSchema); });
26
24
  }
27
25
 
28
26
  function canUseTool(queryOpts) {
@@ -45,8 +45,7 @@ function handleElicitation(appServer, msg, handler, signal) {
45
45
  elicitationId: params.elicitationId || null,
46
46
  requestedSchema: params.requestedSchema || null,
47
47
  };
48
- var input = Array.isArray(params.questions) ? nativeInput(params) : userInput.questionsFromElicitation(request);
49
- userInput.dispatchUserInput(handler, input, {
48
+ var dispatchOpts = {
50
49
  requestId: request.elicitationId || String(msg.id),
51
50
  signal: signal,
52
51
  source: "codex_mcp_elicitation",
@@ -54,8 +53,12 @@ function handleElicitation(appServer, msg, handler, signal) {
54
53
  provider: "codex",
55
54
  presentation: "elicitation",
56
55
  diagnostics: { threadId: params.threadId || null, elicitation: request },
57
- }).then(function (result) {
58
- appServer.respond(msg.id, userInput.elicitationResponse(result));
56
+ };
57
+ var pending = Array.isArray(params.questions)
58
+ ? userInput.dispatchUserInput(handler, nativeInput(params), dispatchOpts)
59
+ : userInput.dispatchElicitation(handler, request, dispatchOpts);
60
+ pending.then(function (result) {
61
+ appServer.respond(msg.id, userInput.elicitationResponse(result, request.requestedSchema));
59
62
  }).catch(function (error) {
60
63
  appServer.respondError(msg.id, -32602, error.message);
61
64
  });
@@ -0,0 +1,204 @@
1
+ // Restricted elicitation schema handling shared by ACP, Claude and Codex.
2
+ // Mirrors lib/public/modules/elicitation-schema.js; keep both in sync
3
+ // (test/elicitation-schema-parity.test.js runs one fixture table on both).
4
+
5
+ var PATTERN_MAX_LENGTH = 200;
6
+ var PATTERN_MAX_VARIABLE_QUANTIFIERS = 2;
7
+ var PATTERN_MAX_INPUT_LENGTH = 256;
8
+
9
+ function hasOwn(object, key) {
10
+ return !!object && Object.prototype.hasOwnProperty.call(object, key);
11
+ }
12
+
13
+ function constChoices(list) {
14
+ if (!Array.isArray(list)) return null;
15
+ var values = [];
16
+ for (var i = 0; i < list.length; i++) {
17
+ if (!hasOwn(list[i], "const")) return null;
18
+ values.push({ value: list[i].const, label: String(list[i].title || list[i].const), description: list[i].description || "" });
19
+ }
20
+ return values.length ? values : null;
21
+ }
22
+
23
+ function enumChoices(list) {
24
+ if (!Array.isArray(list) || !list.length) return null;
25
+ return list.map(function (value) { return { value: value, label: String(value), description: "" }; });
26
+ }
27
+
28
+ // Returns { kind, multi, choices } where kind is one of string, number,
29
+ // integer, boolean, array or unsupported. Unknown types are never rendered
30
+ // or validated as a known control.
31
+ function fieldInfo(property) {
32
+ property = property || {};
33
+ var type = property.type || (Array.isArray(property.enum) || Array.isArray(property.oneOf) ? "string" : null);
34
+ if (type === "array") {
35
+ var items = property.items || {};
36
+ var itemType = items.type || "string";
37
+ var itemChoices = itemType === "string" ? (enumChoices(items.enum) || constChoices(items.anyOf)) : null;
38
+ return itemChoices ? { kind: "array", multi: true, choices: itemChoices } : { kind: "unsupported", multi: false, choices: null };
39
+ }
40
+ if (type === "string") return { kind: "string", multi: false, choices: enumChoices(property.enum) || constChoices(property.oneOf) };
41
+ if (type === "number" || type === "integer" || type === "boolean") return { kind: type, multi: false, choices: null };
42
+ return { kind: "unsupported", multi: false, choices: null };
43
+ }
44
+
45
+ // Agent-supplied patterns are evaluated only inside a subset whose backtracking
46
+ // cost is bounded: no backreferences, lookaround or quantified groups, at most
47
+ // two variable quantifiers, and short inputs. Anything else is an unenforced hint.
48
+ function patternIsBounded(pattern) {
49
+ if (typeof pattern !== "string" || !pattern || pattern.length > PATTERN_MAX_LENGTH) return false;
50
+ var variable = 0;
51
+ var inClass = false;
52
+ for (var i = 0; i < pattern.length; i++) {
53
+ var ch = pattern[i];
54
+ if (ch === "\\") {
55
+ var next = pattern[i + 1];
56
+ if (next === undefined || /[1-9k]/.test(next)) return false;
57
+ i++;
58
+ continue;
59
+ }
60
+ if (inClass) { if (ch === "]") inClass = false; continue; }
61
+ if (ch === "[") { inClass = true; continue; }
62
+ if (ch === "(" && pattern[i + 1] === "?") {
63
+ if (pattern[i + 2] !== ":") return false;
64
+ i += 2;
65
+ continue;
66
+ }
67
+ if (ch === "*" || ch === "+" || ch === "?" || ch === "{") {
68
+ if (pattern[i - 1] === ")") return false;
69
+ if (ch === "{") {
70
+ var close = pattern.indexOf("}", i);
71
+ if (close === -1) return false;
72
+ var body = pattern.slice(i + 1, close);
73
+ if (!/^\d+(,\d*)?$/.test(body)) return false;
74
+ if (body.indexOf(",") !== -1) variable++;
75
+ i = close;
76
+ } else {
77
+ variable++;
78
+ }
79
+ if (pattern[i + 1] === "?") i++;
80
+ }
81
+ }
82
+ if (inClass || variable > PATTERN_MAX_VARIABLE_QUANTIFIERS) return false;
83
+ try { new RegExp(pattern, "u"); } catch (e) { return false; }
84
+ return true;
85
+ }
86
+
87
+ // true or false when evaluated; null when the pattern is left as a hint.
88
+ function patternMatches(pattern, value) {
89
+ if (!patternIsBounded(pattern) || value.length > PATTERN_MAX_INPUT_LENGTH) return null;
90
+ return new RegExp(pattern, "u").test(value);
91
+ }
92
+
93
+ function sameValue(left, right) {
94
+ return left === right;
95
+ }
96
+
97
+ function inChoices(choices, value) {
98
+ return choices.some(function (choice) { return sameValue(choice.value, value); });
99
+ }
100
+
101
+ // Returns an error message or null. `value` must already be typed.
102
+ function valueError(value, property) {
103
+ property = property || {};
104
+ var info = fieldInfo(property);
105
+ if (info.kind === "unsupported") return null;
106
+ if (info.kind === "boolean") return typeof value === "boolean" ? null : "Choose yes or no.";
107
+ if (info.kind === "number" || info.kind === "integer") {
108
+ if (typeof value !== "number" || !Number.isFinite(value)) return "Enter a number.";
109
+ if (info.kind === "integer" && !Number.isInteger(value)) return "Enter a whole number.";
110
+ if (typeof property.minimum === "number" && value < property.minimum) return "Must be at least " + property.minimum + ".";
111
+ if (typeof property.maximum === "number" && value > property.maximum) return "Must be at most " + property.maximum + ".";
112
+ return null;
113
+ }
114
+ if (info.kind === "array") {
115
+ if (!Array.isArray(value)) return "Choose from the listed options.";
116
+ for (var i = 0; i < value.length; i++) {
117
+ if (!inChoices(info.choices, value[i])) return "Choose only listed options.";
118
+ if (value.indexOf(value[i]) !== i) return "Choose each option once.";
119
+ }
120
+ if (typeof property.minItems === "number" && value.length < property.minItems) return "Choose at least " + property.minItems + ".";
121
+ if (typeof property.maxItems === "number" && value.length > property.maxItems) return "Choose at most " + property.maxItems + ".";
122
+ return null;
123
+ }
124
+ if (typeof value !== "string") return "Enter text.";
125
+ if (info.choices) return inChoices(info.choices, value) ? null : "Choose one of the listed options.";
126
+ var length = Array.from(value).length;
127
+ if (typeof property.minLength === "number" && length < property.minLength) return "Must be at least " + property.minLength + " characters.";
128
+ if (typeof property.maxLength === "number" && length > property.maxLength) return "Must be at most " + property.maxLength + " characters.";
129
+ if (patternMatches(property.pattern, value) === false) return "Does not match the required format.";
130
+ return null;
131
+ }
132
+
133
+ // Accepts string answers from the question-card path and returns a typed value.
134
+ function coerceValue(value, property) {
135
+ var info = fieldInfo(property);
136
+ if (info.kind === "boolean" && (value === "true" || value === "false")) return value === "true";
137
+ if ((info.kind === "number" || info.kind === "integer") && typeof value === "string" && value.trim()) return Number(value);
138
+ if (info.kind === "array" && !Array.isArray(value)) return value == null ? [] : [value];
139
+ return value;
140
+ }
141
+
142
+ // Required means presence only. Throws on the first invalid property.
143
+ function validateContent(content, schema) {
144
+ if (!content || typeof content !== "object" || Array.isArray(content)) throw new Error("Elicitation content must be an object.");
145
+ var properties = schema && schema.properties && typeof schema.properties === "object" ? schema.properties : {};
146
+ var required = schema && Array.isArray(schema.required) ? schema.required : [];
147
+ var validated = Object.assign({}, content);
148
+ for (var i = 0; i < required.length; i++) {
149
+ if (hasOwn(properties, required[i]) && !hasOwn(content, required[i])) throw new Error("An answer is required for " + required[i] + ".");
150
+ }
151
+ Object.keys(properties).forEach(function (id) {
152
+ if (!hasOwn(content, id)) return;
153
+ var value = coerceValue(content[id], properties[id]);
154
+ var error = valueError(value, properties[id]);
155
+ if (error) throw new Error("Invalid elicitation answer for " + id + ": " + error);
156
+ validated[id] = value;
157
+ });
158
+ return validated;
159
+ }
160
+
161
+ function clip(text, max) {
162
+ text = typeof text === "string" ? text.trim() : "";
163
+ return text.length > max ? text.slice(0, max - 1) + "…" : text;
164
+ }
165
+
166
+ // Provider-neutral question view of a form. Unlike structured question tools,
167
+ // forms have no property-count or text-length limits; text is only clipped.
168
+ function questionsFromSchema(request) {
169
+ var schema = request && request.requestedSchema;
170
+ var properties = schema && schema.properties && typeof schema.properties === "object" ? schema.properties : null;
171
+ var required = schema && Array.isArray(schema.required) ? schema.required : [];
172
+ var header = clip(request && request.serverName, 40) || "Input";
173
+ var questions = [];
174
+ Object.keys(properties || {}).forEach(function (id) {
175
+ var property = properties[id] || {};
176
+ var info = fieldInfo(property);
177
+ var choices = info.choices || (info.kind === "boolean" ? [{ value: true, label: "true", description: "Yes" }, { value: false, label: "false", description: "No" }] : []);
178
+ questions.push({
179
+ id: id,
180
+ header: header,
181
+ question: clip(property.description || property.title || (request && request.message) || id, 2000) || id,
182
+ multiSelect: info.multi,
183
+ allowOther: !info.choices && info.kind !== "boolean",
184
+ required: required.indexOf(id) !== -1,
185
+ secret: false,
186
+ options: choices.map(function (choice) { return { label: clip(String(choice.value), 60) || "(empty)", description: clip(choice.description || (choice.label !== String(choice.value) ? choice.label : ""), 160) }; }),
187
+ });
188
+ });
189
+ if (!questions.length) {
190
+ questions.push({ id: "response", header: header, question: clip(request && request.message, 2000) || "What should the tool use?", multiSelect: false, allowOther: true, required: true, secret: false, options: [] });
191
+ }
192
+ return questions;
193
+ }
194
+
195
+ module.exports = {
196
+ PATTERN_MAX_INPUT_LENGTH: PATTERN_MAX_INPUT_LENGTH,
197
+ fieldInfo: fieldInfo,
198
+ patternIsBounded: patternIsBounded,
199
+ patternMatches: patternMatches,
200
+ valueError: valueError,
201
+ coerceValue: coerceValue,
202
+ validateContent: validateContent,
203
+ questionsFromSchema: questionsFromSchema,
204
+ };
@@ -47,17 +47,22 @@ var INITIALIZE_TIMEOUT_MS = 90000;
47
47
  * this method may only use completeOnce's explicit unknown-persistence path.
48
48
  *
49
49
  * Optional structured user-input capability:
50
- * .userInputCapability: { mode: "native"|"fallback", native: boolean, transport: string }
50
+ * .userInputCapability: { mode: "native"|"fallback", native: boolean,
51
+ * nativeElicitation?: boolean, transport: string }
51
52
  * createQuery({ userInputMode: "auto"|"native"|"fallback",
52
53
  * onUserInputRequest(request, respond) })
53
54
  * request.presentation is "questions" or "elicitation";
54
- * request.questions contain 1-3 { id, header, question, multiSelect,
55
+ * question requests contain 1-3 { id, header, question, multiSelect,
55
56
  * allowOther, secret, options }; options contain zero or 2-6 safe
56
- * { label, description } entries. respond(answerMap),
57
- * respond.submitContent(typedMap), and respond.cancel(reason) settle
58
- * exactly once. submitContent preserves typed MCP elicitation forms.
59
- * Adapters with mode "native" translate their provider protocol directly.
60
- * Other adapters use createFallbackServer() from yoke/user-input.
57
+ * { label, description } entries. Elicitation requests carry the raw
58
+ * form in request.diagnostics.elicitation and have no question limit.
59
+ * respond(answerMap), respond.submitContent(typedMap),
60
+ * respond.decline(reason) and respond.cancel(reason) settle exactly once.
61
+ * submitContent preserves typed MCP elicitation forms.
62
+ * mode governs structured questions only: "native" adapters translate
63
+ * their provider question protocol, others use createFallbackServer() /
64
+ * fallbackToolDefs() from yoke/user-input. nativeElicitation marks
65
+ * adapters that also forward agent-initiated forms on fallback mode.
61
66
  *
62
67
  * Lightweight utilities:
63
68
  * .generateTitle(messages, opts) : Promise<string> - generate a short session title
@@ -1,6 +1,7 @@
1
1
  // Provider-neutral structured user-input lifecycle for YOKE queries.
2
2
 
3
3
  var crypto = require("crypto");
4
+ var elicitationSchema = require("./elicitation-schema");
4
5
  var z;
5
6
  try { z = require("zod"); } catch (e) { z = null; }
6
7
 
@@ -46,6 +47,7 @@ function normalizeQuestions(input) {
46
47
  question: cleanText(item.question || item.prompt, "Question", 2000),
47
48
  multiSelect: item.multiSelect === true,
48
49
  allowOther: item.allowOther !== false && item.isOther !== false,
50
+ required: item.required !== false,
49
51
  secret: item.secret === true || item.isSecret === true,
50
52
  options: normalizeOptions(item.options, i),
51
53
  });
@@ -54,23 +56,7 @@ function normalizeQuestions(input) {
54
56
  }
55
57
 
56
58
  function questionsFromElicitation(request) {
57
- var schema = request && request.requestedSchema;
58
- var properties = schema && schema.properties && typeof schema.properties === "object" ? schema.properties : null;
59
- var questions = [];
60
- if (properties) {
61
- Object.keys(properties).forEach(function (id) {
62
- var property = properties[id] || {};
63
- var values = Array.isArray(property.enum) ? property.enum : [];
64
- questions.push({
65
- id: id,
66
- header: request.serverName || "Input",
67
- question: property.description || request.message || id,
68
- options: values.map(function (value) { return { label: String(value), description: "" }; }),
69
- });
70
- });
71
- }
72
- if (!questions.length) questions.push({ id: "response", header: request.serverName || "Input", question: request.message || "What should the tool use?", options: [] });
73
- return { questions: questions };
59
+ return { questions: elicitationSchema.questionsFromSchema(request) };
74
60
  }
75
61
 
76
62
  function answerValues(value) {
@@ -86,6 +72,7 @@ function normalizeAnswers(questions, raw) {
86
72
  var question = questions[i];
87
73
  var value = Object.prototype.hasOwnProperty.call(raw, question.id) ? raw[question.id] : raw[i];
88
74
  var values = answerValues(value);
75
+ if (!values.length && question.required === false) continue;
89
76
  if (!values.length) throw new Error("An answer is required for " + question.id + ".");
90
77
  if (!question.multiSelect && values.length > 1) throw new Error("Only one answer is allowed for " + question.id + ".");
91
78
  answers[question.id] = values;
@@ -98,18 +85,33 @@ function normalizeAnswers(questions, raw) {
98
85
  *
99
86
  * handler receives:
100
87
  * request: { id, questions, source, native, provider, diagnostics }
101
- * respond(answers): submits once; respond.cancel(reason) cancels once.
88
+ * respond(answers): submits once; respond.cancel(reason) cancels once;
89
+ * respond.decline(reason) records an explicit refusal once.
102
90
  *
103
91
  * The returned promise resolves to { status, answers|reason }. Provider
104
92
  * adapters translate that result back to their own wire protocol. Nothing in
105
93
  * this lifecycle is persisted; Clay callers may persist sanitized events.
106
94
  */
107
95
  function dispatchUserInput(handler, input, opts) {
108
- opts = opts || {};
109
96
  var questions;
110
97
  try { questions = normalizeQuestions(input); } catch (e) { return Promise.reject(e); }
98
+ return startRequest(handler, questions, opts || {}, input && input.id);
99
+ }
100
+
101
+ /**
102
+ * Start one form elicitation. Forms keep their raw schema in
103
+ * diagnostics.elicitation and are not subject to the 1-3 question limit of
104
+ * structured question tools.
105
+ */
106
+ function dispatchElicitation(handler, request, opts) {
107
+ opts = Object.assign({}, opts || {}, { presentation: "elicitation" });
108
+ opts.diagnostics = Object.assign({}, opts.diagnostics || {}, { elicitation: (opts.diagnostics && opts.diagnostics.elicitation) || request });
109
+ return startRequest(handler, elicitationSchema.questionsFromSchema(request), opts, null);
110
+ }
111
+
112
+ function startRequest(handler, questions, opts, inputId) {
111
113
  if (typeof handler !== "function") return Promise.reject(new Error("This query cannot request structured user input."));
112
- var id = opts.requestId || (input && input.id) || "yoke_input_" + crypto.randomUUID();
114
+ var id = opts.requestId || inputId || "yoke_input_" + crypto.randomUUID();
113
115
  var settled = false;
114
116
  var abortHandler = null;
115
117
  var settleListeners = [];
@@ -143,6 +145,7 @@ function dispatchUserInput(handler, input, opts) {
143
145
  return finish({ status: "submitted", answers: {}, content: content });
144
146
  };
145
147
  respond.cancel = function (reason) { return finish({ status: "cancelled", reason: reason || "Cancelled" }); };
148
+ respond.decline = function (reason) { return finish({ status: "declined", reason: reason || "Declined" }); };
146
149
  respond.isPending = function () { return !settled; };
147
150
  respond.onSettle = function (listener) {
148
151
  if (typeof listener === "function" && !settled) settleListeners.push(listener);
@@ -187,12 +190,17 @@ function codexResponse(result) {
187
190
  return { answers: answers };
188
191
  }
189
192
 
190
- function elicitationResponse(result) {
191
- if (!result || result.status !== "submitted") return { action: "reject" };
192
- if (result.content) return { action: "accept", content: result.content };
193
+ function elicitationResponse(result, schema) {
194
+ if (!result || result.status !== "submitted") return { action: result && result.status === "declined" ? "decline" : "cancel" };
195
+ if (result.content) return { action: "accept", content: elicitationSchema.validateContent(result.content, schema) };
193
196
  var content = {};
194
- Object.keys(result.answers).forEach(function (id) { content[id] = result.answers[id].length === 1 ? result.answers[id][0] : result.answers[id]; });
195
- return { action: "accept", content: content };
197
+ var properties = schema && schema.properties ? schema.properties : {};
198
+ Object.keys(result.answers).forEach(function (id) {
199
+ var values = result.answers[id];
200
+ var multi = elicitationSchema.fieldInfo(properties[id]).multi;
201
+ content[id] = multi ? values : (values.length === 1 ? values[0] : values);
202
+ });
203
+ return { action: "accept", content: elicitationSchema.validateContent(content, schema) };
196
204
  }
197
205
 
198
206
  function buildQuestionShape(maxQuestions) {
@@ -258,6 +266,7 @@ module.exports = {
258
266
  questionsFromElicitation: questionsFromElicitation,
259
267
  normalizeAnswers: normalizeAnswers,
260
268
  dispatchUserInput: dispatchUserInput,
269
+ dispatchElicitation: dispatchElicitation,
261
270
  claudePermissionResult: claudePermissionResult,
262
271
  codexResponse: codexResponse,
263
272
  elicitationResponse: elicitationResponse,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.7.1-beta.1",
3
+ "version": "4.7.1-beta.3",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"