@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 CHANGED
@@ -1,252 +1,249 @@
1
1
  # Enter CLI
2
2
 
3
- ## Production login
3
+ CLI for Enter projects, tasks and approval cards. Node 18+ is required.
4
+ The [Enter Skill](skills/enter/SKILL.md) guides agents through the workflow;
5
+ command options are available through `enter-cli <command> --help`.
4
6
 
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.
7
+ ## Authentication and configuration
9
8
 
9
+ `enter-cli login` uses `auth.converge.ai` and the API at
10
+ `https://api.enter.pro/code/api` (not the web domain `enter.converge.ai`).
11
+ Log in again if credentials came from the former `auth.enter.pro` tenant.
10
12
  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
+ saving it; verification failure preserves existing credentials. Expired local
14
+ OAuth credentials renew automatically. An already-sent renewal saves rotated
15
+ credentials before honoring cancellation, so the next invocation can still log in.
13
16
 
14
- ## Local verification
17
+ `ENTER_API_KEY` takes precedence over local credentials. `login` saves local
18
+ credentials but cannot switch an injected identity. `logout` clears local
19
+ credentials only and reports `logged_out: false, auth_source: environment` when
20
+ that variable remains set. Switch host-managed authentication in the host, or
21
+ unset the variable in your shell.
22
+
23
+ `config get/set/list` supports `api_url`, `base_path`, and `output` (`json`, `yaml`,
24
+ `table`). Environment overrides are `ENTER_API_URL`, `ENTER_BASE_PATH`, and
25
+ `ENTER_OUTPUT`. Workspace IDs are explicit command arguments; the unused
26
+ `default_workspace` setting is rejected and legacy entries are ignored.
27
+
28
+ ## Submit, observe and deliver
15
29
 
16
30
  ```sh
17
- npm ci
18
- npm test
31
+ enter-cli --output json thread chat PROJECT_ID --file requirement.txt
32
+ enter-cli --output json thread wait PROJECT_ID --task-id TASK_ID --timeout 10
33
+ enter-cli --output json thread status PROJECT_ID --task-id TASK_ID
34
+ enter-cli thread wait PROJECT_ID --task-id TASK_ID --stream --timeout 0
19
35
  ```
20
36
 
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.
37
+ `chat --stdin` also accepts multiline input. Submission returns immediately with
38
+ `task_id`, `submission_status: accepted`, and continuation commands. Follow-ups
39
+ can be submitted while Enter works; acceptance is not completion.
24
40
 
