@tryarcanist/cli 0.1.157 → 0.1.159

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 (2) hide show
  1. package/dist/index.js +115 -22
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ function normalizeBaseUrl(url) {
13
13
  }
14
14
 
15
15
  // src/errors.ts
16
+ var EXIT_CODE_INTERRUPTED = 130;
16
17
  var CliError = class extends Error {
17
18
  code;
18
19
  exitCode;
@@ -139,11 +140,12 @@ async function apiFetchText(config, path, init) {
139
140
  }
140
141
 
141
142
  // src/config.ts
142
- import { mkdirSync, readFileSync, writeFileSync } from "fs";
143
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
143
144
  import { homedir } from "os";
144
- import { join } from "path";
145
+ import { dirname, join } from "path";
145
146
  var CONFIG_DIR = join(homedir(), ".arcanist");
146
147
  var CONFIG_FILE = join(CONFIG_DIR, "config.json");
148
+ var PROJECT_CONFIG_FILE = ".arcanist-cli.json";
147
149
  var DEFAULT_API_URL = "https://app.tryarcanist.com";
148
150
  function loadFileConfig() {
149
151
  try {
@@ -154,12 +156,19 @@ function loadFileConfig() {
154
156
  }
155
157
  function loadConfig(overrides = {}) {
156
158
  const fileConfig = loadFileConfig();
157
- const apiUrl = overrides.apiUrl ?? process.env.ARCANIST_API_URL ?? fileConfig?.apiUrl;
158
- const token = overrides.token ?? process.env.ARCANIST_TOKEN ?? fileConfig?.token;
159
+ const completeEnvOrFlagConfig = resolveCompleteEnvOrFlagConfig(overrides);
160
+ if (completeEnvOrFlagConfig) return validateAndNormalizeConfig(completeEnvOrFlagConfig);
161
+ const projectConfig = loadProjectConfig();
162
+ const envOrFlagConfig = resolveEnvOrFlagConfig(overrides, projectConfig !== null);
163
+ const apiUrl = envOrFlagConfig?.apiUrl ?? projectConfig?.apiUrl ?? fileConfig?.apiUrl;
164
+ const token = envOrFlagConfig?.token ?? projectConfig?.token ?? fileConfig?.token;
159
165
  if (!apiUrl || !token) return null;
160
- const urlError = validateApiUrl(apiUrl);
161
- if (urlError) throw new CliError("user", urlError);
162
- return { apiUrl: normalizeBaseUrl(apiUrl), token };
166
+ const config = validateAndNormalizeConfig({ apiUrl, token });
167
+ if (projectConfig && !envOrFlagConfig && !overrides.json) {
168
+ process.stderr.write(`using ${PROJECT_CONFIG_FILE} -> ${config.apiUrl}
169
+ `);
170
+ }
171
+ return config;
163
172
  }
164
173
  function saveConfig(config) {
165
174
  const urlError = validateApiUrl(config.apiUrl);
@@ -182,6 +191,19 @@ function resolveLoginApiUrl(apiUrl) {
182
191
  if (urlError) throw new CliError("user", urlError);
183
192
  return normalizeBaseUrl(raw);
184
193
  }
194
+ function loadProjectConfig(cwd = process.cwd()) {
195
+ const gitRoot = findGitRoot(cwd);
196
+ if (!gitRoot) return null;
197
+ let current = cwd;
198
+ while (true) {
199
+ const configPath = join(current, PROJECT_CONFIG_FILE);
200
+ if (existsSync(configPath)) return parseProjectConfig(configPath);
201
+ if (current === gitRoot) return null;
202
+ const parent = dirname(current);
203
+ if (parent === current) return null;
204
+ current = parent;
205
+ }
206
+ }
185
207
  function validateApiUrl(url) {
186
208
  let parsed;
187
209
  try {
@@ -193,16 +215,84 @@ function validateApiUrl(url) {
193
215
  return "API URL must not include embedded credentials. Use --token or ARCANIST_TOKEN to authenticate.";
194
216
  }
195
217
  const host = parsed.hostname.replace(/^\[|\]$/g, "");
196
- const isLocal = host === "localhost" || host === "127.0.0.1" || host === "::1";
197
- if (parsed.protocol !== "https:" && !isLocal) {
218
+ if (parsed.protocol !== "https:" && !isLoopbackHost(parsed)) {
198
219
  return "API URL must use HTTPS for non-local hosts";
199
220
  }
200
221
  return null;
201
222
  }
223
+ function resolveEnvOrFlagConfig(overrides, projectActive) {
224
+ const apiUrl = overrides.apiUrl ?? process.env.ARCANIST_API_URL;
225
+ const token = overrides.token ?? process.env.ARCANIST_TOKEN;
226
+ if (!projectActive) {
227
+ if (!apiUrl && !token) return null;
228
+ return { apiUrl, token };
229
+ }
230
+ if (apiUrl && !token || !apiUrl && token) {
231
+ throw new CliError(
232
+ "user",
233
+ `${PROJECT_CONFIG_FILE} is active; set both ARCANIST_API_URL and ARCANIST_TOKEN, pass both --api-url and --token, or set neither.`
234
+ );
235
+ }
236
+ if (!apiUrl || !token) return null;
237
+ return { apiUrl, token };
238
+ }
239
+ function resolveCompleteEnvOrFlagConfig(overrides) {
240
+ const apiUrl = overrides.apiUrl ?? process.env.ARCANIST_API_URL;
241
+ const token = overrides.token ?? process.env.ARCANIST_TOKEN;
242
+ if (!apiUrl || !token) return null;
243
+ return { apiUrl, token };
244
+ }
245
+ function validateAndNormalizeConfig(config) {
246
+ const urlError = validateApiUrl(config.apiUrl);
247
+ if (urlError) throw new CliError("user", urlError);
248
+ return { apiUrl: normalizeBaseUrl(config.apiUrl), token: config.token };
249
+ }
250
+ function findGitRoot(cwd) {
251
+ let current = cwd;
252
+ while (true) {
253
+ if (existsSync(join(current, ".git"))) return current;
254
+ const parent = dirname(current);
255
+ if (parent === current) return null;
256
+ current = parent;
257
+ }
258
+ }
259
+ function parseProjectConfig(configPath) {
260
+ let parsed;
261
+ try {
262
+ parsed = JSON.parse(readFileSync(configPath, "utf-8"));
263
+ } catch {
264
+ throw new CliError("user", `${PROJECT_CONFIG_FILE} is not valid JSON.`);
265
+ }
266
+ if (!parsed || typeof parsed !== "object") {
267
+ throw new CliError("user", `${PROJECT_CONFIG_FILE} must contain both apiUrl and token.`);
268
+ }
269
+ const config = parsed;
270
+ if (typeof config.apiUrl !== "string" || typeof config.token !== "string" || !config.apiUrl || !config.token) {
271
+ throw new CliError("user", `${PROJECT_CONFIG_FILE} must contain both apiUrl and token.`);
272
+ }
273
+ let url;
274
+ try {
275
+ url = new URL(config.apiUrl);
276
+ } catch {
277
+ throw new CliError("user", `${PROJECT_CONFIG_FILE} apiUrl must be a valid URL.`);
278
+ }
279
+ if (!isLoopbackHost(url)) {
280
+ throw new CliError("user", `${PROJECT_CONFIG_FILE} apiUrl must be a loopback host.`);
281
+ }
282
+ const urlError = validateApiUrl(config.apiUrl);
283
+ if (urlError) throw new CliError("user", urlError);
284
+ return { apiUrl: normalizeBaseUrl(config.apiUrl), token: config.token };
285
+ }
286
+ function isLoopbackHost(parsed) {
287
+ const host = parsed.hostname.replace(/^\[|\]$/g, "");
288
+ return host === "localhost" || host === "127.0.0.1" || host === "::1";
289
+ }
202
290
 
203
291
  // src/runtime.ts
204
292
  import { randomUUID } from "crypto";
205
293
  import { createInterface } from "readline/promises";
294
+ var ASCII_FIRST_PRINTABLE = 32;
295
+ var ASCII_DELETE = 127;
206
296
  var ANSI_CONTROL_SEQUENCE = /\u001b\[[0-9:;<=>?]*[ -/]*[@-~]/g;
207
297
  function getRuntimeOptions(command, options = {}) {
208
298
  const globals = command?.optsWithGlobals?.();
@@ -290,7 +380,7 @@ async function readHiddenPrompt(prompt) {
290
380
  return;
291
381
  }
292
382
  if (char === "") {
293
- fail2(new CliError("user", "Interrupted.", { exitCode: 130 }));
383
+ fail2(new CliError("user", "Interrupted.", { exitCode: EXIT_CODE_INTERRUPTED }));
294
384
  return;
295
385
  }
296
386
  if (char === "\x7F" || char === "\b") {
@@ -373,7 +463,7 @@ function matchAnsiControlSequence(text, startIndex) {
373
463
  }
374
464
  function isControlCharacter(char) {
375
465
  const code = char.charCodeAt(0);
376
- return code < 32 || code === 127;
466
+ return code < ASCII_FIRST_PRINTABLE || code === ASCII_DELETE;
377
467
  }
378
468
 
379
469
  // src/commands/auth.ts
@@ -2055,6 +2145,8 @@ function formatSessionErrorMessage(error, code) {
2055
2145
  }
2056
2146
 
2057
2147
  // src/utils/session-output.ts
2148
+ var SHORT_SESSION_ID_LENGTH = 8;
2149
+ var PROMPT_LABEL_MAX_CHARS = 80;
2058
2150
  var VALID_PHASES = /* @__PURE__ */ new Set([
2059
2151
  "idle",
2060
2152
  "running",
@@ -2186,7 +2278,7 @@ function renderSessionTranscript(exportData) {
2186
2278
  const eventBuckets = partitionEventsByPrompt(exportData.events, promptIds);
2187
2279
  lines.push("# Session transcript\n");
2188
2280
  if (exportData.session.repoUrl) lines.push(`**Repo:** ${exportData.session.repoUrl} `);
2189
- lines.push(`**Session:** ${exportData.session.id.slice(0, 8)} `);
2281
+ lines.push(`**Session:** ${exportData.session.id.slice(0, SHORT_SESSION_ID_LENGTH)} `);
2190
2282
  lines.push(`**Created:** ${formatDate(exportData.session.createdAt)} `);
2191
2283
  lines.push(`**Status:** ${exportData.session.status} `);
2192
2284
  const noChangeResult = latestCompletedPromptResult(exportData.prompts);
@@ -2347,7 +2439,7 @@ function formatPromptLabel(promptId, promptLabels) {
2347
2439
  function buildPromptLabelMap(prompts) {
2348
2440
  const labels = /* @__PURE__ */ new Map();
2349
2441
  for (const prompt of prompts) {
2350
- labels.set(prompt.promptId, prompt.prompt.replace(/\s+/g, " ").trim().slice(0, 80));
2442
+ labels.set(prompt.promptId, prompt.prompt.replace(/\s+/g, " ").trim().slice(0, PROMPT_LABEL_MAX_CHARS));
2351
2443
  }
2352
2444
  return labels;
2353
2445
  }
@@ -3051,8 +3143,8 @@ async function messageCommand(sessionId, promptArg, options = {}, command) {
3051
3143
 
3052
3144
  // src/commands/sandbox.ts
3053
3145
  import { execFileSync } from "child_process";
3054
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3055
- import { dirname } from "path";
3146
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3147
+ import { dirname as dirname2 } from "path";
3056
3148
 
3057
3149
  // ../../shared/sandbox-layer/parser.ts
3058
3150
  import { isMap, isScalar, isSeq, parseDocument } from "yaml";
@@ -3584,6 +3676,7 @@ async function parseSandboxLayerSource(input) {
3584
3676
  // src/commands/sandbox.ts
3585
3677
  var DEFAULT_MANIFEST_PATH = SANDBOX_LAYER_MANIFEST_PATH;
3586
3678
  var DEFAULT_POLL_INTERVAL_MS = 2500;
3679
+ var MIN_POLL_INTERVAL_MS = 250;
3587
3680
  var TERMINAL_BUILD_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "canceled"]);
3588
3681
  function git(args) {
3589
3682
  try {
@@ -3610,7 +3703,7 @@ function assertCleanAndPushed() {
3610
3703
  throw new CliError("user", "Current commit is not pushed; push before building the sandbox layer.");
3611
3704
  }
3612
3705
  function assertManifestExists(path) {
3613
- if (!existsSync(path)) throw new CliError("user", `Missing sandbox manifest: ${path}`);
3706
+ if (!existsSync2(path)) throw new CliError("user", `Missing sandbox manifest: ${path}`);
3614
3707
  }
3615
3708
  function parseRepoArg2(value, fallback) {
3616
3709
  if (!value) return fallback();
@@ -3633,8 +3726,8 @@ async function resolveBusinessId2(config, options) {
3633
3726
  function parsePollInterval2(value) {
3634
3727
  if (!value) return DEFAULT_POLL_INTERVAL_MS;
3635
3728
  const parsed = Number(value);
3636
- if (!Number.isInteger(parsed) || parsed < 250) {
3637
- throw new CliError("user", "--poll-interval must be an integer >= 250.");
3729
+ if (!Number.isInteger(parsed) || parsed < MIN_POLL_INTERVAL_MS) {
3730
+ throw new CliError("user", `--poll-interval must be an integer >= ${MIN_POLL_INTERVAL_MS}.`);
3638
3731
  }
3639
3732
  return parsed;
3640
3733
  }
@@ -3832,10 +3925,10 @@ function sourceBody(sourceRepo, manifestPath) {
3832
3925
  }
3833
3926
  async function sandboxInitCommand(options = {}, command) {
3834
3927
  const runtime = getRuntimeOptions(command, options);
3835
- if (existsSync(DEFAULT_MANIFEST_PATH)) throw new CliError("conflict", `${DEFAULT_MANIFEST_PATH} already exists`);
3928
+ if (existsSync2(DEFAULT_MANIFEST_PATH)) throw new CliError("conflict", `${DEFAULT_MANIFEST_PATH} already exists`);
3836
3929
  const layerPath = ".arcanist/sandbox.layer.Dockerfile";
3837
- if (existsSync(layerPath)) throw new CliError("conflict", `${layerPath} already exists`);
3838
- mkdirSync2(dirname(DEFAULT_MANIFEST_PATH), { recursive: true });
3930
+ if (existsSync2(layerPath)) throw new CliError("conflict", `${layerPath} already exists`);
3931
+ mkdirSync2(dirname2(DEFAULT_MANIFEST_PATH), { recursive: true });
3839
3932
  writeFileSync2(
3840
3933
  DEFAULT_MANIFEST_PATH,
3841
3934
  [
@@ -4738,7 +4831,7 @@ async function main() {
4738
4831
  if (isCommanderHelpOrVersion(err)) {
4739
4832
  process.exit(0);
4740
4833
  }
4741
- if (err instanceof CliError && err.exitCode === 130) restoreTerminal();
4834
+ if (err instanceof CliError && err.exitCode === EXIT_CODE_INTERRUPTED) restoreTerminal();
4742
4835
  const cliError = toCliError(err);
4743
4836
  const options = program.opts();
4744
4837
  if (options.json) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryarcanist/cli",
3
- "version": "0.1.157",
3
+ "version": "0.1.159",
4
4
  "description": "CLI for Arcanist — create and manage coding agent sessions",
5
5
  "type": "module",
6
6
  "bin": {