@enter-pro/enter-cli 0.4.1 → 0.4.2
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 +252 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +20 -2
- package/dist/client.d.ts +3 -2
- package/dist/client.js +85 -31
- package/dist/commands/login.js +5 -3
- package/dist/commands/project.js +11 -29
- package/dist/commands/thread-tasks.d.ts +2 -0
- package/dist/commands/thread-tasks.js +23 -0
- package/dist/commands/thread.d.ts +27 -0
- package/dist/commands/thread.js +457 -187
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +29 -0
- package/dist/index.js +9 -2
- package/dist/safe-output.d.ts +6 -0
- package/dist/safe-output.js +29 -0
- package/dist/thread-events.d.ts +36 -0
- package/dist/thread-events.js +186 -0
- package/package.json +17 -9
package/README.md
CHANGED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Enter CLI
|
|
2
|
+
|
|
3
|
+
## Production login
|
|
4
|
+
|
|
5
|
+
`enter-cli login` uses the production Auth0 tenant `auth.converge.ai` and the
|
|
6
|
+
production API at `https://api.enter.pro/code/api`. The web app's
|
|
7
|
+
`enter.converge.ai` domain is not the API base URL. Log in again if existing
|
|
8
|
+
credentials were issued by the former `auth.enter.pro` tenant.
|
|
9
|
+
|
|
10
|
+
OAuth and API-key login verify the new token against `/v1/users/info` before
|
|
11
|
+
saving credentials or reporting success. A rejected token leaves existing
|
|
12
|
+
credentials unchanged.
|
|
13
|
+
|
|
14
|
+
## Local verification
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm ci
|
|
18
|
+
npm test
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Tests start HTTP fixture servers on loopback and execute the built CLI in child
|
|
22
|
+
processes with a dummy API key. They do not start Work, connect to Enter, create
|
|
23
|
+
cloud resources, or prove end-to-end Work behavior.
|
|
24
|
+
|
|
25
|
+
### Interactive local simulator
|
|
26
|
+
|
|
27
|
+
Requires Node 18+ and npm; no Docker, Work server, database, or credentials are
|
|
28
|
+
needed. From this repository, build once and start the fixture server:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npm run build
|
|
32
|
+
npm run mock:serve
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
In another terminal in this repository:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm run local:cli -- whoami
|
|
39
|
+
npm run local:cli -- workspace list
|
|
40
|
+
npm run local:cli -- thread status cloud
|
|
41
|
+
npm run local:cli -- thread approve cloud cloud-action
|
|
42
|
+
npm run local:cli -- thread wait cloud --timeout 3
|
|
43
|
+
npm run local:cli -- project get cloud
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The server binds only to `127.0.0.1:43180`. Set `MOCK_ENTER_PORT` in both terminals
|
|
47
|
+
to use another port. `local:cli` always targets loopback and injects only the
|
|
48
|
+
dummy mock key, even if the shell has real `ENTER_API_URL` or `ENTER_API_KEY`
|
|
49
|
+
values. Prefer this wrapper for manual simulation; the normal CLI still uses
|
|
50
|
+
its ordinary configuration. Stop the server with Ctrl-C. Restarting resets all
|
|
51
|
+
in-memory scenarios; no resources are written to Enter or cloud providers.
|
|
52
|
+
|
|
53
|
+
| Project ID | Scenario |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `plan` | Plan approval (`plan-action`) with plan text |
|
|
56
|
+
| `cloud` | Supabase dedicated enable flow (`cloud-action`) |
|
|
57
|
+
| `ai` | AI capability dedicated enable flow (`ai-action`) |
|
|
58
|
+
| `secret` | Secret input (`secret-action`); use fake values only |
|
|
59
|
+
| `questions` | Structured question (`questions-action`) |
|
|
60
|
+
| `subscription` | Feature enable returns `VIP_REQUIRED` |
|
|
61
|
+
| `running` | Never completes; exercise wait timeout |
|
|
62
|
+
| `failed` | Backend turn failed |
|
|
63
|
+
| `http-error` | Backend HTTP 503 |
|
|
64
|
+
| `stalled` | HTTP request never returns; exercise request deadline |
|
|
65
|
+
|
|
66
|
+
Example input-card and timeout checks:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
printf '%s\n' 'dummy-test-value' | npm run local:cli -- thread approve secret secret-action --secret-name LOCAL_TEST_KEY --secret-value-stdin
|
|
70
|
+
npm run local:cli -- thread approve questions questions-action --skip-answers
|
|
71
|
+
npm run local:cli -- thread wait running --timeout 1
|
|
72
|
+
npm run local:cli -- thread wait stalled --timeout 1
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`test/local-workflow.test.mjs` starts fresh ephemeral servers automatically and
|
|
76
|
+
checks approval, simulated resource flags, continuation turns, rejection,
|
|
77
|
+
subscription refusal, and errors. It is a CLI-to-simulator integration suite,
|
|
78
|
+
not a real provisioning or Work UI test. The simulator intentionally supports
|
|
79
|
+
only the listed workflows; unsupported routes fail instead of fabricating a
|
|
80
|
+
successful response. It retains request methods/paths but never request bodies
|
|
81
|
+
or secret values.
|
|
82
|
+
|
|
83
|
+
For later real-backend testing, `.env.integration.example` documents the needed
|
|
84
|
+
variables. Copy it to ignored `.env.integration.local` and configure a dedicated
|
|
85
|
+
test backend only when ready. The file is not loaded automatically; neither the
|
|
86
|
+
simulator nor the mock CLI wrapper reads it.
|
|
87
|
+
|
|
88
|
+
## Agent integration: CLI + Skill
|
|
89
|
+
|
|
90
|
+
The CLI executes Enter operations and streams structured state. The Skill guides
|
|
91
|
+
agents on command selection, following tasks, presenting questions and handling
|
|
92
|
+
approved actions. This works across agent hosts without host-specific hooks.
|
|
93
|
+
Node 18+ is required. Authenticate with `enter-cli login`, or let the host inject
|
|
94
|
+
`ENTER_API_KEY` through its secret environment mechanism.
|
|
95
|
+
|
|
96
|
+
### CLI: submit once, observe the returned task
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
enter-cli --output json thread chat PROJECT_ID --file requirement.txt
|
|
100
|
+
# Copy task_id from the receipt, including for queued follow-ups:
|
|
101
|
+
enter-cli thread watch PROJECT_ID --task-id TASK_ID --timeout 60
|
|
102
|
+
enter-cli --output json thread status PROJECT_ID --task-id TASK_ID
|
|
103
|
+
enter-cli --output json thread wait PROJECT_ID --task-id TASK_ID --timeout 10
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`chat --stdin` also accepts multiline input without shell escaping or putting the
|
|
107
|
+
message into process arguments. Submission returns immediately with `task_id`,
|
|
108
|
+
`submission_status: accepted`, and ready-to-use monitoring commands. You can
|
|
109
|
+
submit follow-ups while Enter is running; acceptance is not completion.
|
|
110
|
+
|
|
111
|
+
- `status` reads one snapshot, with a 30-second total deadline by default.
|
|
112
|
+
- `wait` waits for a pending card or terminal state and emits one JSON result.
|
|
113
|
+
Its default lifetime is 10 seconds.
|
|
114
|
+
- `watch` emits NDJSON snapshots only when meaningful state changes, followed
|
|
115
|
+
by a final `type: result` record. Its default lifetime is 60 seconds.
|
|
116
|
+
|
|
117
|
+
Waiting uses authenticated WebSocket events to trigger authoritative HTTP state
|
|
118
|
+
reads. It reconnects with `last_event_id`, deduplicates replay, and uses HTTP
|
|
119
|
+
polling if the stream is unavailable. Healthy streams have a 30-second safety
|
|
120
|
+
refresh; fallback checks are every 2 seconds plus reconnect transitions. Text
|
|
121
|
+
and argument deltas do not cause repeated full snapshot queries. After a card
|
|
122
|
+
event, a short reconciliation window accounts for delayed card persistence.
|
|
123
|
+
Use `--transport poll` to force HTTP-only operation; `--cursor EVENT_ID` resumes
|
|
124
|
+
a prior stream. Returned `transport` and `cursor` make the active mode visible.
|
|
125
|
+
`transport_reason` distinguishes temporary connection failure, unavailable
|
|
126
|
+
streams, and invalid events. Unsupported endpoints stop reconnecting for the
|
|
127
|
+
current call and use HTTP polling. HTTP 401/403 on the stream returns
|
|
128
|
+
`STREAM_AUTH_ERROR` instead of silently reconnecting. WebSockets honor proxy environment variables and `NO_PROXY`; set
|
|
129
|
+
`NODE_USE_ENV_PROXY=0` to disable proxy use for the stream.
|
|
130
|
+
|
|
131
|
+
Use `--task-id` for submissions and continuation receipts. It follows the exact
|
|
132
|
+
task through queueing, even if an older turn already completed. `--turn N` selects
|
|
133
|
+
a fixed turn instead; the two selectors are mutually exclusive. With neither,
|
|
134
|
+
waiting pins the first observed turn. `--chat-id` scopes chat submission, lookup,
|
|
135
|
+
and continuation commands.
|
|
136
|
+
|
|
137
|
+
Enter can interject a mid-turn message into the active turn and return an external
|
|
138
|
+
message ID instead of a new task ID. The current API does not map that receipt to
|
|
139
|
+
its owning turn. In that case `status --task-id` returns `unknown`, with separate
|
|
140
|
+
`project_activity` and a `project_status_command`; that activity never claims the
|
|
141
|
+
submission succeeded. Inspect the echoed message and resulting change. Do not
|
|
142
|
+
resubmit blindly or infer correlation from the newest turn.
|
|
143
|
+
|
|
144
|
+
`project create` returns continuation commands. `project create --wait` uses the
|
|
145
|
+
same bounded event monitor, yields on cards, and requires a matching successful
|
|
146
|
+
build. `thread wait/status/watch --require-build` enables that build check for an
|
|
147
|
+
existing project; stale successful builds cannot satisfy it. A matching failed
|
|
148
|
+
build returns failure, and timeout preserves progress without cancelling Enter.
|
|
149
|
+
|
|
150
|
+
| Status | Meaning |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `idle` | No turn exists yet |
|
|
153
|
+
| `queued` | The selected task is in the backend queue |
|
|
154
|
+
| `unknown` | The selected submission cannot be correlated; this is not completion |
|
|
155
|
+
| `running` | Selected turn has not terminated and has no pending actions |
|
|
156
|
+
| `blocked` | Inspect `actions` for questions or approval requirements |
|
|
157
|
+
| `completed` | Turn completed; build and deployment remain separate |
|
|
158
|
+
| `failed` | Turn failed, errored or was cancelled |
|
|
159
|
+
| `unknown` | Explicit turn missing or status unavailable |
|
|
160
|
+
|
|
161
|
+
The existing `status` and `wait` commands retain full metadata by default; use
|
|
162
|
+
`--compact` for monitoring fields only. `watch` defaults to compact snapshots
|
|
163
|
+
and accepts `--full`. `build_matches_turn` only becomes true when the build commit matches
|
|
164
|
+
the selected turn's commit; an older successful preview does not prove the new
|
|
165
|
+
turn built successfully. Known credential fields are redacted in monitoring, approval and event output,
|
|
166
|
+
including known JSON-encoded tool argument fields. Ordinary command output and
|
|
167
|
+
arbitrary JSON strings are not rewritten by this monitoring feature.
|
|
168
|
+
|
|
169
|
+
Exit 0 means a successful observation (including `blocked`), not build success.
|
|
170
|
+
Exit 2 means the observation deadline elapsed while work remained pending:
|
|
171
|
+
`wait_timed_out: true`. Exit 1 means failure, unknown state or a failed query.
|
|
172
|
+
A query timeout includes `query_timed_out: true`; retained state is only the last
|
|
173
|
+
complete snapshot. SIGINT/SIGTERM during status/wait/watch returns
|
|
174
|
+
`interrupted: true` with exit 130/143, without labelling it a timeout or cancelling
|
|
175
|
+
remote work.
|
|
176
|
+
|
|
177
|
+
For raw events rather than state snapshots:
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
enter-cli thread messages PROJECT_ID --follow --timeout 60 --cursor EVENT_ID
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
This emits NDJSON on stdout, transport diagnostics on stderr, and supports
|
|
184
|
+
`--max-events N`. It continues across turn boundaries until its deadline or limit.
|
|
185
|
+
Unlike `watch`, raw follow does not synthesize HTTP state when the socket is down.
|
|
186
|
+
|
|
187
|
+
### Skill and host responsibilities
|
|
188
|
+
|
|
189
|
+
The Skill should direct agents to submit once, follow the returned task ID, and
|
|
190
|
+
use bounded `thread watch` or `thread wait` calls. Forward questions with their
|
|
191
|
+
options and selection mode, and approve only within the user's authorization.
|
|
192
|
+
Provide secrets through Enter's secure form or direct CLI stdin.
|
|
193
|
+
|
|
194
|
+
The host remains responsible for scheduling observation and handling incoming
|
|
195
|
+
user messages concurrently. CLI events cannot wake an agent
|
|
196
|
+
whose host has stopped the task. Continue bounded watch calls while work remains
|
|
197
|
+
pending; if the host supports background tasks, schedule a watcher there. Forward
|
|
198
|
+
questions without silently selecting answers, and continue with the task ID
|
|
199
|
+
returned by approval. Do not auto-approve based only on `input_kind: none`.
|
|
200
|
+
|
|
201
|
+
### Errors and retry boundaries
|
|
202
|
+
|
|
203
|
+
`--request-timeout SECONDS` optionally bounds JSON API requests, including
|
|
204
|
+
response bodies. Without this explicit option, ordinary operations retain their
|
|
205
|
+
existing request lifetime; monitoring commands always enforce their own total
|
|
206
|
+
`--timeout`. Transient network errors and HTTP 502/503/504 retry once only for
|
|
207
|
+
thread observation reads (turns, actions, messages and queued tasks).
|
|
208
|
+
Mutations are never automatically replayed. A timed-out write can already have
|
|
209
|
+
reached Enter: `error.outcome_unknown: true` tells the caller to inspect task or
|
|
210
|
+
action state before resubmitting.
|
|
211
|
+
|
|
212
|
+
In JSON mode, command execution errors are JSON on stderr. Status/watch query
|
|
213
|
+
errors accompany their last observed snapshot on stdout. Error fields include
|
|
214
|
+
`code`, `message`, `retryable`, and `outcome_unknown`. Never infer success from a
|
|
215
|
+
missing stderr message.
|
|
216
|
+
|
|
217
|
+
`test/agent-ux.test.mjs` uses local HTTP/WebSocket fixtures to cover event-triggered
|
|
218
|
+
cards, delayed persistence, reconnect and cursor replay, queue tracking, polling
|
|
219
|
+
fallback, deadlines, safe read retries and redaction. These are protocol/CLI integration tests,
|
|
220
|
+
not production cloud-provisioning or a host UI acceptance test.
|
|
221
|
+
|
|
222
|
+
## Approval inputs
|
|
223
|
+
|
|
224
|
+
Feature enable cards use the dedicated feature endpoint and verify that the
|
|
225
|
+
selected action becomes approved. A failed enable request, subscription refusal,
|
|
226
|
+
or failed action must remain an error. Secret cards require both a name and a
|
|
227
|
+
nonempty value; question cards require `--answers` or an explicit `--skip-answers`.
|
|
228
|
+
|
|
229
|
+
OAuth cards (`supabase_configure_auth_provider`) resolve their provider from the
|
|
230
|
+
matching tool call. If the provider was already configured through Enter's secure
|
|
231
|
+
form, ordinary `thread approve PROJECT_ID ACTION_ID` verifies it and submits only
|
|
232
|
+
`auth_provider_result: {provider}`. Otherwise, `--auth-config-stdin` accepts a JSON
|
|
233
|
+
object, saves it through the auth configuration endpoint, then requests approval.
|
|
234
|
+
The backend verifies the stored configuration before accepting the action.
|
|
235
|
+
|
|
236
|
+
| Provider | Configuration fields |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| `google` | `client_ids`, `client_secret`, optional `skip_nonce_checks` |
|
|
239
|
+
| `wechat` | `client_id`, `client_secret` |
|
|
240
|
+
| `alipay` | `app_id`, `private_key` |
|
|
241
|
+
| `feishu` | `app_id`, `app_secret` |
|
|
242
|
+
|
|
243
|
+
Use a secure input mechanism to supply stdin, or complete the secure form in
|
|
244
|
+
Enter. Do not request credentials in ordinary agent chat or place them in process
|
|
245
|
+
arguments. Verbose request logging redacts credential fields. Approval payloads
|
|
246
|
+
contain no OAuth credentials. The host needs a secure input path or a link to Enter's
|
|
247
|
+
form to make these cards usable from its UI.
|
|
248
|
+
|
|
249
|
+
`test/auth-provider-approve.test.mjs` exercises all four provider contracts against
|
|
250
|
+
local HTTP fixtures, including save failures, backend verification failures,
|
|
251
|
+
incorrect provider binding, malformed inputs, and verbose output redaction.
|
|
252
|
+
These tests do not validate a live OAuth login or cloud provisioning.
|
package/dist/auth.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface Credentials {
|
|
|
6
6
|
expires_at?: string;
|
|
7
7
|
}
|
|
8
8
|
export declare function saveCredentials(creds: Credentials): void;
|
|
9
|
+
export declare function verifyAccessToken(token: string): Promise<void>;
|
|
9
10
|
export declare function loadCredentials(): Credentials | null;
|
|
10
11
|
export declare function clearCredentials(): void;
|
|
11
12
|
export declare function getToken(): string;
|
package/dist/auth.js
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
1
|
+
import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import { configDir } from "./config.js";
|
|
3
|
+
import { baseURL, configDir } from "./config.js";
|
|
4
4
|
const CREDENTIALS_FILE = "credentials.json";
|
|
5
5
|
function credentialsPath() {
|
|
6
6
|
return join(configDir(), CREDENTIALS_FILE);
|
|
7
7
|
}
|
|
8
8
|
export function saveCredentials(creds) {
|
|
9
|
+
mkdirSync(configDir(), { recursive: true, mode: 0o700 });
|
|
9
10
|
writeFileSync(credentialsPath(), JSON.stringify(creds, null, 2), {
|
|
10
11
|
mode: 0o600,
|
|
11
12
|
});
|
|
12
13
|
}
|
|
14
|
+
// Validate the newly issued token, not an older saved token or ENTER_API_KEY.
|
|
15
|
+
// Do not persist it or claim login success until the configured API accepts it.
|
|
16
|
+
export async function verifyAccessToken(token) {
|
|
17
|
+
if (!token)
|
|
18
|
+
throw new Error("Login did not return an access token");
|
|
19
|
+
const response = await fetch(`${baseURL()}/v1/users/info`, {
|
|
20
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
21
|
+
signal: AbortSignal.timeout(15000),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`Login verification failed (HTTP ${response.status}); credentials were not saved`);
|
|
25
|
+
}
|
|
26
|
+
const result = await response.json();
|
|
27
|
+
if (result?.code !== 0) {
|
|
28
|
+
throw new Error("Login verification failed: API did not confirm authentication; credentials were not saved");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
13
31
|
export function loadCredentials() {
|
|
14
32
|
try {
|
|
15
33
|
const data = readFileSync(credentialsPath(), "utf-8");
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
export declare function setRequestTimeout(seconds: number): void;
|
|
1
2
|
export declare function setVerbose(v: boolean): void;
|
|
2
|
-
export declare function get(path: string, params?: Record<string, string
|
|
3
|
-
export declare function post(path: string, body?: unknown): Promise<unknown>;
|
|
3
|
+
export declare function get(path: string, params?: Record<string, string>, signal?: AbortSignal): Promise<unknown>;
|
|
4
|
+
export declare function post(path: string, body?: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
4
5
|
export declare function del(path: string): Promise<unknown>;
|
|
5
6
|
export declare function put(path: string, body?: unknown): Promise<unknown>;
|
|
6
7
|
export declare function patch(path: string, body?: unknown): Promise<unknown>;
|
package/dist/client.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { baseURL, workURL } from "./config.js";
|
|
2
2
|
import { getToken } from "./auth.js";
|
|
3
|
-
import {
|
|
3
|
+
import { safeOutput } from "./safe-output.js";
|
|
4
|
+
import { APIError, RequestError } from "./errors.js";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
6
|
const CODE_SUCCESS = 0;
|
|
5
7
|
let verbose = false;
|
|
8
|
+
let requestTimeoutMs;
|
|
9
|
+
export function setRequestTimeout(seconds) {
|
|
10
|
+
if (!Number.isFinite(seconds) || seconds <= 0 || seconds > 2147483)
|
|
11
|
+
throw new Error("--request-timeout must be a positive number of seconds");
|
|
12
|
+
requestTimeoutMs = seconds * 1000;
|
|
13
|
+
}
|
|
6
14
|
export function setVerbose(v) {
|
|
7
15
|
verbose = v;
|
|
8
16
|
}
|
|
@@ -19,51 +27,97 @@ async function request(method, path, options, base) {
|
|
|
19
27
|
const token = getToken();
|
|
20
28
|
if (token)
|
|
21
29
|
headers["Authorization"] = `Bearer ${token}`;
|
|
22
|
-
const init = { method, headers };
|
|
30
|
+
const init = { method, headers, signal: options?.signal };
|
|
23
31
|
if (options?.body !== undefined) {
|
|
24
32
|
init.body = JSON.stringify(options.body);
|
|
25
33
|
}
|
|
26
34
|
if (verbose) {
|
|
27
35
|
console.error(`> ${method} ${url}`);
|
|
28
36
|
if (options?.body)
|
|
29
|
-
console.error(`> Body: ${JSON.stringify(options.body)}`);
|
|
37
|
+
console.error(`> Body: ${JSON.stringify(safeOutput(options.body, { redactIdentifiers: true }))}`);
|
|
30
38
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
// A single deadline covers retries and reading the response body as well as
|
|
40
|
+
// connecting. Never replay writes after an ambiguous transport failure.
|
|
41
|
+
const readOnly = method === "GET" || (method === "POST" && path.endsWith("/thread/actions"));
|
|
42
|
+
const retryRead = readOnly && /\/thread\/(turns|actions|messages|tasks)$/.test(path);
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const abort = () => controller.abort(options?.signal?.reason);
|
|
45
|
+
options?.signal?.addEventListener("abort", abort, { once: true });
|
|
46
|
+
if (options?.signal?.aborted)
|
|
47
|
+
abort();
|
|
48
|
+
const timer = requestTimeoutMs === undefined ? undefined : setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
49
|
+
init.signal = controller.signal;
|
|
40
50
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
for (let attempt = 0;; attempt++) {
|
|
52
|
+
let resp;
|
|
53
|
+
let text;
|
|
54
|
+
try {
|
|
55
|
+
resp = await fetch(url, init);
|
|
56
|
+
text = await resp.text();
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (options?.signal?.aborted)
|
|
60
|
+
throw error;
|
|
61
|
+
const timedOut = controller.signal.aborted;
|
|
62
|
+
const cause = error.cause?.code;
|
|
63
|
+
if (retryRead && !timedOut && attempt < 1) {
|
|
64
|
+
await delay(200, undefined, { signal: controller.signal });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
throw new RequestError(timedOut ? "REQUEST_TIMEOUT" : "NETWORK_ERROR", timedOut ? "Enter request exceeded its deadline." : `Could not reach Enter${cause ? ` (${cause})` : ""}. Check connectivity and proxy settings.`, readOnly, !readOnly, cause);
|
|
68
|
+
}
|
|
69
|
+
if (verbose) {
|
|
70
|
+
console.error(`< ${resp.status} ${resp.statusText}`);
|
|
71
|
+
}
|
|
72
|
+
if (resp.status === 401) {
|
|
73
|
+
throw new Error("Authentication required. Run `enter login` or set ENTER_API_KEY environment variable.");
|
|
74
|
+
}
|
|
75
|
+
if (retryRead && [502, 503, 504].includes(resp.status) && attempt < 1) {
|
|
76
|
+
await delay(200, undefined, { signal: controller.signal });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
let apiResp;
|
|
80
|
+
try {
|
|
81
|
+
apiResp = JSON.parse(text);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
if (resp.status >= 400) {
|
|
85
|
+
throw new RequestError(`HTTP_${resp.status}`, `Enter returned HTTP ${resp.status}.`, readOnly && resp.status >= 500, !readOnly && resp.status >= 500);
|
|
86
|
+
}
|
|
87
|
+
throw new RequestError("INVALID_RESPONSE", "Enter returned a non-JSON response.", readOnly, !readOnly);
|
|
88
|
+
}
|
|
89
|
+
if (apiResp.code === undefined) {
|
|
90
|
+
// Endpoint returns raw JSON without the standard {code,message,data} envelope.
|
|
91
|
+
// Surface the parsed body as-is.
|
|
92
|
+
if (resp.status >= 400) {
|
|
93
|
+
throw new RequestError(`HTTP_${resp.status}`, `Enter returned HTTP ${resp.status}.`, readOnly && resp.status >= 500, !readOnly && resp.status >= 500);
|
|
94
|
+
}
|
|
95
|
+
return apiResp;
|
|
96
|
+
}
|
|
97
|
+
if (resp.status >= 500)
|
|
98
|
+
throw new RequestError(apiResp.code ? String(apiResp.code) : `HTTP_${resp.status}`, `Enter returned HTTP ${resp.status}${apiResp.message ? `: ${apiResp.message}` : ""}.`, readOnly, !readOnly);
|
|
99
|
+
if (apiResp.code !== CODE_SUCCESS) {
|
|
100
|
+
throw new APIError(apiResp.code, apiResp.message ?? "", apiResp.detail ?? "");
|
|
101
|
+
}
|
|
102
|
+
return apiResp.data;
|
|
46
103
|
}
|
|
47
|
-
return JSON.parse(text);
|
|
48
104
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (resp.status >= 400) {
|
|
53
|
-
throw new Error(`HTTP ${resp.status}: ${text}`);
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (controller.signal.aborted && !options?.signal?.aborted) {
|
|
107
|
+
throw new RequestError("REQUEST_TIMEOUT", "Enter request exceeded its deadline.", readOnly, !readOnly);
|
|
54
108
|
}
|
|
55
|
-
|
|
109
|
+
throw error;
|
|
56
110
|
}
|
|
57
|
-
|
|
58
|
-
|
|
111
|
+
finally {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
options?.signal?.removeEventListener("abort", abort);
|
|
59
114
|
}
|
|
60
|
-
return apiResp.data;
|
|
61
115
|
}
|
|
62
|
-
export async function get(path, params) {
|
|
63
|
-
return request("GET", path, { params });
|
|
116
|
+
export async function get(path, params, signal) {
|
|
117
|
+
return request("GET", path, { params, signal });
|
|
64
118
|
}
|
|
65
|
-
export async function post(path, body) {
|
|
66
|
-
return request("POST", path, { body });
|
|
119
|
+
export async function post(path, body, signal) {
|
|
120
|
+
return request("POST", path, { body, signal });
|
|
67
121
|
}
|
|
68
122
|
export async function del(path) {
|
|
69
123
|
return request("DELETE", path);
|
package/dist/commands/login.js
CHANGED
|
@@ -2,10 +2,10 @@ import { Command } from "commander";
|
|
|
2
2
|
import { createServer } from "http";
|
|
3
3
|
import { randomBytes, createHash } from "crypto";
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
|
-
import { saveCredentials } from "../auth.js";
|
|
5
|
+
import { saveCredentials, verifyAccessToken } from "../auth.js";
|
|
6
6
|
import { printMessage } from "../output.js";
|
|
7
|
-
const AUTH0_DOMAIN = "auth.
|
|
8
|
-
const CLIENT_ID = "
|
|
7
|
+
const AUTH0_DOMAIN = "auth.converge.ai";
|
|
8
|
+
const CLIENT_ID = "anCisSaaIA36fTZ2DUMiTMro3bYuptrf";
|
|
9
9
|
const AUTHORIZE_URL = `https://${AUTH0_DOMAIN}/authorize`;
|
|
10
10
|
const TOKEN_URL = `https://${AUTH0_DOMAIN}/oauth/token`;
|
|
11
11
|
const AUDIENCE = "https://api.enter.pro";
|
|
@@ -83,6 +83,7 @@ function startOAuthFlow() {
|
|
|
83
83
|
}
|
|
84
84
|
try {
|
|
85
85
|
const tokens = await exchangeCodeForTokens(code, codeVerifier);
|
|
86
|
+
await verifyAccessToken(tokens.access_token);
|
|
86
87
|
const expiresAt = new Date(Date.now() + tokens.expires_in * 1000).toISOString();
|
|
87
88
|
saveCredentials({
|
|
88
89
|
access_token: tokens.access_token,
|
|
@@ -132,6 +133,7 @@ export const loginCmd = new Command("login")
|
|
|
132
133
|
.option("--api-key <key>", "Authenticate with a workspace API key instead of OAuth")
|
|
133
134
|
.action(async (opts) => {
|
|
134
135
|
if (opts.apiKey) {
|
|
136
|
+
await verifyAccessToken(opts.apiKey);
|
|
135
137
|
saveCredentials({
|
|
136
138
|
access_token: opts.apiKey,
|
|
137
139
|
token_type: "Bearer",
|
package/dist/commands/project.js
CHANGED
|
@@ -3,6 +3,8 @@ import { writeFileSync } from "fs";
|
|
|
3
3
|
import * as client from "../client.js";
|
|
4
4
|
import { print, printMessage, printResult, printTable, pickList, getFormat } from "../output.js";
|
|
5
5
|
import { pollUntil, TimeoutError } from "../poll.js";
|
|
6
|
+
import { reportThread, threadInteraction } from "./thread.js";
|
|
7
|
+
import { safeOutput } from "../safe-output.js";
|
|
6
8
|
import { resolveLifecycleStatus } from "../lifecycle.js";
|
|
7
9
|
export const projectCmd = new Command("project")
|
|
8
10
|
.alias("proj")
|
|
@@ -89,6 +91,9 @@ projectCmd
|
|
|
89
91
|
.option("--wait", "Wait until the first build completes")
|
|
90
92
|
.option("--timeout <seconds>", "Timeout for --wait in seconds", "300")
|
|
91
93
|
.action(async (id, opts, cmd) => {
|
|
94
|
+
const seconds = Number(opts.timeout);
|
|
95
|
+
if (opts.wait && (!Number.isFinite(seconds) || seconds <= 0 || seconds * 1000 > 2147483647))
|
|
96
|
+
throw new Error("--timeout must be a positive bounded number");
|
|
92
97
|
const body = {};
|
|
93
98
|
if (opts.name)
|
|
94
99
|
body.name = opts.name;
|
|
@@ -99,38 +104,15 @@ projectCmd
|
|
|
99
104
|
if (opts.planMode)
|
|
100
105
|
body.plan_mode = true;
|
|
101
106
|
const data = await client.post(`/v1/workspaces/${id}/projects`, body);
|
|
102
|
-
|
|
103
|
-
print(getFormat(cmd), data);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const created = data;
|
|
107
|
+
const created = unwrapProject(data);
|
|
107
108
|
const projectId = String(created.project_id ?? created.id ?? "");
|
|
108
|
-
if (!projectId)
|
|
109
|
-
|
|
109
|
+
if (!projectId)
|
|
110
|
+
throw new Error("Creation response is missing project ID; creation outcome is unknown. Inspect project list before retrying.");
|
|
111
|
+
if (!opts.wait) {
|
|
112
|
+
print(getFormat(cmd), safeOutput({ ...data, submission_status: "accepted", ...threadInteraction(projectId) }));
|
|
110
113
|
return;
|
|
111
114
|
}
|
|
112
|
-
|
|
113
|
-
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
114
|
-
const buildingStatuses = new Set(["initializing", "building"]);
|
|
115
|
-
try {
|
|
116
|
-
const result = await pollUntil(() => client.get(`/v1/projects/${projectId}/detail`), (d) => !buildingStatuses.has(String(d.status ?? "")), {
|
|
117
|
-
intervalMs: 3000,
|
|
118
|
-
timeoutMs,
|
|
119
|
-
onTick: (elapsed) => {
|
|
120
|
-
process.stderr.write(`\rWaiting... ${Math.round(elapsed / 1000)}s elapsed`);
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
process.stderr.write("\n");
|
|
124
|
-
const enriched = { ...result, lifecycle_status: resolveLifecycleStatus(result) };
|
|
125
|
-
print(getFormat(cmd), enriched);
|
|
126
|
-
}
|
|
127
|
-
catch (err) {
|
|
128
|
-
if (err instanceof TimeoutError) {
|
|
129
|
-
console.error(`\nTimed out after ${opts.timeout}s. Project may still be building.`);
|
|
130
|
-
process.exit(1);
|
|
131
|
-
}
|
|
132
|
-
throw err;
|
|
133
|
-
}
|
|
115
|
+
await reportThread(projectId, { timeout: String(seconds), requireBuild: true, compact: true }, cmd, true);
|
|
134
116
|
});
|
|
135
117
|
projectCmd
|
|
136
118
|
.command("rename <project_id> <new_name>")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as client from "../client.js";
|
|
2
|
+
import { getFormat, print, printTable } from "../output.js";
|
|
3
|
+
// Queue payloads can contain prompts, attachments and credentials. Expose only
|
|
4
|
+
// the metadata needed to diagnose accepted tasks that have no turn yet.
|
|
5
|
+
export function registerThreadTasks(parent) {
|
|
6
|
+
parent.command("tasks <project_id>")
|
|
7
|
+
.description("List queued/running tasks without exposing prompts or credentials")
|
|
8
|
+
.option("--chat-id <id>", "Scope the queue lookup to a chat")
|
|
9
|
+
.action(async (projectId, opts, cmd) => {
|
|
10
|
+
const data = await client.get(`/v1/projects/${projectId}/thread/tasks`, opts.chatId ? { chat_id: opts.chatId } : undefined);
|
|
11
|
+
if (!Array.isArray(data?.tasks))
|
|
12
|
+
throw new Error("Invalid task queue response: expected tasks array");
|
|
13
|
+
const tasks = data.tasks.map(task => Object.fromEntries(["id", "task_type", "task_status", "created_at", "project_id", "chat_id"]
|
|
14
|
+
.filter(key => typeof task[key] === "string")
|
|
15
|
+
.map(key => [key, task[key]])));
|
|
16
|
+
if (getFormat(cmd) === "table") {
|
|
17
|
+
printTable(["ID", "Type", "Status", "Created", "Chat"], tasks.map(task => ["id", "task_type", "task_status", "created_at", "chat_id"].map(key => String(task[key] ?? ""))));
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
print(getFormat(cmd), { project_id: projectId, ...(opts.chatId ? { chat_id: opts.chatId } : {}), tasks, total: tasks.length });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -1,2 +1,29 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
export declare const threadCmd: Command;
|
|
3
|
+
type InteractionOptions = {
|
|
4
|
+
taskId?: string;
|
|
5
|
+
chatId?: string;
|
|
6
|
+
turn?: number;
|
|
7
|
+
requireBuild?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function threadInteraction(id: string, { taskId, chatId, turn, requireBuild }?: InteractionOptions): {
|
|
10
|
+
monitoring_required: boolean;
|
|
11
|
+
wait_command: string;
|
|
12
|
+
watch_command: string;
|
|
13
|
+
status_command: string;
|
|
14
|
+
follow_up_command: string;
|
|
15
|
+
instructions: string;
|
|
16
|
+
};
|
|
17
|
+
type MonitorOptions = {
|
|
18
|
+
turn?: string;
|
|
19
|
+
taskId?: string;
|
|
20
|
+
chatId?: string;
|
|
21
|
+
cursor?: string;
|
|
22
|
+
timeout: string;
|
|
23
|
+
transport?: string;
|
|
24
|
+
full?: boolean;
|
|
25
|
+
compact?: boolean;
|
|
26
|
+
requireBuild?: boolean;
|
|
27
|
+
};
|
|
28
|
+
export declare function reportThread(id: string, opts: MonitorOptions, cmd: Command, wait: boolean, watch?: boolean): Promise<void>;
|
|
29
|
+
export {};
|