25
- ### Interactive local simulator
41
+ | Command | Output and default lifetime |
42
+ | --- | --- |
43
+ | `status` | One JSON/YAML snapshot, 30-second deadline |
44
+ | `wait --stream` | One continuous observer with batched NDJSON; unlimited by default, explicit --timeout bounds lifetime |
45
+ | `wait` | One result on new narrative progress, a card, terminal state or timeout; 10 seconds |
46
+ | `watch` | Changed snapshots as NDJSON, then `type: result`; 60 seconds; unlimited only with explicit `--timeout 0` |
47
+
48
+ Use one observer per task. Hosts that buffer shell output use bounded `wait`;
49
+ Helix uses `--timeout 10` to return control regularly. On timeout, first handle
50
+ incoming user messages, then report the actual state and continue the returned command for the same task,
51
+ without sleep, resubmission or repeated permission questions. Hosts that consume
52
+ stdout incrementally should use `wait --stream`; unchanged successful observations emit a
53
+ heartbeat every max(30 seconds, progress interval). Hosts own scheduling and interruption.
54
+
55
+ `--task-id` follows exactly the submitted task, including queue time. `--turn N`
56
+ selects a fixed turn instead; without either, observation pins the first turn.
57
+ `--chat-id` scopes lookup and continuation. An interjection may return an external
58
+ message ID that the backend cannot correlate to a turn: report `unknown`, inspect
59
+ messages and the resulting change, and do not substitute the newest turn.
26
60
 
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:
61
+ | Status | Meaning |
62
+ | --- | --- |
63
+ | `idle` | No turn yet |
64
+ | `queued` | Selected task is queued |
65
+ | `unknown` | Missing turn, unavailable status or uncorrelated submission |
66
+ | `running` | Nonterminal turn with no pending action |
67
+ | `blocked` | Inspect `actions` for input or approval |
68
+ | `completed` | Turn completed; build and deployment are separate |
69
+ | `failed` | Turn failed, errored or was cancelled |
70
+
71
+ `project create --wait` uses the same observer and requires a matching successful
72
+ build. For existing tasks use `--require-build`; stale builds cannot satisfy it.
73
+ `build_matches_turn` checks commit identity. `status/wait/watch` return monitoring fields by default, excluding usage and unrelated metadata.
74
+ Use `--full` for detailed sanitized metadata; `--compact` remains an explicit alias for the default.
75
+ The additive `workflow` projection supplies `state`, `task`, `next_action`,
76
+ `observation`, build matching and action descriptors; full card details remain in
77
+ `actions`. `use_existing_authorization` does not grant new permission.
78
+
79
+ Exit 0 means observation succeeded (including a blocked card), not build success.
80
+ Exit 2 means a wait timed out with work pending; exit 1 means failure, unknown
81
+ state or query failure. `query_timed_out` marks a query deadline; retained state
82
+ is the last complete snapshot. SIGINT/SIGTERM returns `interrupted: true` and
83
+ 130/143 without cancelling remote work. A cancelled turn can still have a queued
84
+ backend operation; do not confuse it with completed cancellation or restoration.
85
+
86
+ Completed snapshots include `messages_command`. Use
87
+ `thread messages PROJECT_ID --turn N --text` to read Enter's delivery and validation
88
+ summary. Report build success, saved configuration and real-service validation
89
+ separately; do not replace Enter's validation with source keyword scans.
90
+
91
+ ## Transport and errors
92
+
93
+ Observation defaults to HTTP polling every 2 seconds after each read. Each
94
+ snapshot query is bounded to 30 seconds. No WebSocket is opened by default.
95
+ `--transport auto` opts in to WebSocket events with HTTP fallback; 401/403 on the
96
+ stream stops reconnection but leaves state observation on HTTP. Raw message
97
+ follow still fails with STREAM_AUTH_ERROR. HTTP authentication failures stop
98
+ observation. WebSockets respect proxy variables and NO_PROXY.
99
+
100
+ `thread messages PROJECT_ID --follow --timeout 60 --cursor EVENT_ID` emits raw
101
+ NDJSON events with diagnostics on stderr. It supports `--max-events`, crosses turn
102
+ boundaries, and does not synthesize HTTP state when the socket is unavailable.
103
+
104
+ `--request-timeout SECONDS` covers API requests and response bodies, including
105
+ downloads. Monitoring always has its own deadline. `project publish --timeout`
106
+ validates before requests and bounds lookup, submission, observation and URL
107
+ verification. After uncertain publication, query `project publish-status` first.
108
+ Only transient observation reads retry automatically; mutations never do.
109
+ `error.outcome_unknown: true` means inspect actual state before retrying a write.
110
+
111
+ JSON execution errors go to stderr; observation errors accompany the last
112
+ snapshot on stdout. Error fields are `code/message/retryable/outcome_unknown`.
113
+ Keep stdout and stderr separate. JSON/YAML confirmations are structured;
114
+ interactive login instructions use stderr. Known credential fields and encoded
115
+ tool arguments are redacted in monitoring, approval, events and verbose bodies;
116
+ ordinary content is not arbitrarily rewritten.
117
+
118
+ ## Approval cards
119
+
120
+ Feature cards call the actual enable endpoint and verify action state; subscription
121
+ refusal or enable failure remains an error. Plans require user approval. Questions
122
+ carry their original text, options and selection mode: use that text as the answer
123
+ key, not a host question ID. `thread approve --help` describes `selected_options`
124
+ and `other_text`; skip only when the user requests it.
125
+
126
+ Configuration cards expose required fields. Reuse authorized input, ask only for
127
+ missing values, and use stdin from a secure input or user-provided file, or Enter's
128
+ form. OAuth configuration is saved before credential-free approval; after form
129
+ submission, check status because the form may already have approved the action.
130
+ Provider fields, Secret/Stripe inputs and examples are in
131
+ [configuration guidance](skills/enter/references/configuration.md).
132
+ Do not echo secrets or embed them in shell text/build prompts: stdin does not
133
+ protect earlier chat or tool logs. User-approved placeholders remain pending
134
+ integrations; mock configuration does not prove real OAuth login or payment.
135
+
136
+ ## Development and host installation
29
137
 
30
138
  ```sh
139
+ npm ci
140
+ npm test
31
141
  npm run build
32
- npm run mock:serve
142
+ npm pack
143
+ npm run install:hosts -- --package /absolute/path/enter-pro-enter-cli-0.4.4.tgz
33
144
  ```
34
145
 
35
- In another terminal in this repository:
146
+ Tests use loopback HTTP/WebSocket fixtures and dummy credentials, not real cloud
147
+ provisioning or host UI acceptance. The installer puts the same package and Skill
148
+ into Codex/DSH, renders the wrapper, and records its SHA-256. `--host codex|dsh`
149
+ selects one host; `--home` supports isolated installs. No checkout is needed at runtime.
150
+
151
+ For manual simulation, run `npm run mock:serve`, then in another terminal:
36
152
 
