@haystackeditor/cli 0.15.0 → 0.15.1
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/dist/utils/prompter.js +48 -20
- package/package.json +1 -1
package/dist/utils/prompter.js
CHANGED
|
@@ -131,22 +131,32 @@ class JsonPrompter {
|
|
|
131
131
|
try {
|
|
132
132
|
obj = JSON.parse(trimmed);
|
|
133
133
|
}
|
|
134
|
-
catch {
|
|
135
|
-
|
|
134
|
+
catch (err) {
|
|
135
|
+
// Surface malformed input on stderr (PR001: no silent swallow) so an
|
|
136
|
+
// agent can tell its line was rejected and resend, instead of the
|
|
137
|
+
// prompt hanging on input that never parsed. stderr keeps stdout NDJSON.
|
|
138
|
+
process.stderr.write(`haystack setup: ignoring unparseable stdin line: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
139
|
+
return;
|
|
136
140
|
}
|
|
137
|
-
if (!obj || typeof obj
|
|
141
|
+
if (!obj || typeof obj !== 'object')
|
|
142
|
+
return;
|
|
143
|
+
// Mirror OpenCode: question replies are keyed by `requestID`, permission
|
|
144
|
+
// replies by `permissionID`. Legacy `id` accepted too.
|
|
145
|
+
const key = (obj.requestID ?? obj.permissionID ?? obj.id);
|
|
146
|
+
if (typeof key !== 'string')
|
|
138
147
|
return;
|
|
139
|
-
const resolve = this.pending.get(
|
|
148
|
+
const resolve = this.pending.get(key);
|
|
140
149
|
if (resolve) {
|
|
141
|
-
this.pending.delete(
|
|
142
|
-
resolve(obj
|
|
150
|
+
this.pending.delete(key);
|
|
151
|
+
resolve(obj);
|
|
143
152
|
}
|
|
144
153
|
else {
|
|
145
|
-
this.buffered.set(
|
|
154
|
+
this.buffered.set(key, obj);
|
|
146
155
|
}
|
|
147
156
|
});
|
|
148
157
|
}
|
|
149
|
-
|
|
158
|
+
/** Wait for the agent's reply object, keyed by the request/permission id. */
|
|
159
|
+
awaitReply(id) {
|
|
150
160
|
if (this.buffered.has(id)) {
|
|
151
161
|
const v = this.buffered.get(id);
|
|
152
162
|
this.buffered.delete(id);
|
|
@@ -158,8 +168,14 @@ class JsonPrompter {
|
|
|
158
168
|
async confirm(q) {
|
|
159
169
|
if (q.id in this.answers)
|
|
160
170
|
return Boolean(this.answers[q.id]);
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
// A confirm is a single-choice question; emit OpenCode's `question` envelope.
|
|
172
|
+
// Reply: {requestID, answers: <bool>}; reject: {requestID, reject: true}.
|
|
173
|
+
this.emit({ type: 'question', requestID: q.id, kind: 'confirm', prompt: q.message, default: q.default ?? true });
|
|
174
|
+
const reply = await this.awaitReply(q.id);
|
|
175
|
+
if (reply.reject)
|
|
176
|
+
return false;
|
|
177
|
+
const a = reply.answers ?? reply.value; // `answers` is canonical; `value` accepted
|
|
178
|
+
return typeof a === 'boolean' ? a : Boolean(a);
|
|
163
179
|
}
|
|
164
180
|
async multiselect(q) {
|
|
165
181
|
if (q.id in this.answers)
|
|
@@ -167,33 +183,45 @@ class JsonPrompter {
|
|
|
167
183
|
const options = q.choices
|
|
168
184
|
.filter((c) => !isSeparator(c))
|
|
169
185
|
.map((c) => ({ value: c.value, label: c.name, default: c.checked ?? false }));
|
|
170
|
-
|
|
171
|
-
|
|
186
|
+
// Reply: {requestID, answers: <value[]>}; reject: {requestID, reject: true}.
|
|
187
|
+
this.emit({ type: 'question', requestID: q.id, kind: 'multiselect', prompt: q.message, options });
|
|
188
|
+
const reply = await this.awaitReply(q.id);
|
|
189
|
+
if (reply.reject)
|
|
190
|
+
return [];
|
|
191
|
+
const a = reply.answers ?? reply.value;
|
|
192
|
+
return Array.isArray(a) ? a : [];
|
|
172
193
|
}
|
|
173
194
|
async action(q) {
|
|
174
195
|
if (this.answers[q.id] === 'skip')
|
|
175
196
|
return false;
|
|
176
197
|
if (await q.check())
|
|
177
198
|
return true;
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
199
|
+
// An out-of-band action maps to OpenCode's `permission` request. Reply:
|
|
200
|
+
// {permissionID, response: "once"|"always"|"reject"}. We also keep polling
|
|
201
|
+
// check() so it resolves on its own once the action completes.
|
|
202
|
+
this.emit({ type: 'permission', permissionID: q.id, prompt: q.message, url: q.url, poll: true });
|
|
181
203
|
const started = Date.now();
|
|
182
204
|
const timeout = q.timeoutMs ?? 10 * 60 * 1000;
|
|
183
205
|
const interval = q.pollIntervalMs ?? 3000;
|
|
184
206
|
let override = null;
|
|
185
|
-
// Resolve the moment the agent answers, OR when polling detects completion.
|
|
186
207
|
this.ensureStdin();
|
|
187
|
-
const answerPromise = this.
|
|
188
|
-
|
|
208
|
+
const answerPromise = this.awaitReply(q.id).then((reply) => {
|
|
209
|
+
const r = (reply.response ?? reply.value);
|
|
210
|
+
override = r === 'reject' || r === 'skip' ? 'skip' : 'done';
|
|
189
211
|
});
|
|
190
212
|
while (Date.now() - started < timeout) {
|
|
191
213
|
if (await q.check())
|
|
192
214
|
return true;
|
|
193
215
|
if (override === 'skip')
|
|
194
216
|
return false;
|
|
195
|
-
|
|
196
|
-
|
|
217
|
+
// 'done' means the agent reports the action complete — but completion may
|
|
218
|
+
// be eventually-consistent, so keep polling check() at the interval until
|
|
219
|
+
// it confirms or we time out (matching TTY behavior), rather than failing
|
|
220
|
+
// on a single immediate check.
|
|
221
|
+
if (override === 'done') {
|
|
222
|
+
await sleep(interval);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
197
225
|
await Promise.race([sleep(interval), answerPromise]);
|
|
198
226
|
}
|
|
199
227
|
return false;
|