@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/dist/errors.d.ts CHANGED
@@ -1,3 +1,11 @@
1
+ export declare class RequestError extends Error {
2
+ code: string;
3
+ retryable: boolean;
4
+ outcomeUnknown: boolean;
5
+ causeCode?: string | undefined;
6
+ constructor(code: string, message: string, retryable?: boolean, outcomeUnknown?: boolean, causeCode?: string | undefined);
7
+ }
8
+ export declare function errorEnvelope(error: unknown): Record<string, unknown>;
1
9
  export declare class APIError extends Error {
2
10
  code: number | string;
3
11
  detail: string;
package/dist/errors.js CHANGED
@@ -1,3 +1,32 @@
1
+ import { redactText } from "./safe-output.js";
2
+ export class RequestError extends Error {
3
+ code;
4
+ retryable;
5
+ outcomeUnknown;
6
+ causeCode;
7
+ constructor(code, message, retryable = false, outcomeUnknown = false, causeCode) {
8
+ super(message);
9
+ this.code = code;
10
+ this.retryable = retryable;
11
+ this.outcomeUnknown = outcomeUnknown;
12
+ this.causeCode = causeCode;
13
+ this.name = "RequestError";
14
+ }
15
+ }
16
+ export function errorEnvelope(error) {
17
+ const e = error instanceof Error ? error : new Error(String(error));
18
+ const request = e instanceof RequestError ? e : undefined;
19
+ const api = e instanceof APIError ? e : undefined;
20
+ return { error: {
21
+ code: request?.code ?? api?.code ?? "CLI_ERROR",
22
+ message: redactText(e.message),
23
+ retryable: request?.retryable ?? api?.retryable ?? false,
24
+ outcome_unknown: request?.outcomeUnknown ?? false,
25
+ ...(request?.causeCode ? { cause_code: request.causeCode } : {}),
26
+ ...(request?.outcomeUnknown ? { hint: "The write may have reached Enter. Inspect task/action state before retrying; do not resubmit blindly." }
27
+ : api?.hint ? { hint: api.hint } : {}),
28
+ } };
29
+ }
1
30
  export class APIError extends Error {
2
31
  code;
3
32
  detail;
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import { createRequire } from "module";
4
- import { setVerbose } from "./client.js";
4
+ import { setVerbose, setRequestTimeout } from "./client.js";
5
5
  import { loadConfig } from "./config.js";
6
+ import { errorEnvelope } from "./errors.js";
6
7
  const require = createRequire(import.meta.url);
7
8
  const { version } = require("../package.json");
8
9
  import { loginCmd } from "./commands/login.js";
@@ -21,10 +22,13 @@ program
21
22
  .version(version)
22
23
  .option("-o, --output <format>", "Output format: table, json, yaml")
23
24
  .option("-v, --verbose", "Verbose output (show HTTP requests)")
25
+ .option("--request-timeout <seconds>", "Optional HTTP deadline including observation read retries")
24
26
  .hook("preAction", () => {
25
27
  const opts = program.opts();
26
28
  if (opts.verbose)
27
29
  setVerbose(true);
30
+ if (opts.requestTimeout !== undefined)
31
+ setRequestTimeout(Number(opts.requestTimeout));
28
32
  if (!opts.output) {
29
33
  const cfg = loadConfig();
30
34
  program.setOptionValue("output", cfg.output);
@@ -40,6 +44,9 @@ program.addCommand(threadCmd);
40
44
  program.addCommand(domainCmd);
41
45
  program.addCommand(modelsCmd);
42
46
  program.parseAsync().catch((err) => {
43
- console.error(`Error: ${err.message}`);
47
+ if ((program.opts().output ?? loadConfig().output) === "json")
48
+ console.error(JSON.stringify(errorEnvelope(err)));
49
+ else
50
+ console.error(`Error: ${err.message}`);
44
51
  process.exit(1);
45
52
  });
@@ -0,0 +1,6 @@
1
+ export declare function redactText(value: string): string;
2
+ type OutputOptions = {
3
+ redactIdentifiers?: boolean;
4
+ };
5
+ export declare function safeOutput(value: unknown, options?: OutputOptions): unknown;
6
+ export {};
@@ -0,0 +1,29 @@
1
+ // Applied at output boundaries, never to requests or data used for execution.
2
+ const credentialField = /^(?:.*[_-])?(?:access_token|refresh_token|id_token|token|password|secret|secret_value|secret_key|api_key|anon_key|service_role_key|private_key|client_secret|app_secret|authorization|credentials?)$/i;
3
+ export function redactText(value) {
4
+ return value
5
+ .replace(/\bBearer\s+[^\s"'<>]+/gi, "Bearer [REDACTED]")
6
+ .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[REDACTED]")
7
+ .replace(/\b(?:epk_|sk-)[A-Za-z0-9_-]+/g, "[REDACTED]")
8
+ .replace(/([?&](?:token|api_key|access_token)=)[^&\s"']+/gi, "$1[REDACTED]");
9
+ }
10
+ export function safeOutput(value, options = {}) {
11
+ if (Array.isArray(value))
12
+ return value.map(item => safeOutput(item, options));
13
+ if (value && typeof value === "object") {
14
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
15
+ key, credentialField.test(key.replace(/([a-z])([A-Z])/g, "$1_$2")) || (options.redactIdentifiers && /^(client_ids?|app_id)$/i.test(key)) ? "[REDACTED]" : safeField(key, item, options),
16
+ ]));
17
+ }
18
+ return typeof value === "string" ? redactText(value) : value;
19
+ }
20
+ function safeField(key, value, options) {
21
+ // Only decode known JSON-encoded tool fields, never arbitrary user content.
22
+ if (["full_arguments", "tool_call_args", "tool_result", "accumulated_arguments", "arguments"].includes(key) && typeof value === "string") {
23
+ try {
24
+ return JSON.stringify(safeOutput(JSON.parse(value), options));
25
+ }
26
+ catch { /* partial JSON */ }
27
+ }
28
+ return safeOutput(value, options);
29
+ }
@@ -0,0 +1,36 @@
1
+ import { RequestError } from "./errors.js";
2
+ export type ThreadEvent = Record<string, unknown>;
3
+ export type StreamMode = "connecting" | "websocket" | "reconnecting" | "polling";
4
+ export interface StreamOptions {
5
+ turn?: number;
6
+ chatId?: string;
7
+ cursor?: string;
8
+ stateOnly?: boolean;
9
+ signal: AbortSignal;
10
+ onEvent?: (event: ThreadEvent) => void;
11
+ onMode?: (mode: StreamMode) => void;
12
+ }
13
+ export declare function streamURL(base: string, projectId: string, options: Omit<StreamOptions, "signal">): URL;
14
+ export declare class ThreadEvents {
15
+ private projectId;
16
+ private options;
17
+ mode: StreamMode;
18
+ cursor?: string;
19
+ reason?: string;
20
+ failure?: RequestError;
21
+ revision: number;
22
+ private socket?;
23
+ private retry?;
24
+ private stopped;
25
+ private failures;
26
+ private listeners;
27
+ private seen;
28
+ private abort;
29
+ constructor(projectId: string, options: StreamOptions);
30
+ private wake;
31
+ private setMode;
32
+ private connect;
33
+ wait(afterRevision: number, timeoutMs: number): Promise<void>;
34
+ close(): void;
35
+ }
36
+ export declare function isStateEvent(event: ThreadEvent): boolean;
@@ -0,0 +1,186 @@
1
+ import WebSocket from "ws";
2
+ import { HttpsProxyAgent } from "https-proxy-agent";
3
+ import { getProxyForUrl } from "proxy-from-env";
4
+ import { baseURL } from "./config.js";
5
+ import { getToken } from "./auth.js";
6
+ import { RequestError } from "./errors.js";
7
+ export function streamURL(base, projectId, options) {
8
+ const url = new URL(`${base.replace(/\/$/, "")}/v1/projects/${encodeURIComponent(projectId)}/thread/stream`);
9
+ if (url.protocol === "https:")
10
+ url.protocol = "wss:";
11
+ else if (url.protocol === "http:")
12
+ url.protocol = "ws:";
13
+ else
14
+ throw new Error("Enter API URL must use HTTP or HTTPS");
15
+ if (options.turn)
16
+ url.searchParams.set("turn", String(options.turn));
17
+ if (options.chatId)
18
+ url.searchParams.set("chat_id", options.chatId);
19
+ if (options.cursor)
20
+ url.searchParams.set("last_event_id", options.cursor);
21
+ return url;
22
+ }
23
+ function laterCursor(current, next) {
24
+ if (typeof next !== "string" || !/^\d+-\d+$/.test(next))
25
+ return current;
26
+ if (!current || !/^\d+-\d+$/.test(current))
27
+ return next;
28
+ const a = current.split("-").map(BigInt), b = next.split("-").map(BigInt);
29
+ return b[0] > a[0] || (b[0] === a[0] && b[1] > a[1]) ? next : current;
30
+ }
31
+ // A socket is only a wake-up source. Callers still read authoritative action
32
+ // state: a tool-start event can precede persistence of its approval card.
33
+ export class ThreadEvents {
34
+ projectId;
35
+ options;
36
+ mode = "connecting";
37
+ cursor;
38
+ reason;
39
+ failure;
40
+ revision = 0;
41
+ socket;
42
+ retry;
43
+ stopped = false;
44
+ failures = 0;
45
+ listeners = new Set();
46
+ seen = new Set();
47
+ abort = () => this.close();
48
+ constructor(projectId, options) {
49
+ this.projectId = projectId;
50
+ this.options = options;
51
+ this.cursor = options.cursor;
52
+ options.signal.addEventListener("abort", this.abort, { once: true });
53
+ if (options.signal.aborted)
54
+ this.close();
55
+ else {
56
+ try {
57
+ this.connect();
58
+ }
59
+ catch (error) {
60
+ this.close();
61
+ throw error;
62
+ }
63
+ }
64
+ }
65
+ wake() {
66
+ this.revision++;
67
+ for (const listener of this.listeners)
68
+ listener();
69
+ this.listeners.clear();
70
+ }
71
+ setMode(mode, reason) {
72
+ if (this.mode === mode && this.reason === reason)
73
+ return;
74
+ this.mode = mode;
75
+ this.reason = reason;
76
+ this.options.onMode?.(mode);
77
+ this.wake();
78
+ }
79
+ connect() {
80
+ if (this.stopped)
81
+ return;
82
+ const url = streamURL(baseURL(), this.projectId, { ...this.options, cursor: this.cursor });
83
+ const token = getToken();
84
+ // proxy-from-env honors NO_PROXY, including loopback fixture servers.
85
+ const proxy = process.env.NODE_USE_ENV_PROXY === "0" ? "" : getProxyForUrl(url.toString().replace(/^ws/, "http"));
86
+ const socket = new WebSocket(url, {
87
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
88
+ ...(proxy ? { agent: new HttpsProxyAgent(proxy) } : {}),
89
+ handshakeTimeout: 1500,
90
+ maxPayload: 8 * 1024 * 1024,
91
+ followRedirects: false,
92
+ });
93
+ this.socket = socket;
94
+ let opened = false;
95
+ let retryable = true;
96
+ let reason = "connection_lost";
97
+ socket.on("unexpected-response", (_request, response) => {
98
+ const status = response.statusCode ?? 0;
99
+ response.resume();
100
+ if ([401, 403].includes(status)) {
101
+ retryable = false;
102
+ reason = status === 401 ? "authentication_required" : "permission_denied";
103
+ this.failure = new RequestError("STREAM_AUTH_ERROR", "Enter event stream rejected access. Check authentication and project permissions.");
104
+ }
105
+ else if ([404, 405, 426, 501].includes(status)) {
106
+ retryable = false;
107
+ reason = "stream_unavailable";
108
+ }
109
+ else
110
+ reason = `stream_http_${status}`;
111
+ socket.terminate();
112
+ });
113
+ socket.on("open", () => { opened = true; this.setMode("websocket"); });
114
+ socket.on("message", raw => {
115
+ let event;
116
+ try {
117
+ const parsed = JSON.parse(raw.toString());
118
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
119
+ return;
120
+ event = parsed;
121
+ }
122
+ catch {
123
+ retryable = false;
124
+ reason = "invalid_event";
125
+ socket.close();
126
+ return;
127
+ }
128
+ if (this.options.chatId && event.chat_id && event.chat_id !== this.options.chatId)
129
+ return;
130
+ const key = typeof event.event_id === "string" ? event.event_id : JSON.stringify(event);
131
+ if (this.seen.has(key))
132
+ return;
133
+ this.seen.add(key);
134
+ if (this.seen.size > 1024)
135
+ this.seen.delete(this.seen.values().next().value);
136
+ this.cursor = laterCursor(this.cursor, event.event_id);
137
+ this.failures = 0;
138
+ this.options.onEvent?.(event);
139
+ if (!this.options.stateOnly || isStateEvent(event))
140
+ this.wake();
141
+ });
142
+ // Do not expose raw URLs/errors (proxies and credentials can be embedded).
143
+ socket.on("error", () => { if (reason === "connection_lost" && !opened)
144
+ reason = "connection_failed"; });
145
+ socket.on("close", () => {
146
+ if (this.stopped)
147
+ return;
148
+ this.failures++;
149
+ this.setMode(opened && retryable ? "reconnecting" : "polling", reason);
150
+ if (!retryable)
151
+ return;
152
+ this.retry = setTimeout(() => this.connect(), Math.min(1000 * 2 ** Math.min(this.failures - 1, 4), 15000));
153
+ });
154
+ }
155
+ wait(afterRevision, timeoutMs) {
156
+ if (this.stopped || this.revision !== afterRevision)
157
+ return Promise.resolve();
158
+ return new Promise(resolve => {
159
+ const done = () => { clearTimeout(timer); this.listeners.delete(done); resolve(); };
160
+ const timer = setTimeout(done, timeoutMs);
161
+ this.listeners.add(done);
162
+ });
163
+ }
164
+ close() {
165
+ if (this.stopped)
166
+ return;
167
+ this.stopped = true;
168
+ clearTimeout(this.retry);
169
+ this.options.signal.removeEventListener("abort", this.abort);
170
+ this.socket?.terminate();
171
+ this.wake();
172
+ }
173
+ }
174
+ export function isStateEvent(event) {
175
+ const type = String(event.message_type ?? "");
176
+ if (/^(turn_|build_|sync_|agent_stopped|insufficient_credits)/.test(type))
177
+ return true;
178
+ const detail = event.detail;
179
+ const tool = detail?.[type];
180
+ // Legacy argument chunks carry tool_name too; they are not new card state.
181
+ if (["tool_call_arguments", "tool_call_arguments_delta", "tool_call_arguments_start"].includes(type))
182
+ return false;
183
+ if (tool?.tool_action_id)
184
+ return true;
185
+ return (type.startsWith("tool_call_") && /^(ask_user_question|confirm_plan_mode|supabase_|stripe_|enable_|i18n_enable)/.test(String(tool?.tool_name ?? "")));
186
+ }
package/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "@enter-pro/enter-cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Enter CLI - manage Enter platform resources from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "enter-cli": "dist/index.js"
9
9
  },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/index.js",
14
+ "test": "npm run build && node --test",
15
+ "mock:serve": "node scripts/mock-enter.mjs",
16
+ "local:cli": "node scripts/local-cli.mjs",
17
+ "prepublishOnly": "npm run build"
18
+ },
10
19
  "files": [
11
20
  "dist"
12
21
  ],
@@ -25,17 +34,16 @@
25
34
  },
26
35
  "dependencies": {
27
36
  "commander": "^13.0.0",
28
- "js-yaml": "^4.1.0"
37
+ "https-proxy-agent": "^7.0.6",
38
+ "js-yaml": "^4.3.2",
39
+ "proxy-from-env": "^1.1.0",
40
+ "ws": "^8.21.3"
29
41
  },
30
42
  "devDependencies": {
31
43
  "@types/js-yaml": "^4.0.9",
32
44
  "@types/node": "^22.0.0",
45
+ "@types/proxy-from-env": "^1.0.4",
46
+ "@types/ws": "^8.18.1",
33
47
  "typescript": "^5.7.0"
34
- },
35
- "scripts": {
36
- "build": "tsc",
37
- "dev": "tsc --watch",
38
- "start": "node dist/index.js",
39
- "test": "npm run build && node --test"
40
48
  }
41
- }
49
+ }