37
153
  ```sh
38
- npm run local:cli -- whoami
39
- npm run local:cli -- workspace list
40
154
  npm run local:cli -- thread status cloud
41
155
  npm run local:cli -- thread approve cloud cloud-action
42
156
  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
157
  ```
74
158
 
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
159
+ The server binds loopback port 43180 (`MOCK_ENTER_PORT` overrides it). The wrapper
160
+ forces loopback and a dummy key even if production environment variables exist.
161
+ Scenarios: `plan`, `cloud`, `ai`, `secret`, `questions`, `subscription`, `running`,
162
+ `failed`, `http-error`, `stalled`. Card IDs are `<scenario>-action`. Restart to reset;
163
+ unsupported routes fail, and request bodies/secrets are not retained. Only use
164
+ fake values. `.env.integration.example` lists real-test variables; its ignored
165
+ `.env.integration.local` counterpart is never loaded automatically.
166
+
167
+ ### Agent host integration
168
+
169
+ All observers return `progress` from the selected task's HTTP message history:
170
+ `source: assistant_messages`, `available`, a scoped `revision`, and the latest
171
+ assistant text in `messages` (up to 1600 characters, marked `truncated` if clipped).
172
+ Reasoning and raw tool arguments are excluded. `observed_at` timestamps the last
173
+ successful state read; `emitted_at` timestamps watch output, including stale results.
174
+ A narrative read failure is separate from authoritative task/action status.
175
+
176
+ `wait` batches ordinary narrative for 10 seconds by default, keeping the latest text,
177
+ and returns early when that interval has elapsed and progress changed.
178
+ `--progress-interval <seconds>` controls batching; `0` opts into every change.
179
+ Actions, terminal states and errors bypass batching. The overall `--timeout` still
180
+ bounds the call and returns the latest observation even before the interval elapses.
181
+ `status` remains an immediate read; no phase is guessed from narrative keywords. `workflow.next_action=report_progress` means relay the text first, then `after_reporting=observe`. Exit 0 means an observation result, not build completion. Execute the returned `wait_command`, including
182
+ `--after-progress`, to wait for the next revision without replaying the same update.
183
+ `watch` uses the same interval for ordinary narrative/metadata updates; state changes
184
+ and final results are emitted immediately.
185
+ These are latest-progress snapshots, not a lossless replay of every message.
186
+
187
+ The host agent relays the supplied text, forwards questions, and executes the next
188
+ command; it need not reason over logs or infer development phases. Relay new progress
189
+ proactively, suppress repeated revisions/heartbeats, and answer user questions from
190
+ the last observation before refreshing. Background hosts with only completion notifications fall back to bounded `wait`: batched progress ends the job and triggers that notification. Merely being able to read running stdout is insufficient for automatic `watch` delivery. For background jobs, read output nonblockingly
191
+ or with a short bounded wait; a five-minute blocking job-output call defeats background
192
+ execution. The CLI cannot schedule the host agent or interrupt its tool calls.
193
+
194
+ Both `wait` and `watch` use HTTP by default. The CLI performs the polling;
195
+ do not generate `sleep && enter-cli` or a loop of `thread status` commands.
196
+
197
+ For hosts that return shell output only after the command exits, use `wait`.
198
+ For the current Helix integration, use a short 10-second observation window
199
+ to return control for incoming messages:
200
+
201
+ ```bash
103
202
  enter-cli --output json thread wait PROJECT_ID --task-id TASK_ID --timeout 10
104
203
  ```
