@tryarcanist/cli 0.1.0
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 +198 -0
- package/package.json +32 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/api.ts
|
|
7
|
+
async function apiFetch(config, path, init) {
|
|
8
|
+
const res = await fetch(`${config.apiUrl}${path}`, {
|
|
9
|
+
...init,
|
|
10
|
+
headers: {
|
|
11
|
+
"Content-Type": "application/json",
|
|
12
|
+
Authorization: `Bearer ${config.token}`,
|
|
13
|
+
...init?.headers
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
if (res.status === 401) {
|
|
17
|
+
throw new Error("Token is invalid or expired. Run `arcanist login` to re-authenticate.");
|
|
18
|
+
}
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
const body = await res.text().catch(() => "");
|
|
21
|
+
throw new Error(`API error ${res.status}: ${body}`);
|
|
22
|
+
}
|
|
23
|
+
return res.json();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/config.ts
|
|
27
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
28
|
+
import { homedir } from "os";
|
|
29
|
+
import { join } from "path";
|
|
30
|
+
var CONFIG_DIR = join(homedir(), ".arcanist");
|
|
31
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
32
|
+
function loadConfig() {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function saveConfig(config) {
|
|
40
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
41
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
42
|
+
}
|
|
43
|
+
function requireConfig() {
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
if (!config) {
|
|
46
|
+
console.error("Error: Not logged in. Run `arcanist login` first.");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
return config;
|
|
50
|
+
}
|
|
51
|
+
function validateApiUrl(url) {
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = new URL(url);
|
|
55
|
+
} catch {
|
|
56
|
+
return "Invalid URL format";
|
|
57
|
+
}
|
|
58
|
+
const host = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
59
|
+
const isLocal = host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
60
|
+
if (parsed.protocol !== "https:" && !isLocal) {
|
|
61
|
+
return "API URL must use HTTPS for non-local hosts";
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/commands/create.ts
|
|
67
|
+
async function createCommand(repoUrl, prompt, options) {
|
|
68
|
+
const config = requireConfig();
|
|
69
|
+
let sessionId;
|
|
70
|
+
try {
|
|
71
|
+
const sessionData = await apiFetch(config, "/api/sessions", {
|
|
72
|
+
method: "POST",
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
context: { repoUrl },
|
|
75
|
+
...options.model ? { model: options.model } : {}
|
|
76
|
+
})
|
|
77
|
+
});
|
|
78
|
+
sessionId = sessionData.sessionId;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error(`Error creating session: ${err instanceof Error ? err.message : String(err)}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
await apiFetch(config, `/api/sessions/${sessionId}/prompts`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
body: JSON.stringify({ prompt })
|
|
87
|
+
});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error(`Session created (${sessionId}) but prompt failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
90
|
+
console.error(`Retry with: arcanist message ${sessionId} "${prompt}"`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
const uiUrl = config.apiUrl.replace(/:\d+$/, ":5173");
|
|
94
|
+
console.log(`Session: ${sessionId}`);
|
|
95
|
+
console.log(`URL: ${uiUrl}/sessions/${sessionId}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/commands/login.ts
|
|
99
|
+
import { createInterface } from "readline";
|
|
100
|
+
async function loginCommand(options) {
|
|
101
|
+
let token;
|
|
102
|
+
if (options.tokenStdin) {
|
|
103
|
+
const chunks = [];
|
|
104
|
+
for await (const chunk of process.stdin) {
|
|
105
|
+
chunks.push(chunk);
|
|
106
|
+
}
|
|
107
|
+
token = Buffer.concat(chunks).toString().trim();
|
|
108
|
+
} else {
|
|
109
|
+
token = await promptHidden("Enter your CLI token: ");
|
|
110
|
+
}
|
|
111
|
+
if (!token) {
|
|
112
|
+
console.error("Error: No token provided.");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
if (!token.startsWith("arc_")) {
|
|
116
|
+
console.error("Error: Invalid token format. Token must start with 'arc_'.");
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const apiUrl = options.apiUrl ?? loadConfig()?.apiUrl ?? "https://app.tryarcanist.com";
|
|
120
|
+
if (options.apiUrl) {
|
|
121
|
+
const urlError = validateApiUrl(options.apiUrl);
|
|
122
|
+
if (urlError) {
|
|
123
|
+
console.error(`Error: ${urlError}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
saveConfig({ apiUrl, token });
|
|
128
|
+
console.log(`Logged in. API: ${apiUrl}`);
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch(`${apiUrl}/api/cli-tokens`, {
|
|
131
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
132
|
+
});
|
|
133
|
+
if (res.ok) {
|
|
134
|
+
console.log("Token verified.");
|
|
135
|
+
} else if (res.status === 401) {
|
|
136
|
+
console.warn("Warning: Token could not be verified (401). It may be invalid or expired.");
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
console.warn("Warning: Could not reach API to verify token.");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function promptHidden(prompt) {
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
145
|
+
process.stdout.write(prompt);
|
|
146
|
+
const stdin = process.stdin;
|
|
147
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
148
|
+
let input = "";
|
|
149
|
+
const onData = (char) => {
|
|
150
|
+
const c = char.toString();
|
|
151
|
+
if (c === "\n" || c === "\r") {
|
|
152
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
153
|
+
stdin.removeListener("data", onData);
|
|
154
|
+
process.stdout.write("\n");
|
|
155
|
+
rl.close();
|
|
156
|
+
resolve(input);
|
|
157
|
+
} else if (c === "") {
|
|
158
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
} else if (c === "\x7F" || c === "\b") {
|
|
161
|
+
input = input.slice(0, -1);
|
|
162
|
+
} else {
|
|
163
|
+
input += c;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
stdin.on("data", onData);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/commands/message.ts
|
|
171
|
+
async function messageCommand(sessionId, prompt) {
|
|
172
|
+
const config = requireConfig();
|
|
173
|
+
try {
|
|
174
|
+
await apiFetch(config, `/api/sessions/${sessionId}/prompts`, {
|
|
175
|
+
method: "POST",
|
|
176
|
+
body: JSON.stringify({ prompt })
|
|
177
|
+
});
|
|
178
|
+
console.log(`Message sent to session ${sessionId}.`);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/index.ts
|
|
186
|
+
var program = new Command().name("arcanist").description("Arcanist CLI").version("0.1.0");
|
|
187
|
+
program.command("login").description("Authenticate with a personal access token").option("--token-stdin", "Read token from stdin instead of interactive prompt").option("--api-url <url>", "Set custom API URL").action(loginCommand);
|
|
188
|
+
program.command("create").description("Create a session and send a prompt").argument("<repo-url>", "Repository URL").argument("<prompt>", "Prompt to send").option("--model <model>", "Model to use").action(createCommand);
|
|
189
|
+
program.command("message").description("Send a message to an existing session").argument("<session-id>", "Session ID").argument("<prompt>", "Message to send").action(messageCommand);
|
|
190
|
+
async function main() {
|
|
191
|
+
try {
|
|
192
|
+
await program.parseAsync(process.argv);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error(`Error: ${err instanceof Error ? err.message : "An unexpected error occurred."}`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tryarcanist/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for Arcanist — create and manage coding agent sessions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"arcanist": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/tryarcanist/arcanist",
|
|
22
|
+
"directory": "apps/cli"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://www.tryarcanist.com",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"commander": "^12.1.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"tsup": "^8.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|