@enter-pro/enter-cli 0.4.2 → 0.4.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.
- package/README.md +142 -227
- 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 +74 -41
- package/dist/commands/thread.d.ts +27 -0
- package/dist/commands/thread.js +110 -42
- 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 +12 -2
- package/dist/workflow.d.ts +44 -0
- package/dist/workflow.js +34 -0
- package/package.json +7 -3
- package/scripts/install-hosts.mjs +29 -0
- package/skills/enter/SKILL.md +36 -0
- package/skills/enter/references/configuration.md +19 -0
package/dist/commands/thread.js
CHANGED
|
@@ -7,7 +7,8 @@ import { pollUntil, TimeoutError } from "../poll.js";
|
|
|
7
7
|
import { resolveLifecycleStatus } from "../lifecycle.js";
|
|
8
8
|
import { ThreadEvents, isStateEvent } from "../thread-events.js";
|
|
9
9
|
import { safeOutput } from "../safe-output.js";
|
|
10
|
-
import { errorEnvelope } from "../errors.js";
|
|
10
|
+
import { RequestError, errorEnvelope } from "../errors.js";
|
|
11
|
+
import { workflowResult } from "../workflow.js";
|
|
11
12
|
import { registerThreadTasks } from "./thread-tasks.js";
|
|
12
13
|
export const threadCmd = new Command("thread").description("Manage project threads and chat");
|
|
13
14
|
registerThreadTasks(threadCmd);
|
|
@@ -27,6 +28,8 @@ function normalizeAction(raw) {
|
|
|
27
28
|
updated_at: String(raw.updated_at ?? raw.UpdatedAt ?? ""),
|
|
28
29
|
};
|
|
29
30
|
}
|
|
31
|
+
// Single registry: adding a new tool only requires editing one row.
|
|
32
|
+
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
33
|
async function loadToolCallArgs(projectId, action, toolName, context = {}) {
|
|
31
34
|
const turn = String(action.turn);
|
|
32
35
|
const data = await (context.messages ??= client.get(`/v1/projects/${projectId}/thread/messages`, { start_turn: turn, end_turn: turn }, context.signal));
|
|
@@ -73,18 +76,22 @@ async function loadToolCallArgs(projectId, action, toolName, context = {}) {
|
|
|
73
76
|
}
|
|
74
77
|
return { kind: "not_found" };
|
|
75
78
|
}
|
|
79
|
+
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
80
|
const TOOL_HANDLERS = {
|
|
77
81
|
// ask_user_question: surface the questions array directly so callers don't
|
|
78
82
|
// have to fetch + parse thread messages to know what to ask the user.
|
|
79
83
|
ask_user_question: {
|
|
80
84
|
kind: "questions",
|
|
81
|
-
approveSuffix:
|
|
82
|
-
instructions:
|
|
85
|
+
approveSuffix: "--answers '<json>'",
|
|
86
|
+
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
87
|
enrich: async (projectId, action, context) => {
|
|
84
88
|
const args = await loadToolCallArgs(projectId, action, "ask_user_question", context);
|
|
85
89
|
switch (args.kind) {
|
|
86
90
|
case "ok":
|
|
87
|
-
return {
|
|
91
|
+
return {
|
|
92
|
+
questions: args.value.questions ?? [],
|
|
93
|
+
skip_command: `enter-cli thread approve ${projectId} ${action.action_id} --skip-answers`,
|
|
94
|
+
};
|
|
88
95
|
case "parse_error":
|
|
89
96
|
return { questions: { raw_arguments: args.raw_arguments, parse_error: true } };
|
|
90
97
|
case "not_found":
|
|
@@ -94,31 +101,31 @@ const TOOL_HANDLERS = {
|
|
|
94
101
|
},
|
|
95
102
|
supabase_add_secret: {
|
|
96
103
|
kind: "secret",
|
|
97
|
-
approveSuffix:
|
|
98
|
-
instructions: "
|
|
104
|
+
approveSuffix: `--secret-name <NAME> --secret-value-stdin`,
|
|
105
|
+
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
106
|
},
|
|
100
107
|
supabase_configure_auth_provider: {
|
|
101
108
|
kind: "auth_provider",
|
|
102
|
-
approveSuffix:
|
|
103
|
-
instructions: "Configure
|
|
104
|
-
enrich: async (projectId, action, context) => (
|
|
109
|
+
approveSuffix: "",
|
|
110
|
+
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.",
|
|
111
|
+
enrich: async (projectId, action, context) => authProviderInput(await authProviderFor(projectId, action, context)),
|
|
105
112
|
},
|
|
106
113
|
stripe_enable: {
|
|
107
114
|
kind: "secret",
|
|
108
|
-
approveSuffix:
|
|
109
|
-
instructions:
|
|
115
|
+
approveSuffix: `--secret-value-stdin`,
|
|
116
|
+
instructions: STRIPE_INPUT_INSTRUCTIONS,
|
|
110
117
|
},
|
|
111
118
|
stripe_update_key_and_migrate: {
|
|
112
119
|
kind: "secret",
|
|
113
|
-
approveSuffix:
|
|
114
|
-
instructions:
|
|
120
|
+
approveSuffix: `--secret-value-stdin`,
|
|
121
|
+
instructions: `${STRIPE_INPUT_INSTRUCTIONS} Product IDs come from the pending tool call.`,
|
|
115
122
|
},
|
|
116
123
|
// confirm_plan_mode: surface the plan text directly on the action so callers
|
|
117
124
|
// don't have to fetch + parse thread messages themselves.
|
|
118
125
|
confirm_plan_mode: {
|
|
119
126
|
kind: "none",
|
|
120
|
-
approveSuffix:
|
|
121
|
-
instructions: "Plan is included in this action's `plan` field.
|
|
127
|
+
approveSuffix: "",
|
|
128
|
+
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
129
|
enrich: async (projectId, action, context) => {
|
|
123
130
|
const args = await loadToolCallArgs(projectId, action, "confirm_plan_mode", context);
|
|
124
131
|
switch (args.kind) {
|
|
@@ -141,7 +148,7 @@ const TOOL_HANDLERS = {
|
|
|
141
148
|
};
|
|
142
149
|
const DEFAULT_HANDLER = {
|
|
143
150
|
kind: "none",
|
|
144
|
-
approveSuffix:
|
|
151
|
+
approveSuffix: "",
|
|
145
152
|
instructions: "No additional input required. Approve only within the user-authorized scope; input_kind none does not itself grant authorization.",
|
|
146
153
|
};
|
|
147
154
|
const FEATURE_ENABLE_ROUTES = new Map([
|
|
@@ -156,6 +163,25 @@ const AUTH_PROVIDER_FIELDS = {
|
|
|
156
163
|
alipay: ["enabled", "app_id", "private_key"],
|
|
157
164
|
feishu: ["enabled", "app_id", "app_secret"],
|
|
158
165
|
};
|
|
166
|
+
// Input requirements are separate from optional provider settings. Never put
|
|
167
|
+
// stored configuration values (including masked secrets) in action metadata.
|
|
168
|
+
const AUTH_PROVIDER_INPUTS = {
|
|
169
|
+
google: ["client_ids", "client_secret"],
|
|
170
|
+
wechat: ["client_id", "client_secret"],
|
|
171
|
+
alipay: ["app_id", "private_key"],
|
|
172
|
+
feishu: ["app_id", "app_secret"],
|
|
173
|
+
};
|
|
174
|
+
function authProviderInput(provider) {
|
|
175
|
+
return {
|
|
176
|
+
provider,
|
|
177
|
+
configuration: {
|
|
178
|
+
input_surface: "enter_project_auth_form",
|
|
179
|
+
input_methods: ["auth_config_stdin", "enter_project_auth_form"],
|
|
180
|
+
fields: AUTH_PROVIDER_INPUTS[provider],
|
|
181
|
+
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.",
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
159
185
|
async function authProviderFor(projectId, action, context = {}) {
|
|
160
186
|
if (!action.tool_call_id)
|
|
161
187
|
throw new Error("Auth provider action is missing its tool_call_id");
|
|
@@ -183,7 +209,7 @@ function handlerFor(toolName) {
|
|
|
183
209
|
}
|
|
184
210
|
function buildApproveCommand(projectId, action) {
|
|
185
211
|
const base = `enter-cli thread approve ${projectId} ${action.action_id}`;
|
|
186
|
-
const suffix = handlerFor(action.tool_name).approveSuffix
|
|
212
|
+
const suffix = handlerFor(action.tool_name).approveSuffix;
|
|
187
213
|
return suffix ? `${base} ${suffix}` : base;
|
|
188
214
|
}
|
|
189
215
|
async function fetchPendingActions(projectId, actionIds = [], signal) {
|
|
@@ -217,8 +243,7 @@ threadCmd
|
|
|
217
243
|
content = opts.message;
|
|
218
244
|
}
|
|
219
245
|
else {
|
|
220
|
-
|
|
221
|
-
process.exit(1);
|
|
246
|
+
throw new Error("Provide non-empty input using --message, --file or --stdin");
|
|
222
247
|
}
|
|
223
248
|
if (!content.trim())
|
|
224
249
|
throw new Error("Message must not be empty");
|
|
@@ -241,6 +266,7 @@ threadCmd
|
|
|
241
266
|
.option("--end-turn <n>", "End turn number")
|
|
242
267
|
.option("--turn <n>", "Get messages for a specific turn (shorthand for --start-turn N --end-turn N)")
|
|
243
268
|
.option("--latest", "Get messages from the most recent turn (most common usage)")
|
|
269
|
+
.option("--text", "Return assistant text with turn numbers, without raw event envelopes")
|
|
244
270
|
.option("--tail <n>", "Get messages from the last N turns")
|
|
245
271
|
.option("--follow", "Stream NDJSON events over WebSocket; no automatic approval")
|
|
246
272
|
.option("--cursor <id>", "Resume after a previously returned event ID")
|
|
@@ -249,6 +275,8 @@ threadCmd
|
|
|
249
275
|
.option("--max-events <n>", "Stop --follow after N events")
|
|
250
276
|
.action(async (id, opts, cmd) => {
|
|
251
277
|
if (opts.follow) {
|
|
278
|
+
if (opts.text)
|
|
279
|
+
throw new Error("--text is not supported with --follow");
|
|
252
280
|
await followThreadStream(id, opts);
|
|
253
281
|
return;
|
|
254
282
|
}
|
|
@@ -264,7 +292,7 @@ threadCmd
|
|
|
264
292
|
const resp = turnsData;
|
|
265
293
|
const turns = resp.turns || [];
|
|
266
294
|
if (turns.length === 0) {
|
|
267
|
-
|
|
295
|
+
print(getFormat(cmd), { messages: [] });
|
|
268
296
|
return;
|
|
269
297
|
}
|
|
270
298
|
if (opts.latest) {
|
|
@@ -283,15 +311,24 @@ threadCmd
|
|
|
283
311
|
}
|
|
284
312
|
}
|
|
285
313
|
if (!startTurn) {
|
|
286
|
-
|
|
287
|
-
process.exit(1);
|
|
314
|
+
throw new Error("Specify --start-turn, --turn, --latest, --tail, or --follow");
|
|
288
315
|
}
|
|
289
316
|
const params = {};
|
|
290
317
|
params.start_turn = startTurn;
|
|
291
318
|
if (endTurn)
|
|
292
319
|
params.end_turn = endTurn;
|
|
293
320
|
const data = await client.get(`/v1/projects/${id}/thread/messages`, params);
|
|
294
|
-
|
|
321
|
+
if (opts.text) {
|
|
322
|
+
const rows = Array.isArray(data) ? data : data.messages ?? [];
|
|
323
|
+
const messages = rows.flatMap((row) => {
|
|
324
|
+
const content = row?.detail?.assistant_content?.content;
|
|
325
|
+
return row?.message_type === "assistant_content" && typeof content === "string"
|
|
326
|
+
? [{ turn: row.turn, text: content }] : [];
|
|
327
|
+
});
|
|
328
|
+
print(getFormat(cmd), safeOutput({ messages }));
|
|
329
|
+
}
|
|
330
|
+
else
|
|
331
|
+
print(getFormat(cmd), data);
|
|
295
332
|
});
|
|
296
333
|
async function followThreadStream(projectId, opts) {
|
|
297
334
|
const timeout = positiveNumber(opts.timeout ?? "60", "--timeout") * 1000;
|
|
@@ -364,12 +401,13 @@ export function threadInteraction(id, { taskId, chatId, turn, requireBuild } = {
|
|
|
364
401
|
const target = (taskId ? ` --task-id ${taskId}` : turn === undefined ? "" : ` --turn ${turn}`)
|
|
365
402
|
+ (chatId ? ` --chat-id ${chatId}` : "") + (requireBuild ? " --require-build" : "");
|
|
366
403
|
return {
|
|
404
|
+
workflow: workflowResult({ status: "running", task_id: taskId, turn: { turn, chat_id: chatId } }),
|
|
367
405
|
monitoring_required: true,
|
|
368
|
-
wait_command: `enter-cli --output json thread wait ${id}${target}`,
|
|
369
|
-
watch_command: `enter-cli thread watch ${id}${target}`,
|
|
370
|
-
status_command: `enter-cli --output json thread status ${id}${target}`,
|
|
406
|
+
wait_command: `enter-cli --output json thread wait ${id}${target} --timeout 10`,
|
|
407
|
+
watch_command: `enter-cli thread watch ${id}${target} --timeout 60`,
|
|
408
|
+
status_command: `enter-cli --output json thread status ${id}${target} --timeout 10`,
|
|
371
409
|
follow_up_command: `enter-cli --output json thread chat ${id}${chatId ? ` --chat-id ${chatId}` : ""} --file <message-file>`,
|
|
372
|
-
instructions: "Accepted or timeout is not completion. Follow this task until blocked, failed or completed. Forward questions unchanged; approve only within existing authorization. Watching never approves or cancels remote work.
|
|
410
|
+
instructions: "Accepted or timeout is not completion. Follow this task until blocked, failed or completed. Forward questions unchanged; approve only within existing authorization. Use short foreground waits and handle new user messages before continuing observation. Use background watch only with non-blocking output collection and completion notifications; do not follow it with a long blocking job wait. Observation timeout is normal: keep the same task and bounded wait, rather than increasing timeouts. CLI observation timeout and host job-wait timeout are separate limits, not elapsed time. Watching never approves or cancels remote work.",
|
|
373
411
|
};
|
|
374
412
|
}
|
|
375
413
|
function positiveNumber(value, option, integer = false) {
|
|
@@ -386,9 +424,10 @@ async function readThreadSnapshot(id, turnNumber, signal, taskId, chatId) {
|
|
|
386
424
|
: turnNumber === undefined ? turns[0] : turns.find(t => Number(t.turn) === turnNumber);
|
|
387
425
|
if (!selected && taskId) {
|
|
388
426
|
const queue = await client.get(`/v1/projects/${id}/thread/tasks`, { simple: "true", ...(chatId ? { chat_id: chatId } : {}) }, signal);
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
427
|
+
const queued = queue.task_ids?.includes(taskId);
|
|
428
|
+
return { status: queued ? "queued" : "unknown", project_id: id, turn: null,
|
|
429
|
+
reason: queued ? "task_queued" : "task_not_observed",
|
|
430
|
+
...(!queued ? {
|
|
392
431
|
correlation: "unavailable",
|
|
393
432
|
project_activity: turns[0] ? pick(turns[0], ["id", "turn", "status", "chat_id", "commit_id"]) : null,
|
|
394
433
|
project_status_command: `enter-cli --output json thread status ${id}${chatId ? ` --chat-id ${chatId}` : ""}`,
|
|
@@ -435,18 +474,22 @@ async function readThreadSnapshot(id, turnNumber, signal, taskId, chatId) {
|
|
|
435
474
|
return { ...base, status: "completed", project: { ...project, lifecycle_status: resolveLifecycleStatus(project) } };
|
|
436
475
|
}
|
|
437
476
|
function projectSnapshot(snapshot, compact = false) {
|
|
477
|
+
const workflow = workflowResult(snapshot);
|
|
438
478
|
const result = { ...snapshot };
|
|
439
479
|
if (compact && snapshot.turn)
|
|
440
480
|
result.turn = pick(snapshot.turn, ["id", "turn", "status", "turn_name", "created_at", "updated_at", "commit_id", "chat_id"]);
|
|
481
|
+
if (snapshot.status === "completed" && snapshot.turn) {
|
|
482
|
+
result.messages_command = `enter-cli --output json thread messages ${snapshot.project_id} --turn ${snapshot.turn.turn} --text`;
|
|
483
|
+
}
|
|
441
484
|
if (snapshot.project) {
|
|
442
485
|
const p = snapshot.project;
|
|
443
486
|
if (compact)
|
|
444
487
|
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);
|
|
488
|
+
result.build_matches_turn = workflow.build?.matches_task ?? false;
|
|
447
489
|
const supabase = p.supabase;
|
|
448
490
|
result.integrations = { cloud: supabase?.status ?? "unknown", ai: p.ai_connection_state ?? (p.ai_capability_enabled === true ? "enabled" : "unknown") };
|
|
449
491
|
}
|
|
492
|
+
result.workflow = workflow;
|
|
450
493
|
return result;
|
|
451
494
|
}
|
|
452
495
|
// Owns observation and lifetime only; callers choose JSON or NDJSON rendering.
|
|
@@ -491,7 +534,19 @@ async function observeThread(id, opts, wait, onSnapshot) {
|
|
|
491
534
|
throw stream.failure;
|
|
492
535
|
const revision = stream?.revision ?? 0;
|
|
493
536
|
querying = true;
|
|
494
|
-
|
|
537
|
+
try {
|
|
538
|
+
last = await readThreadSnapshot(id, target, controller.signal, opts.taskId, opts.chatId);
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
// A read outage does not end remote work. Keep bounded observation alive;
|
|
542
|
+
// never retry writes or authentication/permission failures here.
|
|
543
|
+
if (!wait || controller.signal.aborted || !(error instanceof RequestError) || !error.retryable)
|
|
544
|
+
throw error;
|
|
545
|
+
querying = false;
|
|
546
|
+
emit({ ...errorEnvelope(error), observation_retrying: true, last_observed: true });
|
|
547
|
+
await delay(1000, undefined, { signal: controller.signal });
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
495
550
|
if (opts.requireBuild && last.status === "completed") {
|
|
496
551
|
const build = last.project?.build_status;
|
|
497
552
|
const matches = Boolean(last.turn?.commit_id && build?.commit_id === last.turn.commit_id);
|
|
@@ -600,7 +655,7 @@ threadCmd
|
|
|
600
655
|
else {
|
|
601
656
|
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
602
657
|
}
|
|
603
|
-
|
|
658
|
+
printResult(getFormat(cmd), { path }, `Diff written to ${path}`);
|
|
604
659
|
return;
|
|
605
660
|
}
|
|
606
661
|
print(getFormat(cmd), data);
|
|
@@ -608,10 +663,11 @@ threadCmd
|
|
|
608
663
|
threadCmd
|
|
609
664
|
.command("cancel <project_id>")
|
|
610
665
|
.description("Cancel the currently running turn. No-op if nothing is running.")
|
|
611
|
-
.
|
|
666
|
+
.option("--chat-id <id>", "Scope cancellation to this chat")
|
|
667
|
+
.action(async (id, opts, cmd) => {
|
|
612
668
|
// Pre-check the latest turn — the server returns code 1000 even when
|
|
613
669
|
// nothing is running, which is misleading. We want a clean idempotent no-op.
|
|
614
|
-
const turnsResp = await client.get(`/v1/projects/${id}/thread/turns
|
|
670
|
+
const turnsResp = await client.get(`/v1/projects/${id}/thread/turns`, opts.chatId ? { chat_id: opts.chatId } : undefined);
|
|
615
671
|
const turns = turnsResp.turns ?? [];
|
|
616
672
|
const latest = turns[0];
|
|
617
673
|
const isRunning = latest && !TERMINAL_TURN_STATUSES.has(String(latest.status ?? ""));
|
|
@@ -622,11 +678,13 @@ threadCmd
|
|
|
622
678
|
reason: "no_running_turn",
|
|
623
679
|
latest_turn: latest?.turn ?? null,
|
|
624
680
|
latest_status: latest?.status ?? null,
|
|
681
|
+
tasks_command: `enter-cli --output json thread tasks ${id}${opts.chatId ? ` --chat-id ${opts.chatId}` : ""}`,
|
|
625
682
|
}, `Nothing to cancel (latest turn ${latest?.turn ?? "?"} status: ${latest?.status ?? "unknown"}).`);
|
|
626
683
|
return;
|
|
627
684
|
}
|
|
628
|
-
|
|
629
|
-
|
|
685
|
+
const chatId = opts.chatId ?? latest.chat_id;
|
|
686
|
+
await client.post(`/v1/projects/${id}/thread/cancel`, chatId ? { chat_id: chatId } : undefined);
|
|
687
|
+
printResult(format, { cancelled: true, turn: latest.turn, ...(chatId ? { chat_id: chatId } : {}) }, `Cancelled turn ${latest.turn}.`);
|
|
630
688
|
});
|
|
631
689
|
threadCmd
|
|
632
690
|
.command("restore <project_id>")
|
|
@@ -756,7 +814,7 @@ threadCmd
|
|
|
756
814
|
.option("--secret-value-stdin", "Read the secret value from stdin instead of process arguments")
|
|
757
815
|
.option("--auth-config-stdin", "Read configuration JSON for the action's auth provider from stdin; credentials never go into the approval response")
|
|
758
816
|
.option("--tool-result <result>", "Custom tool result string")
|
|
759
|
-
.option("--answers <json>",
|
|
817
|
+
.option("--answers <json>", `Answers for ask_user_question. ${ANSWERS_FORMAT}`)
|
|
760
818
|
.option("--skip-answers", "Skip all questions for ask_user_question (sets skipped: true)")
|
|
761
819
|
.action(async (projectId, actionIdArg, opts, cmd) => {
|
|
762
820
|
const format = getFormat(cmd);
|
|
@@ -792,7 +850,17 @@ threadCmd
|
|
|
792
850
|
}
|
|
793
851
|
const data = await client.get(configPath);
|
|
794
852
|
if (data.config?.providers?.[provider]?.enabled !== true) {
|
|
795
|
-
|
|
853
|
+
print(format, {
|
|
854
|
+
status: "blocked", reason: "auth_provider_configuration_required",
|
|
855
|
+
project_id: projectId, approved: false,
|
|
856
|
+
actions: [{
|
|
857
|
+
action_id: action.action_id, tool_name: action.tool_name, turn: action.turn,
|
|
858
|
+
input_kind: "auth_provider", ...authProviderInput(provider),
|
|
859
|
+
approve_command: buildApproveCommand(projectId, action),
|
|
860
|
+
}],
|
|
861
|
+
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.",
|
|
862
|
+
});
|
|
863
|
+
return;
|
|
796
864
|
}
|
|
797
865
|
// The backend additionally verifies stored credentials and the card's
|
|
798
866
|
// ExpectedProvider before claiming the action. Never send credentials here.
|
|
@@ -819,10 +887,10 @@ threadCmd
|
|
|
819
887
|
if (answers) {
|
|
820
888
|
for (const answer of Object.values(answers)) {
|
|
821
889
|
if (!answer || typeof answer !== "object" || Array.isArray(answer))
|
|
822
|
-
throw new Error(
|
|
890
|
+
throw new Error(`Invalid --answers: each answer must be an object. ${ANSWERS_FORMAT}`);
|
|
823
891
|
const value = answer;
|
|
824
892
|
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(
|
|
893
|
+
throw new Error(`Invalid --answers: selected_options must be a string array and other_text an optional string. ${ANSWERS_FORMAT}`);
|
|
826
894
|
}
|
|
827
895
|
}
|
|
828
896
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -6,7 +6,7 @@ export const whoamiCmd = new Command("whoami")
|
|
|
6
6
|
.description("Show current user info")
|
|
7
7
|
.action(async (_opts, cmd) => {
|
|
8
8
|
if (!isAuthenticated()) {
|
|
9
|
-
throw new Error("Not authenticated. Run `enter login` or set ENTER_API_KEY environment variable.");
|
|
9
|
+
throw new Error("Not authenticated. Run `enter-cli login` or set ENTER_API_KEY environment variable.");
|
|
10
10
|
}
|
|
11
11
|
const data = await client.get("/v1/users/info");
|
|
12
12
|
const format = cmd.optsWithGlobals().output || "json";
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import * as client from "../client.js";
|
|
3
|
-
import { print,
|
|
3
|
+
import { print, printResult, printTable, pickList, getFormat } from "../output.js";
|
|
4
4
|
export const workspaceCmd = new Command("workspace")
|
|
5
5
|
.alias("ws")
|
|
6
6
|
.description("Manage workspaces");
|
|
7
|
-
function getFormat(cmd) {
|
|
8
|
-
return cmd.optsWithGlobals().output || "json";
|
|
9
|
-
}
|
|
10
7
|
workspaceCmd
|
|
11
8
|
.command("list")
|
|
12
9
|
.description("List workspaces")
|
|
@@ -68,9 +65,9 @@ workspaceCmd
|
|
|
68
65
|
workspaceCmd
|
|
69
66
|
.command("delete <workspace_id>")
|
|
70
67
|
.description("Delete a workspace")
|
|
71
|
-
.action(async (id) => {
|
|
68
|
+
.action(async (id, _opts, cmd) => {
|
|
72
69
|
await client.del(`/v1/workspaces/${id}`);
|
|
73
|
-
|
|
70
|
+
printResult(getFormat(cmd), { deleted: true, workspace_id: id }, "Workspace deleted successfully.");
|
|
74
71
|
});
|
|
75
72
|
// Members subcommand group
|
|
76
73
|
const membersCmd = new Command("members").description("Manage workspace members");
|
|
@@ -118,7 +115,7 @@ membersCmd
|
|
|
118
115
|
.description("Remove a member from workspace")
|
|
119
116
|
.option("--email <email>", "Member email")
|
|
120
117
|
.option("--user-id <id>", "Member user ID")
|
|
121
|
-
.action(async (id, opts) => {
|
|
118
|
+
.action(async (id, opts, cmd) => {
|
|
122
119
|
if (!opts.email && !opts.userId) {
|
|
123
120
|
throw new Error("--email or --user-id is required");
|
|
124
121
|
}
|
|
@@ -128,7 +125,7 @@ membersCmd
|
|
|
128
125
|
if (opts.userId)
|
|
129
126
|
body.user_id = Number(opts.userId);
|
|
130
127
|
await client.post(`/v1/workspaces/${id}/members/remove`, body);
|
|
131
|
-
|
|
128
|
+
printResult(getFormat(cmd), { removed: true, workspace_id: id }, "Member removed successfully.");
|
|
132
129
|
});
|
|
133
130
|
membersCmd
|
|
134
131
|
.command("update-role <workspace_id>")
|
|
@@ -155,9 +152,9 @@ membersCmd
|
|
|
155
152
|
membersCmd
|
|
156
153
|
.command("leave <workspace_id>")
|
|
157
154
|
.description("Leave a workspace")
|
|
158
|
-
.action(async (id) => {
|
|
155
|
+
.action(async (id, _opts, cmd) => {
|
|
159
156
|
await client.post(`/v1/workspaces/${id}/leave`);
|
|
160
|
-
|
|
157
|
+
printResult(getFormat(cmd), { left: true, workspace_id: id }, "Left workspace successfully.");
|
|
161
158
|
});
|
|
162
159
|
workspaceCmd.addCommand(membersCmd);
|
|
163
160
|
// Credits subcommand group
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -8,7 +8,6 @@ const defaults = {
|
|
|
8
8
|
api_url: "https://api.enter.pro",
|
|
9
9
|
base_path: "/code/api",
|
|
10
10
|
output: "json",
|
|
11
|
-
default_workspace: "",
|
|
12
11
|
};
|
|
13
12
|
function ensureConfigDir() {
|
|
14
13
|
if (!existsSync(CONFIG_DIR)) {
|
|
@@ -32,8 +31,6 @@ function getEnvOverrides() {
|
|
|
32
31
|
overrides.base_path = process.env.ENTER_BASE_PATH;
|
|
33
32
|
if (process.env.ENTER_OUTPUT)
|
|
34
33
|
overrides.output = process.env.ENTER_OUTPUT;
|
|
35
|
-
if (process.env.ENTER_DEFAULT_WORKSPACE)
|
|
36
|
-
overrides.default_workspace = process.env.ENTER_DEFAULT_WORKSPACE;
|
|
37
34
|
return overrides;
|
|
38
35
|
}
|
|
39
36
|
export function configDir() {
|
|
@@ -42,15 +39,24 @@ export function configDir() {
|
|
|
42
39
|
export function loadConfig() {
|
|
43
40
|
const fileConfig = loadFromFile();
|
|
44
41
|
const envOverrides = getEnvOverrides();
|
|
45
|
-
|
|
42
|
+
const merged = { ...defaults, ...fileConfig, ...envOverrides };
|
|
43
|
+
return Object.fromEntries(Object.keys(defaults).map(key => [key, merged[key]]));
|
|
46
44
|
}
|
|
47
45
|
export function setConfig(key, value) {
|
|
46
|
+
validateKey(key);
|
|
47
|
+
if (key === "output" && !["json", "yaml", "table"].includes(value))
|
|
48
|
+
throw new Error("output must be json, yaml or table");
|
|
48
49
|
ensureConfigDir();
|
|
49
50
|
const current = loadFromFile();
|
|
50
51
|
current[key] = value;
|
|
51
52
|
writeFileSync(CONFIG_FILE, yaml.dump(current), "utf-8");
|
|
52
53
|
}
|
|
54
|
+
function validateKey(key) {
|
|
55
|
+
if (!Object.hasOwn(defaults, key))
|
|
56
|
+
throw new Error(`Unsupported setting "${key}". Supported settings: ${Object.keys(defaults).join(", ")}. Pass workspace IDs explicitly to project commands.`);
|
|
57
|
+
}
|
|
53
58
|
export function getConfig(key) {
|
|
59
|
+
validateKey(key);
|
|
54
60
|
const cfg = loadConfig();
|
|
55
61
|
return cfg[key] || "";
|
|
56
62
|
}
|
package/dist/output.d.ts
CHANGED
|
@@ -6,21 +6,5 @@ export declare function printMessage(msg: string): void;
|
|
|
6
6
|
export declare function printResult(format: string, structured: unknown, message: string): void;
|
|
7
7
|
import type { Command } from "commander";
|
|
8
8
|
export declare function getFormat(cmd: Command): string;
|
|
9
|
-
export declare function printError(err: Error | string): void;
|
|
10
9
|
export declare function pick<T extends Record<string, unknown>>(obj: T, keys: string[]): Record<string, unknown>;
|
|
11
10
|
export declare function pickList(items: Record<string, unknown>[], keys: string[]): Record<string, unknown>[];
|
|
12
|
-
export interface ListEnvelope {
|
|
13
|
-
items: unknown[];
|
|
14
|
-
total?: number;
|
|
15
|
-
page?: number;
|
|
16
|
-
page_size?: number;
|
|
17
|
-
}
|
|
18
|
-
export interface TableConfig {
|
|
19
|
-
headers: string[];
|
|
20
|
-
rowMapper: (item: Record<string, unknown>) => string[];
|
|
21
|
-
}
|
|
22
|
-
export declare function printList(format: string, data: ListEnvelope, tableConfig?: TableConfig): void;
|
|
23
|
-
export declare function printSingle(format: string, data: unknown, tableConfig?: {
|
|
24
|
-
headers: string[];
|
|
25
|
-
rowMapper: (item: Record<string, unknown>) => string[];
|
|
26
|
-
}): void;
|
package/dist/output.js
CHANGED
|
@@ -59,9 +59,6 @@ export function printResult(format, structured, message) {
|
|
|
59
59
|
export function getFormat(cmd) {
|
|
60
60
|
return cmd.optsWithGlobals().output || "json";
|
|
61
61
|
}
|
|
62
|
-
export function printError(err) {
|
|
63
|
-
console.error(`Error: ${typeof err === "string" ? err : err.message}`);
|
|
64
|
-
}
|
|
65
62
|
export function pick(obj, keys) {
|
|
66
63
|
const result = {};
|
|
67
64
|
for (const key of keys) {
|
|
@@ -72,18 +69,3 @@ export function pick(obj, keys) {
|
|
|
72
69
|
export function pickList(items, keys) {
|
|
73
70
|
return items.map((item) => pick(item, keys));
|
|
74
71
|
}
|
|
75
|
-
export function printList(format, data, tableConfig) {
|
|
76
|
-
if (format === "table" && tableConfig) {
|
|
77
|
-
const rows = data.items.map(tableConfig.rowMapper);
|
|
78
|
-
printTable(tableConfig.headers, rows);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
print(format, data);
|
|
82
|
-
}
|
|
83
|
-
export function printSingle(format, data, tableConfig) {
|
|
84
|
-
if (format === "table" && tableConfig) {
|
|
85
|
-
printTable(tableConfig.headers, [tableConfig.rowMapper(data)]);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
print(format, data);
|
|
89
|
-
}
|
package/dist/poll.d.ts
CHANGED
package/dist/poll.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
1
2
|
export class TimeoutError extends Error {
|
|
2
3
|
constructor(elapsedMs) {
|
|
3
4
|
super(`Timed out after ${Math.round(elapsedMs / 1000)}s`);
|
|
@@ -10,6 +11,7 @@ export async function pollUntil(fetcher, predicate, options) {
|
|
|
10
11
|
const onTick = options?.onTick;
|
|
11
12
|
const start = Date.now();
|
|
12
13
|
while (true) {
|
|
14
|
+
options?.signal?.throwIfAborted();
|
|
13
15
|
const data = await fetcher();
|
|
14
16
|
const elapsed = Date.now() - start;
|
|
15
17
|
if (predicate(data)) {
|
|
@@ -19,6 +21,6 @@ export async function pollUntil(fetcher, predicate, options) {
|
|
|
19
21
|
throw new TimeoutError(elapsed);
|
|
20
22
|
}
|
|
21
23
|
onTick?.(elapsed, data);
|
|
22
|
-
await
|
|
24
|
+
await delay(Math.min(intervalMs, Math.max(0, timeoutMs - elapsed)), undefined, { signal: options?.signal });
|
|
23
25
|
}
|
|
24
26
|
}
|
package/dist/thread-events.d.ts
CHANGED
package/dist/thread-events.js
CHANGED
|
@@ -2,7 +2,7 @@ import WebSocket from "ws";
|
|
|
2
2
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
3
3
|
import { getProxyForUrl } from "proxy-from-env";
|
|
4
4
|
import { baseURL } from "./config.js";
|
|
5
|
-
import {
|
|
5
|
+
import { getValidToken } from "./auth.js";
|
|
6
6
|
import { RequestError } from "./errors.js";
|
|
7
7
|
export function streamURL(base, projectId, options) {
|
|
8
8
|
const url = new URL(`${base.replace(/\/$/, "")}/v1/projects/${encodeURIComponent(projectId)}/thread/stream`);
|
|
@@ -77,10 +77,20 @@ export class ThreadEvents {
|
|
|
77
77
|
this.wake();
|
|
78
78
|
}
|
|
79
79
|
connect() {
|
|
80
|
+
void this.connectAuthenticated().catch(error => {
|
|
81
|
+
if (this.stopped)
|
|
82
|
+
return;
|
|
83
|
+
this.failure = new RequestError("STREAM_SETUP_ERROR", error instanceof Error ? error.message : "Could not initialize Enter event stream.");
|
|
84
|
+
this.setMode("polling", "stream_setup_failed");
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async connectAuthenticated() {
|
|
80
88
|
if (this.stopped)
|
|
81
89
|
return;
|
|
82
90
|
const url = streamURL(baseURL(), this.projectId, { ...this.options, cursor: this.cursor });
|
|
83
|
-
const token =
|
|
91
|
+
const token = await getValidToken(this.options.signal);
|
|
92
|
+
if (this.stopped)
|
|
93
|
+
return;
|
|
84
94
|
// proxy-from-env honors NO_PROXY, including loopback fixture servers.
|
|
85
95
|
const proxy = process.env.NODE_USE_ENV_PROXY === "0" ? "" : getProxyForUrl(url.toString().replace(/^ws/, "http"));
|
|
86
96
|
const socket = new WebSocket(url, {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type ThreadStatus = 'idle' | 'running' | 'queued' | 'pending' | 'blocked' | 'completed' | 'failed' | 'unknown';
|
|
2
|
+
export type InputKind = 'none' | 'secret' | 'questions' | 'auth_provider';
|
|
3
|
+
export interface WorkflowSnapshot {
|
|
4
|
+
status: ThreadStatus;
|
|
5
|
+
task_id?: unknown;
|
|
6
|
+
turn?: Record<string, unknown> | null;
|
|
7
|
+
actions?: Record<string, unknown>[];
|
|
8
|
+
project?: Record<string, unknown>;
|
|
9
|
+
reason?: string;
|
|
10
|
+
interrupted?: boolean;
|
|
11
|
+
wait_timed_out?: boolean;
|
|
12
|
+
observation_retrying?: boolean;
|
|
13
|
+
error?: {
|
|
14
|
+
retryable?: boolean;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Host-facing workflow decisions, independent of transport and backend phases. */
|
|
18
|
+
export declare function workflowResult(snapshot: WorkflowSnapshot): {
|
|
19
|
+
actions?: {
|
|
20
|
+
id: unknown;
|
|
21
|
+
kind: unknown;
|
|
22
|
+
decision: string;
|
|
23
|
+
required_fields: {};
|
|
24
|
+
submit_command: unknown;
|
|
25
|
+
}[] | undefined;
|
|
26
|
+
build?: {
|
|
27
|
+
commit: {} | null;
|
|
28
|
+
matches_task: boolean;
|
|
29
|
+
success: {} | null;
|
|
30
|
+
} | undefined;
|
|
31
|
+
observation: {
|
|
32
|
+
timed_out: boolean;
|
|
33
|
+
retrying: boolean;
|
|
34
|
+
interrupted: boolean;
|
|
35
|
+
};
|
|
36
|
+
reason?: string | undefined;
|
|
37
|
+
state: string;
|
|
38
|
+
task: {
|
|
39
|
+
id: {} | null;
|
|
40
|
+
turn: {} | null;
|
|
41
|
+
chat_id: {} | null;
|
|
42
|
+
};
|
|
43
|
+
next_action: string;
|
|
44
|
+
};
|