@tryarcanist/cli 0.1.167 → 0.1.168

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.
Files changed (3) hide show
  1. package/README.md +12 -1
  2. package/dist/index.js +116 -38
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -53,9 +53,12 @@ Supported environment variables:
53
53
  ```bash
54
54
  export ARCANIST_TOKEN=arc_...
55
55
  export ARCANIST_API_URL=https://api.tryarcanist.com
56
+ export ARCANIST_HTTP_TIMEOUT_MS=30000
56
57
  ```
57
58
 
58
59
  `--token` and `--api-url` are available for one-off invocations. Prefer `ARCANIST_TOKEN` over `--token` for persistent use; CLI flags can appear in shell history and process lists.
60
+ Each HTTP request times out after `ARCANIST_HTTP_TIMEOUT_MS` milliseconds, default `30000`.
61
+ The value must be between `1000` and `600000`; invalid values fail closed instead of falling back silently.
59
62
 
60
63
  Non-local API URLs must use HTTPS and must not embed credentials.
61
64
 
@@ -89,10 +92,18 @@ With `--json`, successful command output is machine-readable and newline-termina
89
92
 
90
93
  ```json
91
94
  {
92
- "error": { "code": "auth", "message": "Not logged in.", "hint": "Run `arcanist auth login` or set `ARCANIST_TOKEN`." }
95
+ "error": {
96
+ "code": "auth",
97
+ "message": "Not logged in.",
98
+ "hint": "Run `arcanist auth login` or set `ARCANIST_TOKEN`.",
99
+ "data": { "serverCode": "token_revoked" }
100
+ }
93
101
  }
94
102
  ```
95
103
 
104
+ `error.data` is omitted when there is no structured recovery data.
105
+ Stable fields are `serverCode` for server-provided error codes and `sessionId`/`sessionUrl` when `sessions create` created the session but failed to enqueue the prompt.
106
+
96
107
  Exit codes:
97
108
 