105
204
 
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.
205
+ The CLI and Helix guidance both default to 10 seconds. A shell deadline alone
206
+ does not guarantee responsiveness. On exit 2, handle incoming messages, report
207
+ the returned Enter stage/status and execute the returned `wait_command` to keep observing.
208
+ It preserves the task or pinned turn, chat, build requirement and observation
209
+ timeout. Do not ask the user whether to continue, recreate the project or
210
+ resubmit the build. A timeout ends observation, not the remote task. On
211
+ `blocked`, forward the action and follow the continuation after answering;
212
+ on `completed` or `failed`, consume the result.
213
+
214
+ For hosts that notify the agent when incremental stdout arrives without blocking user interaction,
215
+ use `watch_command` (60 seconds by default). Only explicitly select `--timeout 0`
216
+ when the host can manage an unlimited background observer.
217
+ `snapshot` contains actual Enter task/turn state, `heartbeat` confirms a
218
+ successful observation without a state change, and `result` ends observation.
219
+ This is stage/status progress, not generated percentages or a raw model-token
220
+ log. Each snapshot query has a 30-second deadline. SIGINT/SIGTERM stop only
221
+ the observer (130/143); the host owns background execution and reconnection.
222
+
223
+ The current Helix sandbox adapter buffers stdout until command completion;
224
+ therefore an indefinitely running `watch` is not its default integration path.
225
+ Helix colleagues only need to update the actual sandbox CLI and the agent's
226
+ usage instructions for the bounded `wait` path, then verify it in a fresh
227
+ sandbox. Continuous in-flight display requires host-side incremental output
228
+ support; local CLI tests do not prove that integration.
229
+
230
+ State and pending-action reads run concurrently to avoid spending the observation
231
+ budget on sequential independent requests. Failed turns do not wait for pending
232
+ action reads. `QUERY_TIMEOUT` before the first snapshot means the observation
233
+ deadline expired without a known task state; it does not locate the cause in the
234
+ network or server. The overall deadline remains bounded. Usage changes are not
235
+ progress; continue quietly when there is no new narrative or actionable state.
236
+
237
+ ### One background observer
238
+
239
+ Use `enter-cli thread wait PROJECT_ID --task-id TASK_ID --stream --timeout 0`
240
+ with a host that supports managed background execution and incremental output delivery.
241
+ The CLI polls HTTP internally and emits batched progress until an action, terminal state,
242
+ interruption or explicit deadline. Ordinary progress does not end this process.
243
+ Consume events without blocking user interaction; do not restart it on report_progress.
244
+ The returned `stream_argv` / `stream_command` starts this observer.
245
+ `watch` remains a compatible streaming entry point.
246
+
247
+ Without `--stream`, wait retains its bounded single-JSON contract for buffered hosts.
248
+ This fallback can require repeated calls; CLI cannot enable host background scheduling.
249
+ Do not detach with shell `&` or assume buffered Bash output is delivered incrementally.
package/dist/auth.d.ts CHANGED
@@ -11,3 +11,6 @@ export declare function loadCredentials(): Credentials | null;
11
11
  export declare function clearCredentials(): void;
12
12
  export declare function getToken(): string;
13
13
  export declare function isAuthenticated(): boolean;
14
+ export declare const OAUTH_TOKEN_URL = "https://auth.converge.ai/oauth/token";
15
+ export declare const OAUTH_CLIENT_ID = "anCisSaaIA36fTZ2DUMiTMro3bYuptrf";
16
+ export declare function getValidToken(signal?: AbortSignal): Promise<string>;
package/dist/auth.js CHANGED
@@ -1,4 +1,6 @@
1
- import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "fs";
1
+ import { readFileSync, writeFileSync, unlinkSync, mkdirSync, renameSync, rmdirSync } from "fs";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import { randomUUID } from "node:crypto";
2
4
  import { join } from "path";
3
5
  import { baseURL, configDir } from "./config.js";
4
6
  const CREDENTIALS_FILE = "credentials.json";
@@ -7,9 +9,17 @@ function credentialsPath() {
7
9
  }
8
10
  export function saveCredentials(creds) {
9
11
  mkdirSync(configDir(), { recursive: true, mode: 0o700 });
10
- writeFileSync(credentialsPath(), JSON.stringify(creds, null, 2), {
11
- mode: 0o600,
12
- });
12
+ const temporary = `${credentialsPath()}.${randomUUID()}.tmp`;
13
+ try {
14
+ writeFileSync(temporary, JSON.stringify(creds, null, 2), { mode: 0o600 });
15
+ renameSync(temporary, credentialsPath());
16
+ }
17
+ finally {
18
+ try {
19
+ unlinkSync(temporary);
20
+ }
21
+ catch { /* already renamed */ }
22
+ }
13
23
  }
14
24
  // Validate the newly issued token, not an older saved token or ENTER_API_KEY.
15
25
  // Do not persist it or claim login success until the configured API accepts it.
