@enter-pro/enter-cli 0.4.2 → 0.4.4
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 +222 -225
- package/dist/auth.d.ts +3 -0
- package/dist/auth.js +114 -6
- package/dist/client.d.ts +1 -4
- package/dist/client.js +13 -63
- package/dist/commands/config.js +5 -10
- package/dist/commands/domain.js +3 -6
- package/dist/commands/login.js +15 -11
- package/dist/commands/logout.js +7 -3
- package/dist/commands/project.js +75 -42
- package/dist/commands/thread.d.ts +37 -1
- package/dist/commands/thread.js +290 -74
- package/dist/commands/whoami.js +1 -1
- package/dist/commands/workspace.js +7 -10
- package/dist/config.d.ts +0 -1
- package/dist/config.js +10 -4
- package/dist/output.d.ts +0 -16
- package/dist/output.js +0 -18
- package/dist/poll.d.ts +1 -0
- package/dist/poll.js +3 -1
- package/dist/thread-events.d.ts +1 -0
- package/dist/thread-events.js +16 -3
- package/dist/workflow.d.ts +46 -0
- package/dist/workflow.js +36 -0
- package/package.json +7 -3
- package/scripts/install-hosts.mjs +29 -0
- package/skills/enter/SKILL.md +40 -0
- package/skills/enter/references/configuration.md +19 -0
package/dist/commands/thread.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
4
|
import { writeFileSync, readFileSync } from "fs";
|
|
@@ -7,7 +8,8 @@ import { pollUntil, TimeoutError } from "../poll.js";
|
|
|
7
8
|
import { resolveLifecycleStatus } from "../lifecycle.js";
|
|
8
9
|
import { ThreadEvents, isStateEvent } from "../thread-events.js";
|
|
9
10
|
import { safeOutput } from "../safe-output.js";
|
|
10
|
-
import { errorEnvelope } from "../errors.js";
|
|
11
|
+
import { RequestError, errorEnvelope } from "../errors.js";
|
|
12
|
+
import { workflowResult } from "../workflow.js";
|
|
11
13
|
import { registerThreadTasks } from "./thread-tasks.js";
|
|
12
14
|
export const threadCmd = new Command("thread").description("Manage project threads and chat");
|
|
13
15
|
registerThreadTasks(threadCmd);
|
|
@@ -27,6 +29,8 @@ function normalizeAction(raw) {
|
|
|
27
29
|
updated_at: String(raw.updated_at ?? raw.UpdatedAt ?? ""),
|
|
28
30
|
};
|
|
29
31
|
}
|
|
32
|
+
// Single registry: adding a new tool only requires editing one row.
|
|
33
|
+
const STRIPE_INPUT_INSTRUCTIONS = "Submit the Stripe key through --secret-value-stdin from an authorized input source. Reuse input already supplied for this task; otherwise offer an available secure input or local-file path. Do not echo the key or embed it in shell text or build prompts. Report connection errors without assuming their cause.";
|
|
30
34
|
async function loadToolCallArgs(projectId, action, toolName, context = {}) {
|
|
31
35
|
const turn = String(action.turn);
|
|
32
36
|
const data = await (context.messages ??= client.get(`/v1/projects/${projectId}/thread/messages`, { start_turn: turn, end_turn: turn }, context.signal));
|
|
@@ -73,18 +77,22 @@ async function loadToolCallArgs(projectId, action, toolName, context = {}) {
|
|
|
73
77
|
}
|
|
74
78
|
return { kind: "not_found" };
|
|
75
79
|
}
|
|
80
|
+
const ANSWERS_FORMAT = 'Use the exact original question text as each key, not a host question ID. Each value must be {"selected_options":["exact option label"],"other_text":"optional free text"}. Use an empty selected_options array for free-text-only answers. See thread approve --help.';
|
|
76
81
|
const TOOL_HANDLERS = {
|
|
77
82
|
// ask_user_question: surface the questions array directly so callers don't
|
|
78
83
|
// have to fetch + parse thread messages to know what to ask the user.
|
|
79
84
|
ask_user_question: {
|
|
80
85
|
kind: "questions",
|
|
81
|
-
approveSuffix:
|
|
82
|
-
instructions:
|
|
86
|
+
approveSuffix: "--answers '<json>'",
|
|
87
|
+
instructions: `Forward questions, options and multiSelect to the user unchanged. Map their response back to the original question text. ${ANSWERS_FORMAT} After receiving the user response, run approve_command with that JSON. Use skip_command only if the user asks to skip.`,
|
|
83
88
|
enrich: async (projectId, action, context) => {
|
|
84
89
|
const args = await loadToolCallArgs(projectId, action, "ask_user_question", context);
|
|
85
90
|
switch (args.kind) {
|
|
86
91
|
case "ok":
|
|
87
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
questions: args.value.questions ?? [],
|
|
94
|
+
skip_command: `enter-cli thread approve ${projectId} ${action.action_id} --skip-answers`,
|
|
95
|
+
};
|
|
88
96
|
case "parse_error":
|
|
89
97
|
return { questions: { raw_arguments: args.raw_arguments, parse_error: true } };
|
|
90
98
|
case "not_found":
|
|
@@ -94,31 +102,31 @@ const TOOL_HANDLERS = {
|
|
|
94
102
|
},
|
|
95
103
|
supabase_add_secret: {
|
|
96
104
|
kind: "secret",
|
|
97
|
-
approveSuffix:
|
|
98
|
-
instructions: "
|
|
105
|
+
approveSuffix: `--secret-name <NAME> --secret-value-stdin`,
|
|
106
|
+
instructions: "Submit the secret name and value through --secret-value-stdin from an authorized input source. Reuse input already supplied for this task; otherwise ask for the missing value through an available secure input or local-file path. Do not echo the value or embed it in shell text or build prompts.",
|
|
99
107
|
},
|
|
100
108
|
supabase_configure_auth_provider: {
|
|
101
109
|
kind: "auth_provider",
|
|
102
|
-
approveSuffix:
|
|
103
|
-
instructions: "Configure
|
|
104
|
-
enrich: async (projectId, action, context) => (
|
|
110
|
+
approveSuffix: "",
|
|
111
|
+
instructions: "Configure this provider with --auth-config-stdin using an authorized input source, or use Enter's provider form. Reuse configuration already supplied for this task; ask only for missing fields. After form submission query status before approving again. Missing input is not build failure.",
|
|
112
|
+
enrich: async (projectId, action, context) => authProviderInput(await authProviderFor(projectId, action, context)),
|
|
105
113
|
},
|
|
106
114
|
stripe_enable: {
|
|
107
115
|
kind: "secret",
|
|
108
|
-
approveSuffix:
|
|
109
|
-
instructions:
|
|
116
|
+
approveSuffix: `--secret-value-stdin`,
|
|
117
|
+
instructions: STRIPE_INPUT_INSTRUCTIONS,
|
|
110
118
|
},
|
|
111
119
|
stripe_update_key_and_migrate: {
|
|
112
120
|
kind: "secret",
|
|
113
|
-
approveSuffix:
|
|
114
|
-
instructions:
|
|
121
|
+
approveSuffix: `--secret-value-stdin`,
|
|
122
|
+
instructions: `${STRIPE_INPUT_INSTRUCTIONS} Product IDs come from the pending tool call.`,
|
|
115
123
|
},
|
|
116
124
|
// confirm_plan_mode: surface the plan text directly on the action so callers
|
|
117
125
|
// don't have to fetch + parse thread messages themselves.
|
|
118
126
|
confirm_plan_mode: {
|
|
119
127
|
kind: "none",
|
|
120
|
-
approveSuffix:
|
|
121
|
-
instructions: "Plan is included in this action's `plan` field.
|
|
128
|
+
approveSuffix: "",
|
|
129
|
+
instructions: "Plan is included in this action's `plan` field. Present it for the user's approval and wait for their decision before running approve_command. Displaying the plan is not approval.",
|
|
122
130
|
enrich: async (projectId, action, context) => {
|
|
123
131
|
const args = await loadToolCallArgs(projectId, action, "confirm_plan_mode", context);
|
|
124
132
|
switch (args.kind) {
|
|
@@ -141,7 +149,7 @@ const TOOL_HANDLERS = {
|
|
|
141
149
|
};
|
|
142
150
|
const DEFAULT_HANDLER = {
|
|
143
151
|
kind: "none",
|
|
144
|
-
approveSuffix:
|
|
152
|
+
approveSuffix: "",
|
|
145
153
|
instructions: "No additional input required. Approve only within the user-authorized scope; input_kind none does not itself grant authorization.",
|
|
146
154
|
};
|
|
147
155
|
const FEATURE_ENABLE_ROUTES = new Map([
|
|
@@ -156,6 +164,25 @@ const AUTH_PROVIDER_FIELDS = {
|
|
|
156
164
|
alipay: ["enabled", "app_id", "private_key"],
|
|
157
165
|
feishu: ["enabled", "app_id", "app_secret"],
|
|
158
166
|
};
|
|
167
|
+
// Input requirements are separate from optional provider settings. Never put
|
|
168
|
+
// stored configuration values (including masked secrets) in action metadata.
|
|
169
|
+
const AUTH_PROVIDER_INPUTS = {
|
|
170
|
+
google: ["client_ids", "client_secret"],
|
|
171
|
+
wechat: ["client_id", "client_secret"],
|
|
172
|
+
alipay: ["app_id", "private_key"],
|
|
173
|
+
feishu: ["app_id", "app_secret"],
|
|
174
|
+
};
|
|
175
|
+
function authProviderInput(provider) {
|
|
176
|
+
return {
|
|
177
|
+
provider,
|
|
178
|
+
configuration: {
|
|
179
|
+
input_surface: "enter_project_auth_form",
|
|
180
|
+
input_methods: ["auth_config_stdin", "enter_project_auth_form"],
|
|
181
|
+
fields: AUTH_PROVIDER_INPUTS[provider],
|
|
182
|
+
instructions: "Supply the listed fields as JSON through --auth-config-stdin from an authorized input source; Enter's provider form is an alternative, not a required detour. If input is missing, offer an available secure input or local-file path. Do not echo credentials or embed them in shell text or build prompts. Use the actual OAuth callback URL when configuring the provider. After saving through the form query status, since it may already have approved the action.",
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
159
186
|
async function authProviderFor(projectId, action, context = {}) {
|
|
160
187
|
if (!action.tool_call_id)
|
|
161
188
|
throw new Error("Auth provider action is missing its tool_call_id");
|
|
@@ -183,7 +210,7 @@ function handlerFor(toolName) {
|
|
|
183
210
|
}
|
|
184
211
|
function buildApproveCommand(projectId, action) {
|
|
185
212
|
const base = `enter-cli thread approve ${projectId} ${action.action_id}`;
|
|
186
|
-
const suffix = handlerFor(action.tool_name).approveSuffix
|
|
213
|
+
const suffix = handlerFor(action.tool_name).approveSuffix;
|
|
187
214
|
return suffix ? `${base} ${suffix}` : base;
|
|
188
215
|
}
|
|
189
216
|
async function fetchPendingActions(projectId, actionIds = [], signal) {
|
|
@@ -217,8 +244,7 @@ threadCmd
|
|
|
217
244
|
content = opts.message;
|
|
218
245
|
}
|
|
219
246
|
else {
|
|
220
|
-
|
|
221
|
-
process.exit(1);
|
|
247
|
+
throw new Error("Provide non-empty input using --message, --file or --stdin");
|
|
222
248
|
}
|
|
223
249
|
if (!content.trim())
|
|
224
250
|
throw new Error("Message must not be empty");
|
|
@@ -241,6 +267,7 @@ threadCmd
|
|
|
241
267
|
.option("--end-turn <n>", "End turn number")
|
|
242
268
|
.option("--turn <n>", "Get messages for a specific turn (shorthand for --start-turn N --end-turn N)")
|
|
243
269
|
.option("--latest", "Get messages from the most recent turn (most common usage)")
|
|
270
|
+
.option("--text", "Return assistant text with turn numbers, without raw event envelopes")
|
|
244
271
|
.option("--tail <n>", "Get messages from the last N turns")
|
|
245
272
|
.option("--follow", "Stream NDJSON events over WebSocket; no automatic approval")
|
|
246
273
|
.option("--cursor <id>", "Resume after a previously returned event ID")
|
|
@@ -249,6 +276,8 @@ threadCmd
|
|
|
249
276
|
.option("--max-events <n>", "Stop --follow after N events")
|
|
250
277
|
.action(async (id, opts, cmd) => {
|
|
251
278
|
if (opts.follow) {
|
|
279
|
+
if (opts.text)
|
|
280
|
+
throw new Error("--text is not supported with --follow");
|
|
252
281
|
await followThreadStream(id, opts);
|
|
253
282
|
return;
|
|
254
283
|
}
|
|
@@ -264,7 +293,7 @@ threadCmd
|
|
|
264
293
|
const resp = turnsData;
|
|
265
294
|
const turns = resp.turns || [];
|
|
266
295
|
if (turns.length === 0) {
|
|
267
|
-
|
|
296
|
+
print(getFormat(cmd), { messages: [] });
|
|
268
297
|
return;
|
|
269
298
|
}
|
|
270
299
|
if (opts.latest) {
|
|
@@ -283,15 +312,18 @@ threadCmd
|
|
|
283
312
|
}
|
|
284
313
|
}
|
|
285
314
|
if (!startTurn) {
|
|
286
|
-
|
|
287
|
-
process.exit(1);
|
|
315
|
+
throw new Error("Specify --start-turn, --turn, --latest, --tail, or --follow");
|
|
288
316
|
}
|
|
289
317
|
const params = {};
|
|
290
318
|
params.start_turn = startTurn;
|
|
291
319
|
if (endTurn)
|
|
292
320
|
params.end_turn = endTurn;
|
|
293
321
|
const data = await client.get(`/v1/projects/${id}/thread/messages`, params);
|
|
294
|
-
|
|
322
|
+
if (opts.text) {
|
|
323
|
+
print(getFormat(cmd), safeOutput({ messages: assistantMessages(data) }));
|
|
324
|
+
}
|
|
325
|
+
else
|
|
326
|
+
print(getFormat(cmd), data);
|
|
295
327
|
});
|
|
296
328
|
async function followThreadStream(projectId, opts) {
|
|
297
329
|
const timeout = positiveNumber(opts.timeout ?? "60", "--timeout") * 1000;
|
|
@@ -360,16 +392,64 @@ threadCmd
|
|
|
360
392
|
]);
|
|
361
393
|
printTable(["Turn", "Name", "Status", "Model", "Credits", "Created"], rows);
|
|
362
394
|
});
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
395
|
+
function assistantMessages(data) {
|
|
396
|
+
const rows = Array.isArray(data) ? data : data?.messages ?? [];
|
|
397
|
+
return rows.flatMap((row) => {
|
|
398
|
+
const content = row?.detail?.assistant_content?.content;
|
|
399
|
+
return row?.message_type === "assistant_content" && typeof content === "string" && content.trim()
|
|
400
|
+
? [{ turn: row.turn, text: content }] : [];
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
async function readProgress(id, turn, signal, timeoutMs = 30000) {
|
|
404
|
+
const controller = new AbortController();
|
|
405
|
+
const abort = () => controller.abort(signal.reason);
|
|
406
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
407
|
+
if (signal.aborted)
|
|
408
|
+
abort();
|
|
409
|
+
// Optional narrative must not hold up action handling or status indefinitely.
|
|
410
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
411
|
+
try {
|
|
412
|
+
const data = await client.get(`/v1/projects/${id}/thread/messages`, {
|
|
413
|
+
start_turn: String(turn.turn), end_turn: String(turn.turn),
|
|
414
|
+
...(typeof turn.chat_id === "string" ? { chat_id: turn.chat_id } : {}),
|
|
415
|
+
}, controller.signal);
|
|
416
|
+
const messages = assistantMessages(data).filter(m => m.turn === undefined || String(m.turn) === String(turn.turn)).slice(-1);
|
|
417
|
+
const revision = createHash("sha256").update(JSON.stringify([id, turn.id, turn.chat_id, turn.turn, messages])).digest("hex");
|
|
418
|
+
return { source: "assistant_messages", available: true, revision,
|
|
419
|
+
messages: messages.map(m => ({ ...m, text: m.text.slice(0, 1600), ...(m.text.length > 1600 ? { truncated: true } : {}) })) };
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
return { source: "assistant_messages", available: false, messages: [],
|
|
423
|
+
error: controller.signal.aborted ? { code: "PROGRESS_QUERY_TIMEOUT", retryable: true } : errorEnvelope(error).error };
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
clearTimeout(timer);
|
|
427
|
+
signal.removeEventListener("abort", abort);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
export function threadInteraction(id, { taskId, chatId, turn, requireBuild, timeout, afterProgress, progressInterval } = {}) {
|
|
431
|
+
const targetArgs = [
|
|
432
|
+
...(taskId ? ["--task-id", taskId] : turn === undefined ? [] : ["--turn", String(turn)]),
|
|
433
|
+
...(chatId ? ["--chat-id", chatId] : []), ...(requireBuild ? ["--require-build"] : []),
|
|
434
|
+
];
|
|
435
|
+
const target = targetArgs.length ? ` ${targetArgs.join(" ")}` : "";
|
|
436
|
+
const waitArgv = ["enter-cli", "--output", "json", "thread", "wait", id, ...targetArgs,
|
|
437
|
+
...(afterProgress ? ["--after-progress", afterProgress] : []),
|
|
438
|
+
...(progressInterval !== undefined ? ["--progress-interval", progressInterval] : []),
|
|
439
|
+
"--timeout", timeout === "0" ? "10" : timeout ?? "10"];
|
|
440
|
+
const streamArgv = [...waitArgv.slice(0, -2), "--stream", "--timeout", "0"];
|
|
441
|
+
const shellCommand = (argv) => argv.map(arg => /^[a-zA-Z0-9_./:-]+$/.test(arg) ? arg : "'" + arg.replace(/'/g, "'\"'\"'") + "'").join(" ");
|
|
366
442
|
return {
|
|
443
|
+
workflow: workflowResult({ status: "running", task_id: taskId, turn: { turn, chat_id: chatId } }),
|
|
367
444
|
monitoring_required: true,
|
|
368
|
-
wait_command:
|
|
369
|
-
|
|
370
|
-
|
|
445
|
+
wait_command: shellCommand(waitArgv),
|
|
446
|
+
wait_argv: waitArgv,
|
|
447
|
+
stream_command: shellCommand(streamArgv),
|
|
448
|
+
stream_argv: streamArgv,
|
|
449
|
+
watch_command: `enter-cli thread watch ${id}${target}${afterProgress ? ` --after-progress ${afterProgress}` : ""}${progressInterval !== undefined ? ` --progress-interval ${progressInterval}` : ""} --timeout ${timeout ?? "60"}`,
|
|
450
|
+
status_command: `enter-cli --output json thread status ${id}${target} --timeout 10`,
|
|
371
451
|
follow_up_command: `enter-cli --output json thread chat ${id}${chatId ? ` --chat-id ${chatId}` : ""} --file <message-file>`,
|
|
372
|
-
instructions: "
|
|
452
|
+
instructions: "Follow workflow.next_action; tool completion or CLI exit 0 is not Enter task completion. First handle incoming user messages: reply from the last observation before refreshing status. Briefly relay batched progress.messages as Enter-reported progress; do not infer progress from credits, timestamps, or tool activity. With no new narrative, observe quietly; on user inquiry report the last known state and any unavailable progress. Suppress repeated revisions and heartbeats. After a bounded observer exits while observation is still required, continue with wait_argv or validated wait_command, including after observation timeout. Do not end the task or ask whether to continue merely because wait returned. Shell-quote each wait_argv argument and invoke bare enter-cli without sleep, cd, or other prefixes. Stop observing on completed, failed, blocked, resolve_error, or resume_when_requested and handle that state. Forward questions unchanged; approve only within existing authorization. Prefer one background stream_argv invocation when the host delivers incremental output and can handle user messages concurrently. While that process is active, consume its events without starting another observer; report_progress means relay text, not restart the process. Use bounded wait only as a fallback for buffered output or completion-only notifications; batched progress ends that observation. Keep the returned timeout and progress interval; do not lengthen waits. Streaming wait and watch require incremental output delivery; consume output nonblockingly. Do not resubmit work. Hosts own scheduling; observation never cancels or approves remote work.",
|
|
373
453
|
};
|
|
374
454
|
}
|
|
375
455
|
function positiveNumber(value, option, integer = false) {
|
|
@@ -380,15 +460,19 @@ function positiveNumber(value, option, integer = false) {
|
|
|
380
460
|
return number;
|
|
381
461
|
}
|
|
382
462
|
async function readThreadSnapshot(id, turnNumber, signal, taskId, chatId) {
|
|
383
|
-
|
|
463
|
+
// Both are read-only; serial reads unnecessarily consume the observation budget.
|
|
464
|
+
const turnsPromise = client.get(`/v1/projects/${id}/thread/turns`, chatId ? { chat_id: chatId } : undefined, signal);
|
|
465
|
+
const actionsPromise = fetchPendingActions(id, [], signal).then(actions => ({ actions }), error => ({ error }));
|
|
466
|
+
const data = await turnsPromise;
|
|
384
467
|
const turns = data.turns ?? [];
|
|
385
468
|
const selected = taskId ? turns.find(t => t.id === taskId || t.task_id === taskId)
|
|
386
469
|
: turnNumber === undefined ? turns[0] : turns.find(t => Number(t.turn) === turnNumber);
|
|
387
470
|
if (!selected && taskId) {
|
|
388
471
|
const queue = await client.get(`/v1/projects/${id}/thread/tasks`, { simple: "true", ...(chatId ? { chat_id: chatId } : {}) }, signal);
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
472
|
+
const queued = queue.task_ids?.includes(taskId);
|
|
473
|
+
return { status: queued ? "queued" : "unknown", project_id: id, turn: null,
|
|
474
|
+
reason: queued ? "task_queued" : "task_not_observed",
|
|
475
|
+
...(!queued ? {
|
|
392
476
|
correlation: "unavailable",
|
|
393
477
|
project_activity: turns[0] ? pick(turns[0], ["id", "turn", "status", "chat_id", "commit_id"]) : null,
|
|
394
478
|
project_status_command: `enter-cli --output json thread status ${id}${chatId ? ` --chat-id ${chatId}` : ""}`,
|
|
@@ -403,7 +487,10 @@ async function readThreadSnapshot(id, turnNumber, signal, taskId, chatId) {
|
|
|
403
487
|
if (["cancelled", "error", "failed"].includes(status)) {
|
|
404
488
|
return { ...base, status: "failed", reason: `turn_${status}` };
|
|
405
489
|
}
|
|
406
|
-
const
|
|
490
|
+
const actionResult = await actionsPromise;
|
|
491
|
+
if ("error" in actionResult)
|
|
492
|
+
throw actionResult.error;
|
|
493
|
+
const pending = actionResult.actions.filter(a => String(a.turn) === String(selected.turn));
|
|
407
494
|
if (pending.length) {
|
|
408
495
|
const context = { signal };
|
|
409
496
|
const actions = await Promise.all(pending.map(async (a) => {
|
|
@@ -435,22 +522,32 @@ async function readThreadSnapshot(id, turnNumber, signal, taskId, chatId) {
|
|
|
435
522
|
return { ...base, status: "completed", project: { ...project, lifecycle_status: resolveLifecycleStatus(project) } };
|
|
436
523
|
}
|
|
437
524
|
function projectSnapshot(snapshot, compact = false) {
|
|
525
|
+
const workflow = workflowResult(snapshot);
|
|
438
526
|
const result = { ...snapshot };
|
|
439
|
-
if (compact && snapshot.turn)
|
|
527
|
+
if (compact && snapshot.turn) {
|
|
440
528
|
result.turn = pick(snapshot.turn, ["id", "turn", "status", "turn_name", "created_at", "updated_at", "commit_id", "chat_id"]);
|
|
529
|
+
if (typeof snapshot.turn.turn_name === "string")
|
|
530
|
+
result.turn.turn_name = snapshot.turn.turn_name.slice(0, 1000);
|
|
531
|
+
}
|
|
532
|
+
if (snapshot.status === "completed" && snapshot.turn) {
|
|
533
|
+
result.messages_command = `enter-cli --output json thread messages ${snapshot.project_id} --turn ${snapshot.turn.turn} --text`;
|
|
534
|
+
}
|
|
441
535
|
if (snapshot.project) {
|
|
442
536
|
const p = snapshot.project;
|
|
443
537
|
if (compact)
|
|
444
538
|
result.project = pick(p, ["project_id", "name", "status", "lifecycle_status", "commit", "commit_turn", "preview_url", "publish_url", "build_status"]);
|
|
445
|
-
|
|
446
|
-
result.build_matches_turn = Boolean(snapshot.turn?.commit_id && build?.commit_id === snapshot.turn.commit_id);
|
|
539
|
+
result.build_matches_turn = workflow.build?.matches_task ?? false;
|
|
447
540
|
const supabase = p.supabase;
|
|
448
541
|
result.integrations = { cloud: supabase?.status ?? "unknown", ai: p.ai_connection_state ?? (p.ai_capability_enabled === true ? "enabled" : "unknown") };
|
|
449
542
|
}
|
|
450
|
-
|
|
543
|
+
result.workflow = workflow;
|
|
544
|
+
// Buffered hosts may truncate verbose metadata: keep the decision and continuation first.
|
|
545
|
+
return { status: snapshot.status, workflow,
|
|
546
|
+
...(result.wait_command ? { wait_command: result.wait_command, wait_argv: result.wait_argv, instructions: result.instructions } : {}),
|
|
547
|
+
...result };
|
|
451
548
|
}
|
|
452
549
|
// Owns observation and lifetime only; callers choose JSON or NDJSON rendering.
|
|
453
|
-
async function observeThread(id, opts, wait, onSnapshot) {
|
|
550
|
+
async function observeThread(id, opts, wait, onSnapshot, continuous = false) {
|
|
454
551
|
let target = opts.turn === undefined ? undefined : positiveNumber(opts.turn, "--turn", true);
|
|
455
552
|
if (opts.taskId && target !== undefined)
|
|
456
553
|
throw new Error("Use only one of --task-id and --turn");
|
|
@@ -458,25 +555,35 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
458
555
|
throw new Error("--transport must be auto or poll");
|
|
459
556
|
if (opts.cursor && !/^\d+-\d+$/.test(opts.cursor))
|
|
460
557
|
throw new Error("--cursor must be an event ID such as 123-0");
|
|
461
|
-
|
|
558
|
+
if (opts.afterProgress && !/^[a-f0-9]{64}$/.test(opts.afterProgress))
|
|
559
|
+
throw new Error("--after-progress must be a returned progress revision");
|
|
560
|
+
const progressIntervalMs = Number(opts.progressInterval ?? "10") * 1000;
|
|
561
|
+
if (!Number.isFinite(progressIntervalMs) || progressIntervalMs < 0 || progressIntervalMs > 2147483647)
|
|
562
|
+
throw new Error("--progress-interval must be between 0 and 2147483 seconds");
|
|
563
|
+
const progressStartedAt = Date.now();
|
|
564
|
+
const timeoutMs = continuous && opts.timeout === "0" ? 0 : positiveNumber(opts.timeout, "--timeout") * 1000;
|
|
462
565
|
if (timeoutMs > 2147483647)
|
|
463
566
|
throw new Error("--timeout is too large");
|
|
464
567
|
const controller = new AbortController();
|
|
465
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
568
|
+
const timer = timeoutMs === 0 ? undefined : setTimeout(() => controller.abort(), timeoutMs);
|
|
466
569
|
const interrupt = () => controller.abort("SIGINT");
|
|
467
570
|
const terminate = () => controller.abort("SIGTERM");
|
|
468
571
|
process.once("SIGINT", interrupt);
|
|
469
572
|
process.once("SIGTERM", terminate);
|
|
470
573
|
let last = { status: "unknown", project_id: id, turn: null };
|
|
471
574
|
let querying = false;
|
|
575
|
+
let hasSnapshot = false;
|
|
576
|
+
let progressPromise;
|
|
577
|
+
let progressRead;
|
|
472
578
|
let stream;
|
|
473
579
|
let reconcileUntil = 0;
|
|
474
580
|
const emit = (extra = {}, final = false) => onSnapshot({
|
|
475
581
|
...last,
|
|
476
582
|
...(["idle", "running", "queued", "pending", "unknown"].includes(last.status)
|
|
477
|
-
? threadInteraction(id, { taskId: opts.taskId, chatId: opts.chatId, turn: target, requireBuild: opts.requireBuild
|
|
583
|
+
? threadInteraction(id, { taskId: opts.taskId, chatId: opts.chatId, turn: target, requireBuild: opts.requireBuild,
|
|
584
|
+
timeout: wait ? opts.timeout : undefined, progressInterval: opts.progressInterval === "10" ? undefined : opts.progressInterval, afterProgress: last.progress?.messages.length ? last.progress.revision : opts.afterProgress }) : {}),
|
|
478
585
|
...(opts.taskId ? { task_id: opts.taskId } : {}),
|
|
479
|
-
...(stream ? { transport: stream.mode, transport_reason: stream.reason, cursor: stream.cursor } : {}), ...extra,
|
|
586
|
+
...(stream ? { transport: stream.mode, transport_reason: stream.reason, cursor: stream.cursor } : wait ? { transport: "polling" } : {}), ...extra,
|
|
480
587
|
}, final);
|
|
481
588
|
const interruption = () => {
|
|
482
589
|
const reason = controller.signal.reason;
|
|
@@ -491,7 +598,32 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
491
598
|
throw stream.failure;
|
|
492
599
|
const revision = stream?.revision ?? 0;
|
|
493
600
|
querying = true;
|
|
494
|
-
|
|
601
|
+
// A persistent observer still bounds each read so a stalled endpoint cannot hang forever.
|
|
602
|
+
const queryController = new AbortController();
|
|
603
|
+
const abortQuery = () => queryController.abort(controller.signal.reason);
|
|
604
|
+
controller.signal.addEventListener("abort", abortQuery, { once: true });
|
|
605
|
+
const queryTimer = setTimeout(() => queryController.abort(), 30000);
|
|
606
|
+
try {
|
|
607
|
+
last = await readThreadSnapshot(id, target, queryController.signal, opts.taskId, opts.chatId);
|
|
608
|
+
last.observed_at = new Date().toISOString();
|
|
609
|
+
hasSnapshot = true;
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
if (queryController.signal.aborted && !controller.signal.aborted) {
|
|
613
|
+
error = new RequestError("QUERY_TIMEOUT", "Enter status query exceeded its deadline.", true);
|
|
614
|
+
}
|
|
615
|
+
if (!wait || controller.signal.aborted || !(error instanceof RequestError) || !error.retryable)
|
|
616
|
+
throw error;
|
|
617
|
+
querying = false;
|
|
618
|
+
emit({ ...errorEnvelope(error), observation_retrying: true, last_observed: hasSnapshot });
|
|
619
|
+
await delay(1000, undefined, { signal: controller.signal });
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
finally {
|
|
623
|
+
queryController.abort();
|
|
624
|
+
clearTimeout(queryTimer);
|
|
625
|
+
controller.signal.removeEventListener("abort", abortQuery);
|
|
626
|
+
}
|
|
495
627
|
if (opts.requireBuild && last.status === "completed") {
|
|
496
628
|
const build = last.project?.build_status;
|
|
497
629
|
const matches = Boolean(last.turn?.commit_id && build?.commit_id === last.turn.commit_id);
|
|
@@ -501,6 +633,30 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
501
633
|
last = { ...last, status: "failed", reason: "build_failed" };
|
|
502
634
|
}
|
|
503
635
|
querying = false;
|
|
636
|
+
if (last.turn && ["running", "completed"].includes(last.status)) {
|
|
637
|
+
if (!wait || last.status === "completed") {
|
|
638
|
+
last.progress = await readProgress(id, last.turn, controller.signal, 3000);
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
const key = JSON.stringify([last.turn.id, last.turn.chat_id, last.turn.turn]);
|
|
642
|
+
if (progressRead?.key !== key)
|
|
643
|
+
progressRead = { key, pending: false, nextReadAt: 0 };
|
|
644
|
+
const read = progressRead;
|
|
645
|
+
if (!read.pending && Date.now() >= read.nextReadAt) {
|
|
646
|
+
read.pending = true;
|
|
647
|
+
read.nextReadAt = Date.now() + 2000;
|
|
648
|
+
// Narrative latency must not delay authoritative state/card checks.
|
|
649
|
+
progressPromise = readProgress(id, last.turn, controller.signal).then(value => {
|
|
650
|
+
read.value = value;
|
|
651
|
+
read.pending = false;
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
last.progress = read.value ?? { source: "assistant_messages", available: false, messages: [] };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const stopped = interruption();
|
|
658
|
+
if (stopped !== undefined)
|
|
659
|
+
return stopped;
|
|
504
660
|
if (target === undefined && last.turn && !opts.taskId)
|
|
505
661
|
target = positiveNumber(String(last.turn.turn), "server turn", true);
|
|
506
662
|
// A newly accepted task can be temporarily absent from both reads. Keep
|
|
@@ -510,7 +666,9 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
510
666
|
emit({}, true);
|
|
511
667
|
return last.status === "failed" || last.status === "unknown" ? 1 : 0;
|
|
512
668
|
}
|
|
513
|
-
if (
|
|
669
|
+
if (controller.signal.aborted)
|
|
670
|
+
break;
|
|
671
|
+
if (!stream && opts.transport === "auto") {
|
|
514
672
|
stream = new ThreadEvents(id, {
|
|
515
673
|
signal: controller.signal, turn: target ?? (last.turn ? Number(last.turn.turn) : undefined),
|
|
516
674
|
chatId: opts.chatId ?? last.turn?.chat_id, cursor: opts.cursor, stateOnly: true,
|
|
@@ -518,16 +676,24 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
518
676
|
reconcileUntil = Date.now() + 1500; },
|
|
519
677
|
});
|
|
520
678
|
}
|
|
679
|
+
const progressChanged = last.progress?.available && last.progress.messages.length
|
|
680
|
+
&& last.progress.revision !== opts.afterProgress;
|
|
681
|
+
if (!continuous && progressChanged && Date.now() - progressStartedAt >= progressIntervalMs) {
|
|
682
|
+
emit({ progress_changed: true }, true);
|
|
683
|
+
return 0;
|
|
684
|
+
}
|
|
521
685
|
emit();
|
|
522
686
|
if (stream)
|
|
523
687
|
await stream.wait(revision, Date.now() < reconcileUntil ? 250 : stream.mode === "websocket" ? 30000 : 2000);
|
|
524
|
-
else
|
|
525
|
-
|
|
688
|
+
else {
|
|
689
|
+
const pause = delay(2000, undefined, { signal: controller.signal });
|
|
690
|
+
await (progressRead?.pending && progressPromise ? Promise.race([pause, progressPromise]) : pause);
|
|
691
|
+
}
|
|
526
692
|
}
|
|
527
693
|
const interrupted = interruption();
|
|
528
694
|
if (interrupted !== undefined)
|
|
529
695
|
return interrupted;
|
|
530
|
-
emit({ wait_timed_out: true, query_timed_out: false }, true);
|
|
696
|
+
emit({ wait_timed_out: true, query_timed_out: false, last_observed: hasSnapshot }, true);
|
|
531
697
|
return 2;
|
|
532
698
|
}
|
|
533
699
|
catch (error) {
|
|
@@ -535,13 +701,20 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
535
701
|
if (interrupted !== undefined)
|
|
536
702
|
return interrupted;
|
|
537
703
|
if (!controller.signal.aborted) {
|
|
538
|
-
emit({ ...errorEnvelope(error), last_observed: true
|
|
704
|
+
emit({ ...errorEnvelope(error), last_observed: true,
|
|
705
|
+
...(error instanceof RequestError && error.code === "QUERY_TIMEOUT" ? { query_timed_out: true } : {}),
|
|
706
|
+
}, true);
|
|
539
707
|
return 1;
|
|
540
708
|
}
|
|
541
|
-
|
|
709
|
+
if (wait && hasSnapshot) {
|
|
710
|
+
emit({ wait_timed_out: true, query_timed_out: false, last_observed: true }, true);
|
|
711
|
+
return 2;
|
|
712
|
+
}
|
|
713
|
+
emit({ wait_timed_out: wait, query_timed_out: querying, ...(querying ? { error: { code: "QUERY_TIMEOUT", message: "The observation deadline expired before a status snapshot was available. Task state is unknown; this does not identify a network or server failure.", retryable: true, outcome_unknown: false } } : {}) }, true);
|
|
542
714
|
return wait && !querying ? 2 : 1;
|
|
543
715
|
}
|
|
544
716
|
finally {
|
|
717
|
+
controller.abort();
|
|
545
718
|
stream?.close();
|
|
546
719
|
clearTimeout(timer);
|
|
547
720
|
process.removeListener("SIGINT", interrupt);
|
|
@@ -551,38 +724,68 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
551
724
|
export async function reportThread(id, opts, cmd, wait, watch = false) {
|
|
552
725
|
if (opts.full && opts.compact)
|
|
553
726
|
throw new Error("Use only one of --full and --compact");
|
|
727
|
+
const progressIntervalMs = Number(opts.progressInterval ?? "10") * 1000;
|
|
728
|
+
const heartbeatIntervalMs = Math.max(30000, progressIntervalMs);
|
|
554
729
|
let signature = "";
|
|
730
|
+
let lastOutputAt = 0;
|
|
731
|
+
let lastState = "";
|
|
732
|
+
let reportedProgress = opts.afterProgress;
|
|
555
733
|
process.exitCode = await observeThread(id, opts, wait, (snapshot, final) => {
|
|
556
|
-
|
|
734
|
+
if (!watch && !final)
|
|
735
|
+
return;
|
|
736
|
+
const now = Date.now();
|
|
737
|
+
const state = JSON.stringify([snapshot.status, snapshot.reason, snapshot.turn?.id, snapshot.error]);
|
|
738
|
+
const stateChanged = state !== lastState;
|
|
739
|
+
const intervalElapsed = now - lastOutputAt >= progressIntervalMs;
|
|
740
|
+
const heartbeatDue = now - lastOutputAt >= heartbeatIntervalMs;
|
|
741
|
+
// Skip output preparation inside the batch window without delaying state transitions.
|
|
742
|
+
if (watch && !final && !stateChanged && !intervalElapsed && !heartbeatDue)
|
|
743
|
+
return;
|
|
744
|
+
const progressChanged = Boolean(snapshot.progress?.available && snapshot.progress.messages.length
|
|
745
|
+
&& snapshot.progress.revision !== reportedProgress);
|
|
746
|
+
const data = projectSnapshot({ ...snapshot, progress_changed: progressChanged }, !opts.full);
|
|
557
747
|
if (!watch) {
|
|
558
|
-
|
|
559
|
-
print(getFormat(cmd), safeOutput(data));
|
|
748
|
+
print(getFormat(cmd), safeOutput(data));
|
|
560
749
|
return;
|
|
561
750
|
}
|
|
562
751
|
// Cursor-only changes should not flood agents with identical snapshots.
|
|
563
|
-
const next = JSON.stringify({ ...data, cursor: undefined
|
|
564
|
-
|
|
565
|
-
|
|
752
|
+
const next = JSON.stringify({ ...data, cursor: undefined, observed_at: undefined, progress_changed: undefined,
|
|
753
|
+
workflow: workflowResult({ ...snapshot, progress_changed: false }) });
|
|
754
|
+
const changed = next !== signature;
|
|
755
|
+
if (final || stateChanged || (changed && intervalElapsed) || heartbeatDue) {
|
|
756
|
+
lastState = state;
|
|
757
|
+
process.stdout.write(JSON.stringify(safeOutput({ type: final ? "result" : changed || snapshot.error ? "snapshot" : "heartbeat", ...data, progress_changed: progressChanged, emitted_at: new Date(now).toISOString() })) + "\n");
|
|
566
758
|
signature = next;
|
|
759
|
+
lastOutputAt = now;
|
|
760
|
+
if (snapshot.progress?.available && snapshot.progress.messages.length)
|
|
761
|
+
reportedProgress = snapshot.progress.revision;
|
|
567
762
|
}
|
|
568
|
-
});
|
|
763
|
+
}, watch);
|
|
569
764
|
}
|
|
570
765
|
for (const [name, timeout, description] of [
|
|
571
766
|
["status", "30", "Read one task snapshot; never wait for completion"],
|
|
572
|
-
["wait", "10", "Wait for an action or terminal state using
|
|
767
|
+
["wait", "10", "Wait for batched progress, an action or terminal state using HTTP polling"],
|
|
573
768
|
["watch", "60", "Stream meaningful state changes as NDJSON; exit on pending actions or terminal state"],
|
|
574
769
|
]) {
|
|
575
|
-
threadCmd.command(`${name} <project_id>`).description(description)
|
|
770
|
+
const command = threadCmd.command(`${name} <project_id>`).description(description)
|
|
576
771
|
.option("--turn <n>", "Select a fixed turn number")
|
|
577
772
|
.option("--task-id <id>", "Follow exactly the task returned by chat/approve, including queue time")
|
|
578
773
|
.option("--chat-id <id>", "Scope turn lookup and events to a chat")
|
|
579
774
|
.option("--cursor <id>", "Resume stream after an event ID")
|
|
580
|
-
.option("--
|
|
775
|
+
.option("--after-progress <revision>", "Wait for narrative progress after a returned revision; preserves task identity")
|
|
776
|
+
.option("--progress-interval <seconds>", "Batch ordinary progress; 0 reports every change; actions/terminal states bypass this interval", "10")
|
|
777
|
+
.option("--transport <mode>", "poll: HTTP only (default); auto: opt in to WebSocket with fallback", "poll")
|
|
581
778
|
.option("--require-build", "Require a matching successful build before reporting completion")
|
|
582
|
-
.option("--compact", "Return only monitoring fields (default
|
|
583
|
-
.option("--full", "Include full sanitized metadata
|
|
584
|
-
.option("--timeout <seconds>", "Bound the entire call without cancelling Enter", timeout)
|
|
585
|
-
.action(async (id, opts, cmd) =>
|
|
779
|
+
.option("--compact", "Return only monitoring fields (default)")
|
|
780
|
+
.option("--full", "Include full sanitized metadata, including usage")
|
|
781
|
+
.option("--timeout <seconds>", name === "watch" ? "Observation lifetime; 0 waits until action/terminal state or interruption" : "Bound the entire call without cancelling Enter", timeout)
|
|
782
|
+
.action(async (id, opts, cmd) => {
|
|
783
|
+
if (opts.stream && cmd.getOptionValueSource("timeout") === "default")
|
|
784
|
+
opts.timeout = "0";
|
|
785
|
+
await reportThread(id, opts, cmd, name !== "status", name === "watch" || Boolean(opts.stream));
|
|
786
|
+
});
|
|
787
|
+
if (name === "wait")
|
|
788
|
+
command.option("--stream", "Keep one observer running and emit batched NDJSON; default timeout 0, host owns background execution");
|
|
586
789
|
}
|
|
587
790
|
threadCmd
|
|
588
791
|
.command("diff <project_id> <turn_number>")
|
|
@@ -600,7 +803,7 @@ threadCmd
|
|
|
600
803
|
else {
|
|
601
804
|
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
602
805
|
}
|
|
603
|
-
|
|
806
|
+
printResult(getFormat(cmd), { path }, `Diff written to ${path}`);
|
|
604
807
|
return;
|
|
605
808
|
}
|
|
606
809
|
print(getFormat(cmd), data);
|
|
@@ -608,10 +811,11 @@ threadCmd
|
|
|
608
811
|
threadCmd
|
|
609
812
|
.command("cancel <project_id>")
|
|
610
813
|
.description("Cancel the currently running turn. No-op if nothing is running.")
|
|
611
|
-
.
|
|
814
|
+
.option("--chat-id <id>", "Scope cancellation to this chat")
|
|
815
|
+
.action(async (id, opts, cmd) => {
|
|
612
816
|
// Pre-check the latest turn — the server returns code 1000 even when
|
|
613
817
|
// nothing is running, which is misleading. We want a clean idempotent no-op.
|
|
614
|
-
const turnsResp = await client.get(`/v1/projects/${id}/thread/turns
|
|
818
|
+
const turnsResp = await client.get(`/v1/projects/${id}/thread/turns`, opts.chatId ? { chat_id: opts.chatId } : undefined);
|
|
615
819
|
const turns = turnsResp.turns ?? [];
|
|
616
820
|
const latest = turns[0];
|
|
617
821
|
const isRunning = latest && !TERMINAL_TURN_STATUSES.has(String(latest.status ?? ""));
|
|
@@ -622,11 +826,13 @@ threadCmd
|
|
|
622
826
|
reason: "no_running_turn",
|
|
623
827
|
latest_turn: latest?.turn ?? null,
|
|
624
828
|
latest_status: latest?.status ?? null,
|
|
829
|
+
tasks_command: `enter-cli --output json thread tasks ${id}${opts.chatId ? ` --chat-id ${opts.chatId}` : ""}`,
|
|
625
830
|
}, `Nothing to cancel (latest turn ${latest?.turn ?? "?"} status: ${latest?.status ?? "unknown"}).`);
|
|
626
831
|
return;
|
|
627
832
|
}
|
|
628
|
-
|
|
629
|
-
|
|
833
|
+
const chatId = opts.chatId ?? latest.chat_id;
|
|
834
|
+
await client.post(`/v1/projects/${id}/thread/cancel`, chatId ? { chat_id: chatId } : undefined);
|
|
835
|
+
printResult(format, { cancelled: true, turn: latest.turn, ...(chatId ? { chat_id: chatId } : {}) }, `Cancelled turn ${latest.turn}.`);
|
|
630
836
|
});
|
|
631
837
|
threadCmd
|
|
632
838
|
.command("restore <project_id>")
|
|
@@ -756,7 +962,7 @@ threadCmd
|
|
|
756
962
|
.option("--secret-value-stdin", "Read the secret value from stdin instead of process arguments")
|
|
757
963
|
.option("--auth-config-stdin", "Read configuration JSON for the action's auth provider from stdin; credentials never go into the approval response")
|
|
758
964
|
.option("--tool-result <result>", "Custom tool result string")
|
|
759
|
-
.option("--answers <json>",
|
|
965
|
+
.option("--answers <json>", `Answers for ask_user_question. ${ANSWERS_FORMAT}`)
|
|
760
966
|
.option("--skip-answers", "Skip all questions for ask_user_question (sets skipped: true)")
|
|
761
967
|
.action(async (projectId, actionIdArg, opts, cmd) => {
|
|
762
968
|
const format = getFormat(cmd);
|
|
@@ -792,7 +998,17 @@ threadCmd
|
|
|
792
998
|
}
|
|
793
999
|
const data = await client.get(configPath);
|
|
794
1000
|
if (data.config?.providers?.[provider]?.enabled !== true) {
|
|
795
|
-
|
|
1001
|
+
print(format, {
|
|
1002
|
+
status: "blocked", reason: "auth_provider_configuration_required",
|
|
1003
|
+
project_id: projectId, approved: false,
|
|
1004
|
+
actions: [{
|
|
1005
|
+
action_id: action.action_id, tool_name: action.tool_name, turn: action.turn,
|
|
1006
|
+
input_kind: "auth_provider", ...authProviderInput(provider),
|
|
1007
|
+
approve_command: buildApproveCommand(projectId, action),
|
|
1008
|
+
}],
|
|
1009
|
+
next_step: "Provide the missing fields via --auth-config-stdin, or save them in Enter's provider form and query thread status. Continue with existing authorized input without asking the user to enter it again.",
|
|
1010
|
+
});
|
|
1011
|
+
return;
|
|
796
1012
|
}
|
|
797
1013
|
// The backend additionally verifies stored credentials and the card's
|
|
798
1014
|
// ExpectedProvider before claiming the action. Never send credentials here.
|
|
@@ -819,10 +1035,10 @@ threadCmd
|
|
|
819
1035
|
if (answers) {
|
|
820
1036
|
for (const answer of Object.values(answers)) {
|
|
821
1037
|
if (!answer || typeof answer !== "object" || Array.isArray(answer))
|
|
822
|
-
throw new Error(
|
|
1038
|
+
throw new Error(`Invalid --answers: each answer must be an object. ${ANSWERS_FORMAT}`);
|
|
823
1039
|
const value = answer;
|
|
824
1040
|
if (!Array.isArray(value.selected_options) || !value.selected_options.every(item => typeof item === "string") || (value.other_text !== undefined && typeof value.other_text !== "string")) {
|
|
825
|
-
throw new Error(
|
|
1041
|
+
throw new Error(`Invalid --answers: selected_options must be a string array and other_text an optional string. ${ANSWERS_FORMAT}`);
|
|
826
1042
|
}
|
|
827
1043
|
}
|
|
828
1044
|
}
|