@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.
- package/dist/index.js +115 -22
- 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
|
|
158
|
-
|
|
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
|
|
161
|
-
if (
|
|
162
|
-
|
|
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
|
-
|
|
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:
|
|
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 <
|
|
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,
|
|
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,
|
|
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 (!
|
|
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 <
|
|
3637
|
-
throw new CliError("user",
|
|
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 (
|
|
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 (
|
|
3838
|
-
mkdirSync2(
|
|
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 ===
|
|
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) {
|