@@ -41,8 +51,9 @@ export function clearCredentials() {
41
51
  try {
42
52
  unlinkSync(credentialsPath());
43
53
  }
44
- catch {
45
- // ignore if file doesn't exist
54
+ catch (error) {
55
+ if (error.code !== "ENOENT")
56
+ throw error;
46
57
  }
47
58
  }
48
59
  export function getToken() {
@@ -55,3 +66,100 @@ export function getToken() {
55
66
  export function isAuthenticated() {
56
67
  return getToken() !== "";
57
68
  }
69
+ // Same OAuth client as interactive login. Environment keys (including Work's
70
+ // transparent placeholder) must never fall back to a personal OAuth identity.
71
+ export const OAUTH_TOKEN_URL = "https://auth.converge.ai/oauth/token";
72
+ export const OAUTH_CLIENT_ID = "anCisSaaIA36fTZ2DUMiTMro3bYuptrf";
73
+ let refreshing;
74
+ function needsRefresh(creds) {
75
+ return !!creds.expires_at && Date.parse(creds.expires_at) <= Date.now() + 60000;
76
+ }
77
+ async function refreshCredentials(signal) {
78
+ const lock = join(configDir(), "credentials-refresh.lock");
79
+ const deadline = Date.now() + 20000;
80
+ // Serialize separate CLI processes too: rotating refresh tokens are single use.
81
+ while (true) {
82
+ signal?.throwIfAborted();
83
+ try {
84
+ mkdirSync(lock, { mode: 0o700 });
85
+ break;
86
+ }
87
+ catch (error) {
88
+ if (error.code !== "EEXIST")
89
+ throw error;
90
+ if (Date.now() >= deadline)
91
+ throw new Error("Another CLI is refreshing login. Retry after it finishes; if it crashed, remove ~/.enter/credentials-refresh.lock.");
92
+ await delay(100, undefined, { signal });
93
+ }
94
+ }
95
+ try {
96
+ const creds = loadCredentials();
97
+ if (!creds?.access_token)
98
+ return "";
99
+ if (!needsRefresh(creds))
100
+ return creds.access_token;
101
+ if (!creds.refresh_token)
102
+ throw new Error("Login expired. Run `enter-cli login` to sign in again.");
103
+ let response;
104
+ try {
105
+ response = await fetch(OAUTH_TOKEN_URL, {
106
+ method: "POST",
107
+ headers: { "Content-Type": "application/json" },
108
+ body: JSON.stringify({ grant_type: "refresh_token", client_id: OAUTH_CLIENT_ID, refresh_token: creds.refresh_token }),
109
+ signal: AbortSignal.timeout(15000),
110
+ });
111
+ }
112
+ catch {
113
+ throw new Error("Could not refresh login. Check connectivity and retry; saved credentials were preserved.");
114
+ }
115
+ if (!response.ok) {
116
+ if ([400, 401, 403].includes(response.status))
117
+ throw new Error("Login renewal was rejected. Run `enter-cli login` to sign in again.");
118
+ throw new Error(`Login renewal failed (HTTP ${response.status}). Retry later.`);
119
+ }
120
+ let tokens;
121
+ try {
122
+ tokens = await response.json();
123
+ }
124
+ catch {
125
+ throw new Error("Login renewal returned an invalid response.");
126
+ }
127
+ if (typeof tokens?.access_token !== "string" || !tokens.access_token ||
128
+ typeof tokens.expires_in !== "number" || !Number.isFinite(tokens.expires_in) || tokens.expires_in <= 0 ||
129
+ (tokens.refresh_token !== undefined && (typeof tokens.refresh_token !== "string" || !tokens.refresh_token))) {
130
+ throw new Error("Login renewal returned an invalid response.");
131
+ }
132
+ // Persist rotation immediately; an API outage after renewal must not discard
133
+ // the new refresh token and strand the next invocation with the consumed one.
134
+ saveCredentials({
135
+ ...creds, access_token: tokens.access_token,
136
+ refresh_token: tokens.refresh_token ?? creds.refresh_token,
137
+ token_type: typeof tokens.token_type === "string" ? tokens.token_type : creds.token_type,
138
+ expires_at: new Date(Date.now() + tokens.expires_in * 1000).toISOString(),
139
+ });
140
+ return tokens.access_token;
141
+ }
142
+ finally {
143
+ rmdirSync(lock);
144
+ }
145
+ }
146
+ export async function getValidToken(signal) {
147
+ signal?.throwIfAborted();
148
+ if (process.env.ENTER_API_KEY)
149
+ return process.env.ENTER_API_KEY;
150
+ const creds = loadCredentials();
151
+ if (!creds?.access_token)
152
+ return "";
153
+ if (!needsRefresh(creds))
154
+ return creds.access_token;
155
+ // Cancelable callers use the same filesystem lock without canceling another
156
+ // caller's renewal. Once sent, renewal must finish and persist token rotation.
157
+ if (signal) {
158
+ const token = await refreshCredentials(signal);
159
+ signal.throwIfAborted();
160
+ return token;
161
+ }
162
+ if (!refreshing)
163
+ refreshing = refreshCredentials().finally(() => { refreshing = undefined; });
164
+ return refreshing;
165
+ }