98
109
  ```text
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ var CliError = class extends Error {
24
24
  exitCode;
25
25
  hint;
26
26
  requestId;
27
+ data;
27
28
  constructor(code, message, options = {}) {
28
29
  super(message);
29
30
  this.name = "CliError";
@@ -31,17 +32,20 @@ var CliError = class extends Error {
31
32
  this.exitCode = options.exitCode ?? exitCodeForErrorCode(code);
32
33
  this.hint = options.hint;
33
34
  this.requestId = options.requestId;
35
+ this.data = options.data;
34
36
  }
35
37
  };
36
38
  var ApiError = class extends CliError {
37
39
  status;
38
40
  body;
39
- constructor(status, body, requestId) {
41
+ constructor(status, body, requestId, resourceNoun) {
40
42
  const code = codeForHttpStatus(status);
41
- super(code, messageForApiError(status, body), {
43
+ const parsed = parseApiErrorBody(body);
44
+ super(code, messageForApiError(status, body, parsed), {
42
45
  exitCode: exitCodeForHttpStatus(status),
43
- hint: hintForHttpStatus(status),
44
- requestId
46
+ hint: hintForHttpStatus(status, resourceNoun),
47
+ requestId,
48
+ data: parsed?.serverCode ? { serverCode: parsed.serverCode } : void 0
45
49
  });
46
50
  this.name = "ApiError";
47
51
  this.status = status;
@@ -73,9 +77,9 @@ function codeForHttpStatus(status) {
73
77
  function exitCodeForHttpStatus(status) {
74
78
  return exitCodeForErrorCode(codeForHttpStatus(status));
75
79
  }
76
- function messageForApiError(status, body) {
77
- const parsed = parseApiErrorBody(body);
78
- if (parsed) return parsed;
80
+ function messageForApiError(status, body, parsed = parseApiErrorBody(body)) {
81
+ if (parsed?.message) return parsed.message;
82
+ if (parsed?.serverCode) return parsed.serverCode;
79
83
  return body ? `API error ${status}: ${body}` : `API error ${status}`;
80
84
  }
81
85
  function parseApiErrorBody(body) {
@@ -83,14 +87,36 @@ function parseApiErrorBody(body) {
83
87
  const parsed = JSON.parse(body);
84
88
  if (!parsed || typeof parsed !== "object") return null;
85
89
  const error = parsed.error;
86
- return typeof error === "string" && error.length > 0 ? error : null;
90
+ if (typeof error === "string" && error.length > 0) {
91
+ const message2 = parsed.message;
92
+ const code = parsed.code;
93
+ return {
94
+ serverCode: typeof code === "string" && code ? code : error,
95
+ ...typeof message2 === "string" && message2 ? { message: message2 } : { message: error }
96
+ };
97
+ }
98
+ if (error && typeof error === "object") {
99
+ const code = error.code;
100
+ const message2 = error.message;
101
+ return {
102
+ ...typeof message2 === "string" && message2 ? { message: message2 } : {},
103
+ ...typeof code === "string" && code ? { serverCode: code } : {}
104
+ };
105
+ }
106
+ const message = parsed.message;
107
+ return typeof message === "string" && message.length > 0 ? { message } : null;
87
108
  } catch {
88
109
  return null;
89
110
  }
90
111
  }
91
- function hintForHttpStatus(status) {
112
+ function hintForHttpStatus(status, resourceNoun) {
92
113
  if (status === 401 || status === 403) return "Run `arcanist auth login` or set `ARCANIST_TOKEN`.";
93
- if (status === 404) return "List sessions with `arcanist sessions list`.";
114
+ if (status === 404) {
115
+ if (resourceNoun === "token") return "List tokens with `arcanist tokens list`.";
116
+ if (resourceNoun === "sandbox") return "Check sandbox state with `arcanist sandbox status`.";
117
+ if (resourceNoun === "repo") return "Check accessible repositories with `arcanist repos list`.";
118
+ return "List sessions with `arcanist sessions list`.";
119
+ }
94
120
  if (status === 409) return "Check the current resource state and retry the command when it is ready.";
95
121
  if (status >= 500) return "Retry later or check the control-plane logs.";
96
122
  return void 0;
@@ -106,7 +132,8 @@ function formatJsonError(err) {
106
132
  code: err.code,
107
133
  message: err.message,
108
134
  ...err.hint ? { hint: err.hint } : {},
109
- ...err.requestId ? { requestId: err.requestId } : {}
135
+ ...err.requestId ? { requestId: err.requestId } : {},
136
+ ...err.data ? { data: err.data } : {}
110
137
  }
111
138
  });
112
139
  }
@@ -115,33 +142,72 @@ function formatJsonError(err) {
115
142
  var require2 = createRequire(import.meta.url);
116
143
  var { version } = require2("../package.json");
117
144
  var CLI_USER_AGENT = `arcanist-cli/${version}`;
118
- async function apiRequest(config, path, init) {
119
- let res;
145
+ var DEFAULT_HTTP_TIMEOUT_MS = 3e4;
146
+ var MIN_HTTP_TIMEOUT_MS = 1e3;
147
+ var MAX_HTTP_TIMEOUT_MS = 6e5;
148
+ var HTTP_TIMEOUT_ENV = "ARCANIST_HTTP_TIMEOUT_MS";
149
+ function parseHttpTimeoutMs(env = process.env) {
150
+ const raw = env[HTTP_TIMEOUT_ENV];
151
+ if (!raw) return DEFAULT_HTTP_TIMEOUT_MS;
152
+ if (!/^\d+$/.test(raw)) {
153
+ throw new CliError("user", `${HTTP_TIMEOUT_ENV} must be a positive integer between 1000 and 600000.`);
154
+ }
155
+ const value = Number(raw);
156
+ if (value < MIN_HTTP_TIMEOUT_MS || value > MAX_HTTP_TIMEOUT_MS) {
157
+ throw new CliError("user", `${HTTP_TIMEOUT_ENV} must be between 1000 and 600000 milliseconds.`);
158
+ }
159
+ return value;
160
+ }
161
+ function isAbortError(err) {
162
+ return err instanceof DOMException && err.name === "AbortError";
163
+ }
164
+ function timeoutError(timeoutMs) {
165
+ return new CliError("server", `Network timeout after ${timeoutMs}ms.`, {
166
+ hint: `Increase ${HTTP_TIMEOUT_ENV} for slow links, or check the API connection.`
167
+ });
168
+ }
169
+ async function apiRequest(config, path, init, read) {
170
+ const timeoutMs = parseHttpTimeoutMs();
171
+ const controller = new AbortController();
172
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
120
173
  try {
121
174
  const headers = new Headers(init?.headers);
122
175
  headers.set("Content-Type", "application/json");
123
176
  headers.set("User-Agent", CLI_USER_AGENT);
124
177
  headers.set("Authorization", `Bearer ${config.token}`);
125
- res = await fetch(`${normalizeBaseUrl(config.apiUrl)}${path}`, {
178
+ const res = await fetch(`${normalizeBaseUrl(config.apiUrl)}${path}`, {
126
179
  ...init,
127
- headers
180
+ headers,
181
+ signal: controller.signal
128
182
  });
183
+ if (!res.ok) {
184
+ const body = await res.text().catch((err) => {
185
+ if (isAbortError(err)) throw timeoutError(timeoutMs);
186
+ return "";
187
+ });
188
+ throw new ApiError(res.status, body, res.headers.get("x-request-id") ?? void 0, resourceNounForPath(path));
189
+ }
190
+ return await read(res);
129
191
  } catch (err) {
192
+ if (err instanceof CliError) throw err;
193
+ if (isAbortError(err)) throw timeoutError(timeoutMs);
130
194
  throw new CliError("server", `Network error: ${stringifyError(err)}`);
195
+ } finally {
196
+ clearTimeout(timeout);
131
197
  }
132
- if (!res.ok) {
133
- const body = await res.text().catch(() => "");
134
- throw new ApiError(res.status, body, res.headers.get("x-request-id") ?? void 0);
135
- }
136
- return res;
198
+ }
199
+ function resourceNounForPath(path) {
200
+ if (path.startsWith("/api/cli-tokens")) return "token";
201
+ if (path.startsWith("/api/repos")) return "repo";
202
+ if (path.startsWith("/api/sandbox")) return "sandbox";
203
+ if (path.startsWith("/api/sessions")) return "session";
204
+ return void 0;
137
205
  }
138
206
  async function apiFetch(config, path, init) {
139
- const res = await apiRequest(config, path, init);
140
- return res.json();
207
+ return apiRequest(config, path, init, (res) => res.json());
141
208
  }
142
209
  async function apiFetchText(config, path, init) {
143
- const res = await apiRequest(config, path, init);
144
- return res.text();
210
+ return apiRequest(config, path, init, (res) => res.text());
145
211
  }
146
212
  async function resolveBusinessId(config, options) {
147
213
  if (options.business && options.business.trim()) return options.business.trim();
@@ -1126,8 +1192,9 @@ function isTerminalPhase(phase, _sessionKind) {
1126
1192
  }
1127
1193
 
1128
1194
  // src/constants/watch.ts
1129
- var MIN_WATCH_POLL_INTERVAL_MS = 1;
1195
+ var MIN_WATCH_POLL_INTERVAL_MS = 250;
1130
1196
  var DEFAULT_WATCH_POLL_INTERVAL_MS = 1e3;
1197
+ var MAX_WATCH_POLL_INTERVAL_MS = 6e4;
1131
1198
  var WATCH_REPLAY_PAGE_SIZE = 200;
1132
1199
  function isWatchTerminal(phase) {
1133
1200
  return isTerminalPhase(phase);
@@ -2702,7 +2769,14 @@ function parsePollInterval(raw) {
2702
2769
  if (!/^\d+$/.test(raw)) {
2703
2770
  throw new CliError("user", "Polling interval must be a non-negative integer.");
2704
2771
  }
2705
- return Number(raw);
2772
+ const value = Number(raw);
2773
+ if (value < MIN_WATCH_POLL_INTERVAL_MS || value > MAX_WATCH_POLL_INTERVAL_MS) {
2774
+ throw new CliError(
2775
+ "user",
2776
+ `Polling interval must be between ${MIN_WATCH_POLL_INTERVAL_MS} and ${MAX_WATCH_POLL_INTERVAL_MS} milliseconds.`
2777
+ );
2778
+ }
2779
+ return value;
2706
2780
  }
2707
2781
  function parseNonNegativeInteger(raw, name, defaultValue) {
2708
2782
  if (!raw) return defaultValue;
@@ -2762,7 +2836,6 @@ async function watchCommand(sessionId, options, command) {
2762
2836
  const runtime = getRuntimeOptions(command, options);
2763
2837
  const config = requireConfig(runtime);
2764
2838
  const pollIntervalMs = parsePollInterval(options.pollInterval);
2765
- const effectivePollIntervalMs = Math.max(pollIntervalMs, MIN_WATCH_POLL_INTERVAL_MS);
2766
2839
  const initialAfterSequence = parseNonNegativeInteger(options.afterSequence, "--after-sequence", 0);
2767
2840
  const pageSize = parseNonNegativeInteger(options.limit, "--limit", WATCH_REPLAY_PAGE_SIZE);
2768
2841
  if (pageSize === 0) {
@@ -2845,7 +2918,7 @@ async function watchCommand(sessionId, options, command) {
2845
2918
  }
2846
2919
  break;
2847
2920
  }
2848
- await sleep(effectivePollIntervalMs);
2921
+ await sleep(pollIntervalMs);
2849
2922
  }
2850
2923
  } finally {
2851
2924
  if (textOpen) process.stdout.write("\n");
@@ -3020,7 +3093,12 @@ async function createCommand(repoUrl, promptArg, options, command) {
3020
3093
  {
3021
3094
  exitCode: err instanceof CliError ? err.exitCode : void 0,
3022
3095
  hint: `Retry with: arcanist sessions send ${sessionId} --prompt-stdin`,
3023
- requestId: err instanceof CliError ? err.requestId : void 0
3096
+ requestId: err instanceof CliError ? err.requestId : void 0,
3097
+ data: {
3098
+ ...err instanceof CliError && err.data ? err.data : {},
3099
+ sessionId,
3100
+ ...sessionData.sessionUrl ? { sessionUrl: sessionData.sessionUrl } : {}
3101
+ }
3024
3102
  }
3025
3103
  );
3026
3104
  }
@@ -3232,16 +3310,16 @@ async function loginCommand(options, command) {
3232
3310
  console.log(`Logged in. API: ${apiUrl}`);
3233
3311
  }
3234
3312
  try {
3235
- const res = await fetch(`${apiUrl}/api/cli-tokens`, {
3236
- headers: { Authorization: `Bearer ${token}`, "User-Agent": CLI_USER_AGENT }
3237
- });
3238
- if (res.ok && !runtime.json && !runtime.quiet) {
3239
- console.log("Token verified.");
3240
- } else if (res.status === 401 && !runtime.json && !runtime.quiet) {
3241
- console.warn("Warning: Token could not be verified (401). It may be invalid or expired.");
3313
+ await apiFetch({ apiUrl, token }, "/api/cli-tokens");
3314
+ if (!runtime.json && !runtime.quiet) console.log("Token verified.");
3315
+ } catch (err) {
3316
+ if (!runtime.json && !runtime.quiet) {
3317
+ if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
3318
+ console.warn("Warning: Token could not be verified (401). It may be invalid or expired.");
3319
+ } else {
3320
+ console.warn("Warning: Could not reach API to verify token.");
3321
+ }
3242
3322
  }
3243
- } catch {
3244
- if (!runtime.json && !runtime.quiet) console.warn("Warning: Could not reach API to verify token.");
3245
3323
  }
3246
3324
  }
3247
3325
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.167",
3
+ "version": "0.1.168",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {