custie 1.0.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/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/chunk-55T4S4ZY.js +521 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +850 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/package.json +47 -0
- package/slack-app-manifest.yml +38 -0
- package/system.default.md +5 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ensureDirs,
|
|
4
|
+
loadEnvFiles,
|
|
5
|
+
paths,
|
|
6
|
+
startServer
|
|
7
|
+
} from "./chunk-55T4S4ZY.js";
|
|
8
|
+
|
|
9
|
+
// src/commands/start.ts
|
|
10
|
+
async function runStart() {
|
|
11
|
+
loadEnvFiles();
|
|
12
|
+
await startServer();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/commands/prompt.ts
|
|
16
|
+
import { existsSync, copyFileSync } from "fs";
|
|
17
|
+
import { spawn } from "child_process";
|
|
18
|
+
import { resolve } from "path";
|
|
19
|
+
async function runPrompt() {
|
|
20
|
+
ensureDirs();
|
|
21
|
+
if (!existsSync(paths.PROMPT_FILE)) {
|
|
22
|
+
const defaultPrompt = resolve(paths.PACKAGE_ROOT, "system.default.md");
|
|
23
|
+
if (!existsSync(defaultPrompt)) {
|
|
24
|
+
console.error("[custie] system.default.md not found in package root.");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
copyFileSync(defaultPrompt, paths.PROMPT_FILE);
|
|
28
|
+
console.log(`[custie] Copied default prompt to ${paths.PROMPT_FILE}`);
|
|
29
|
+
}
|
|
30
|
+
const editor = process.env["EDITOR"] || "vi";
|
|
31
|
+
console.log(`[custie] Opening ${paths.PROMPT_FILE} in ${editor}...`);
|
|
32
|
+
const child = spawn(editor, [paths.PROMPT_FILE], { stdio: "inherit" });
|
|
33
|
+
return new Promise((resolve5, reject) => {
|
|
34
|
+
child.on("close", (code) => {
|
|
35
|
+
if (code === 0) {
|
|
36
|
+
resolve5();
|
|
37
|
+
} else {
|
|
38
|
+
reject(new Error(`Editor exited with code ${code}`));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
child.on("error", (err) => {
|
|
42
|
+
reject(new Error(`Failed to open editor: ${err.message}`));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/commands/config.ts
|
|
48
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
|
|
49
|
+
import { spawn as spawn2 } from "child_process";
|
|
50
|
+
import { resolve as resolve2 } from "path";
|
|
51
|
+
function maskToken(value) {
|
|
52
|
+
if (!value || value.length < 12) return "****";
|
|
53
|
+
return value.slice(0, 8) + "..." + value.slice(-4);
|
|
54
|
+
}
|
|
55
|
+
function isSensitive(key) {
|
|
56
|
+
const lower = key.toLowerCase();
|
|
57
|
+
return lower.includes("token") || lower.includes("secret");
|
|
58
|
+
}
|
|
59
|
+
function printConfig() {
|
|
60
|
+
loadEnvFiles();
|
|
61
|
+
console.log("\n Paths:");
|
|
62
|
+
console.log(` Config dir: ${paths.CONFIG_DIR}`);
|
|
63
|
+
console.log(` Data dir: ${paths.DATA_DIR}`);
|
|
64
|
+
console.log(` Config file: ${paths.CONFIG_FILE} ${existsSync2(paths.CONFIG_FILE) ? "(exists)" : "(not found)"}`);
|
|
65
|
+
console.log(` Prompt file: ${paths.PROMPT_FILE} ${existsSync2(paths.PROMPT_FILE) ? "(exists)" : "(not found)"}`);
|
|
66
|
+
console.log(` Database: ${paths.DB_FILE} ${existsSync2(paths.DB_FILE) ? "(exists)" : "(not found)"}`);
|
|
67
|
+
console.log(` Log dir: ${paths.LOG_DIR}`);
|
|
68
|
+
const repoEnv = resolve2(process.cwd(), ".env");
|
|
69
|
+
console.log(` Repo .env: ${repoEnv} ${existsSync2(repoEnv) ? "(exists)" : "(not found)"}`);
|
|
70
|
+
const envKeys = [
|
|
71
|
+
"SLACK_BOT_TOKEN",
|
|
72
|
+
"SLACK_APP_TOKEN",
|
|
73
|
+
"SLACK_SIGNING_SECRET",
|
|
74
|
+
"CLAUDE_CWD",
|
|
75
|
+
"CLAUDE_CONFIG_DIR",
|
|
76
|
+
"BOT_NAME",
|
|
77
|
+
"ALLOWED_USER_IDS",
|
|
78
|
+
"OWNER_USER_ID",
|
|
79
|
+
"MAX_TURNS"
|
|
80
|
+
];
|
|
81
|
+
console.log("\n Environment:");
|
|
82
|
+
for (const key of envKeys) {
|
|
83
|
+
const value = process.env[key] ?? "";
|
|
84
|
+
const display = isSensitive(key) && value ? maskToken(value) : value || "(not set)";
|
|
85
|
+
console.log(` ${key}=${display}`);
|
|
86
|
+
}
|
|
87
|
+
console.log("");
|
|
88
|
+
}
|
|
89
|
+
function editConfig() {
|
|
90
|
+
ensureDirs();
|
|
91
|
+
if (!existsSync2(paths.CONFIG_FILE)) {
|
|
92
|
+
const defaultEnv = resolve2(paths.PACKAGE_ROOT, ".env.example");
|
|
93
|
+
if (existsSync2(defaultEnv)) {
|
|
94
|
+
const content = readFileSync(defaultEnv, "utf-8");
|
|
95
|
+
writeFileSync(paths.CONFIG_FILE, content);
|
|
96
|
+
} else {
|
|
97
|
+
writeFileSync(paths.CONFIG_FILE, "");
|
|
98
|
+
}
|
|
99
|
+
console.log(`[custie] Created ${paths.CONFIG_FILE}`);
|
|
100
|
+
}
|
|
101
|
+
const editor = process.env["EDITOR"] || "vi";
|
|
102
|
+
console.log(`[custie] Opening ${paths.CONFIG_FILE} in ${editor}...`);
|
|
103
|
+
const child = spawn2(editor, [paths.CONFIG_FILE], { stdio: "inherit" });
|
|
104
|
+
return new Promise((resolve5, reject) => {
|
|
105
|
+
child.on("close", (code) => {
|
|
106
|
+
if (code === 0) {
|
|
107
|
+
resolve5();
|
|
108
|
+
} else {
|
|
109
|
+
reject(new Error(`Editor exited with code ${code}`));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
child.on("error", (err) => {
|
|
113
|
+
reject(new Error(`Failed to open editor: ${err.message}`));
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async function runConfig(args) {
|
|
118
|
+
if (args.includes("--path")) {
|
|
119
|
+
console.log(paths.CONFIG_FILE);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (args.includes("--edit")) {
|
|
123
|
+
await editConfig();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
printConfig();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/commands/setup.ts
|
|
130
|
+
import { createInterface } from "readline";
|
|
131
|
+
import { existsSync as existsSync3, copyFileSync as copyFileSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
132
|
+
import { resolve as resolve3 } from "path";
|
|
133
|
+
import { tmpdir } from "os";
|
|
134
|
+
function log(msg) {
|
|
135
|
+
console.log(`
|
|
136
|
+
\x1B[36m>\x1B[0m ${msg}`);
|
|
137
|
+
}
|
|
138
|
+
function success(msg) {
|
|
139
|
+
console.log(`\x1B[32m+\x1B[0m ${msg}`);
|
|
140
|
+
}
|
|
141
|
+
function warn(msg) {
|
|
142
|
+
console.log(`\x1B[33m!\x1B[0m ${msg}`);
|
|
143
|
+
}
|
|
144
|
+
function error(msg) {
|
|
145
|
+
console.error(`\x1B[31mx\x1B[0m ${msg}`);
|
|
146
|
+
}
|
|
147
|
+
function mask(token) {
|
|
148
|
+
if (!token || token.length < 12) return "****";
|
|
149
|
+
return token.slice(0, 8) + "..." + token.slice(-4);
|
|
150
|
+
}
|
|
151
|
+
function createPrompt() {
|
|
152
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
153
|
+
function ask(question) {
|
|
154
|
+
return new Promise((resolve5) => rl.question(question, resolve5));
|
|
155
|
+
}
|
|
156
|
+
async function promptToken(name, prefix) {
|
|
157
|
+
while (true) {
|
|
158
|
+
const value = (await ask(` ${name}: `)).trim();
|
|
159
|
+
if (!value) {
|
|
160
|
+
warn("Value is required.");
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (!value.startsWith(prefix)) {
|
|
164
|
+
warn(`Expected value starting with "${prefix}".`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function promptRequired(name) {
|
|
171
|
+
while (true) {
|
|
172
|
+
const value = (await ask(` ${name}: `)).trim();
|
|
173
|
+
if (value) return value;
|
|
174
|
+
warn("Value is required.");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function promptOptional(label, defaultValue = "") {
|
|
178
|
+
const defaultStr = defaultValue ? ` (default: ${defaultValue})` : " (leave blank to skip)";
|
|
179
|
+
const value = (await ask(` ${label}${defaultStr}: `)).trim();
|
|
180
|
+
return value || defaultValue;
|
|
181
|
+
}
|
|
182
|
+
return { rl, ask, promptToken, promptRequired, promptOptional };
|
|
183
|
+
}
|
|
184
|
+
async function manualSetup() {
|
|
185
|
+
const { rl, ask, promptToken, promptRequired, promptOptional } = createPrompt();
|
|
186
|
+
try {
|
|
187
|
+
if (existsSync3(paths.CONFIG_FILE)) {
|
|
188
|
+
const answer = await ask(
|
|
189
|
+
`
|
|
190
|
+
Config already exists at ${paths.CONFIG_FILE}. Reconfigure? (y/N) `
|
|
191
|
+
);
|
|
192
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
193
|
+
success("Keeping existing config.");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
log("Let's configure your Slack app tokens.");
|
|
198
|
+
console.log(`
|
|
199
|
+
1. Go to https://api.slack.com/apps and create (or select) your app.
|
|
200
|
+
Tip: Use the manifest file for quick setup \u2014 see slack-app-manifest.yml
|
|
201
|
+
2. Under "OAuth & Permissions", install the app to your workspace.
|
|
202
|
+
Copy the \x1B[1mBot User OAuth Token\x1B[0m (starts with xoxb-).
|
|
203
|
+
3. Under "Basic Information > App-Level Tokens", create a token with
|
|
204
|
+
the \x1B[1mconnections:write\x1B[0m scope. Copy the token (starts with xapp-).
|
|
205
|
+
4. Under "Basic Information", copy the \x1B[1mSigning Secret\x1B[0m.
|
|
206
|
+
`);
|
|
207
|
+
const botToken = await promptToken("SLACK_BOT_TOKEN", "xoxb-");
|
|
208
|
+
const appToken = await promptToken("SLACK_APP_TOKEN", "xapp-");
|
|
209
|
+
const signingSecret = await promptRequired("SLACK_SIGNING_SECRET");
|
|
210
|
+
log("A few more settings to configure...\n");
|
|
211
|
+
const claudeCwd = await promptOptional(
|
|
212
|
+
"CLAUDE_CWD -- working directory for Claude sessions",
|
|
213
|
+
process.cwd()
|
|
214
|
+
);
|
|
215
|
+
const claudeConfigDir = await promptOptional(
|
|
216
|
+
"CLAUDE_CONFIG_DIR -- Claude config directory (e.g. ~/.claude-custie)"
|
|
217
|
+
);
|
|
218
|
+
const botName = await promptOptional("BOT_NAME -- display name in system prompt", "Custie");
|
|
219
|
+
console.log(
|
|
220
|
+
`
|
|
221
|
+
\x1B[2mTip: Find your Slack user ID by clicking your profile > ... > Copy member ID\x1B[0m`
|
|
222
|
+
);
|
|
223
|
+
const ownerUserId = await promptOptional(
|
|
224
|
+
"OWNER_USER_ID -- your Slack user ID for mention monitoring"
|
|
225
|
+
);
|
|
226
|
+
const defaultAllowed = ownerUserId || "";
|
|
227
|
+
const allowedUserIds = await promptOptional(
|
|
228
|
+
"ALLOWED_USER_IDS -- comma-separated user IDs",
|
|
229
|
+
defaultAllowed
|
|
230
|
+
);
|
|
231
|
+
if (!allowedUserIds) {
|
|
232
|
+
warn(
|
|
233
|
+
"No ALLOWED_USER_IDS set -- the bot runs with --dangerously-skip-permissions, so anyone in the workspace can execute commands. Consider restricting access."
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
writeConfigFile({
|
|
237
|
+
botToken,
|
|
238
|
+
appToken,
|
|
239
|
+
signingSecret,
|
|
240
|
+
claudeCwd,
|
|
241
|
+
claudeConfigDir,
|
|
242
|
+
botName,
|
|
243
|
+
ownerUserId,
|
|
244
|
+
allowedUserIds
|
|
245
|
+
});
|
|
246
|
+
copyDefaultPrompt();
|
|
247
|
+
printNextSteps();
|
|
248
|
+
} finally {
|
|
249
|
+
rl.close();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function browserSetup() {
|
|
253
|
+
const { chromium } = await import("playwright");
|
|
254
|
+
const MANIFEST_PATH = resolve3(paths.PACKAGE_ROOT, "slack-app-manifest.yml");
|
|
255
|
+
if (!existsSync3(MANIFEST_PATH)) {
|
|
256
|
+
error(`Manifest not found: ${MANIFEST_PATH}`);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
const manifestYaml = readFileSync2(MANIFEST_PATH, "utf-8");
|
|
260
|
+
success("Manifest loaded from slack-app-manifest.yml");
|
|
261
|
+
const DEBUG = process.argv.includes("--debug");
|
|
262
|
+
const SLOW_MO = DEBUG ? 200 : 50;
|
|
263
|
+
const USER_DATA_DIR = resolve3(tmpdir(), "custie-playwright-profile");
|
|
264
|
+
const { rl, ask, promptOptional } = createPrompt();
|
|
265
|
+
let context;
|
|
266
|
+
try {
|
|
267
|
+
log("Launching browser...");
|
|
268
|
+
context = await chromium.launchPersistentContext(USER_DATA_DIR, {
|
|
269
|
+
headless: false,
|
|
270
|
+
slowMo: SLOW_MO,
|
|
271
|
+
viewport: { width: 1280, height: 900 },
|
|
272
|
+
args: ["--disable-blink-features=AutomationControlled"]
|
|
273
|
+
});
|
|
274
|
+
const page = context.pages()[0] || await context.newPage();
|
|
275
|
+
log("Opening Slack API dashboard...");
|
|
276
|
+
await page.goto("https://api.slack.com/apps");
|
|
277
|
+
const createBtn = page.locator(
|
|
278
|
+
'a:has-text("Create New App"), button:has-text("Create New App")'
|
|
279
|
+
);
|
|
280
|
+
const isLoggedIn = await createBtn.first().isVisible({ timeout: 3e3 }).catch(() => false);
|
|
281
|
+
if (!isLoggedIn) {
|
|
282
|
+
log("Please log in to Slack in the browser window. Waiting up to 5 minutes...");
|
|
283
|
+
await createBtn.first().waitFor({ state: "visible", timeout: 3e5 });
|
|
284
|
+
}
|
|
285
|
+
success("Authenticated with Slack.");
|
|
286
|
+
log("Creating new Slack App from manifest...");
|
|
287
|
+
await createBtn.first().click();
|
|
288
|
+
await page.waitForTimeout(1e3);
|
|
289
|
+
await page.locator(
|
|
290
|
+
'button:has-text("From an app manifest"), a:has-text("From an app manifest")'
|
|
291
|
+
).first().click();
|
|
292
|
+
await page.waitForTimeout(1e3);
|
|
293
|
+
const nextBtn = page.locator('button:has-text("Next")');
|
|
294
|
+
await nextBtn.first().waitFor({ state: "visible", timeout: 1e4 });
|
|
295
|
+
await page.waitForTimeout(500);
|
|
296
|
+
await nextBtn.first().click();
|
|
297
|
+
await page.waitForTimeout(1500);
|
|
298
|
+
const yamlTab = page.locator('button:has-text("YAML"), [role="tab"]:has-text("YAML")');
|
|
299
|
+
if (await yamlTab.first().isVisible({ timeout: 3e3 }).catch(() => false)) {
|
|
300
|
+
await yamlTab.first().click();
|
|
301
|
+
await page.waitForTimeout(500);
|
|
302
|
+
}
|
|
303
|
+
const textarea = page.locator("textarea").first();
|
|
304
|
+
const hasTextarea = await textarea.isVisible({ timeout: 2e3 }).catch(() => false);
|
|
305
|
+
if (hasTextarea) {
|
|
306
|
+
await textarea.click();
|
|
307
|
+
await page.keyboard.press("Meta+A");
|
|
308
|
+
await page.keyboard.press("Backspace");
|
|
309
|
+
await page.waitForTimeout(200);
|
|
310
|
+
await textarea.fill(manifestYaml);
|
|
311
|
+
} else {
|
|
312
|
+
const editor = page.locator('.CodeMirror, [role="textbox"], [contenteditable="true"]').first();
|
|
313
|
+
await editor.click();
|
|
314
|
+
await page.keyboard.press("Meta+A");
|
|
315
|
+
await page.keyboard.press("Backspace");
|
|
316
|
+
await page.waitForTimeout(200);
|
|
317
|
+
await page.keyboard.insertText(manifestYaml);
|
|
318
|
+
}
|
|
319
|
+
await page.waitForTimeout(500);
|
|
320
|
+
await nextBtn.first().waitFor({ state: "visible", timeout: 1e4 });
|
|
321
|
+
await nextBtn.first().click();
|
|
322
|
+
await page.waitForTimeout(1500);
|
|
323
|
+
const createAppBtn = page.locator('button:has-text("Create")');
|
|
324
|
+
await createAppBtn.first().waitFor({ state: "visible", timeout: 1e4 });
|
|
325
|
+
await createAppBtn.first().click();
|
|
326
|
+
await page.waitForURL("**/apps/A*", { timeout: 3e4 });
|
|
327
|
+
const appUrl = page.url();
|
|
328
|
+
const appId = appUrl.match(/\/apps\/(A[A-Z0-9]+)/)?.[1];
|
|
329
|
+
success(`App created! ID: ${appId}`);
|
|
330
|
+
log("Extracting Signing Secret...");
|
|
331
|
+
await page.waitForSelector("text=Signing Secret", { timeout: 1e4 });
|
|
332
|
+
const signingSection = page.locator("text=Signing Secret").locator("..");
|
|
333
|
+
const showBtn = signingSection.locator('button:has-text("Show"), a:has-text("Show")');
|
|
334
|
+
if (await showBtn.first().isVisible({ timeout: 3e3 }).catch(() => false)) {
|
|
335
|
+
await showBtn.first().click();
|
|
336
|
+
await page.waitForTimeout(500);
|
|
337
|
+
}
|
|
338
|
+
let signingSecret = await signingSection.locator("input").first().inputValue().catch(() => "");
|
|
339
|
+
if (!signingSecret) {
|
|
340
|
+
signingSecret = await page.evaluate(() => {
|
|
341
|
+
const labels = [...document.querySelectorAll("*")].filter(
|
|
342
|
+
(el) => el.textContent?.trim() === "Signing Secret"
|
|
343
|
+
);
|
|
344
|
+
for (const label of labels) {
|
|
345
|
+
const container = label.closest("div");
|
|
346
|
+
if (!container) continue;
|
|
347
|
+
const input = container.querySelector("input");
|
|
348
|
+
if (input?.value) return input.value;
|
|
349
|
+
}
|
|
350
|
+
return "";
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (!signingSecret) {
|
|
354
|
+
warn("Could not auto-extract Signing Secret. Please copy it from the browser.");
|
|
355
|
+
signingSecret = await ask(" Paste your Signing Secret here: ");
|
|
356
|
+
}
|
|
357
|
+
success(`Signing Secret: ${mask(signingSecret)}`);
|
|
358
|
+
log("Generating App-Level Token (Socket Mode)...");
|
|
359
|
+
const tokenSection = page.locator("text=App-Level Tokens");
|
|
360
|
+
await tokenSection.first().scrollIntoViewIfNeeded();
|
|
361
|
+
await page.waitForTimeout(500);
|
|
362
|
+
const generateBtn = page.locator(
|
|
363
|
+
'button:has-text("Generate Token"), a:has-text("Generate Token")'
|
|
364
|
+
);
|
|
365
|
+
await generateBtn.first().click();
|
|
366
|
+
await page.waitForTimeout(1e3);
|
|
367
|
+
const nameInput = page.locator(
|
|
368
|
+
'input[placeholder*="oken"], input[placeholder*="name"], input[name*="token"]'
|
|
369
|
+
);
|
|
370
|
+
await nameInput.first().waitFor({ state: "visible", timeout: 1e4 });
|
|
371
|
+
await nameInput.first().fill("socket-mode");
|
|
372
|
+
await page.waitForTimeout(500);
|
|
373
|
+
const addScopeBtn = page.locator(
|
|
374
|
+
'button:has-text("Add Scope"), a:has-text("Add Scope")'
|
|
375
|
+
);
|
|
376
|
+
await addScopeBtn.first().click();
|
|
377
|
+
await page.waitForTimeout(500);
|
|
378
|
+
await page.locator("text=connections:write").first().click();
|
|
379
|
+
await page.waitForTimeout(500);
|
|
380
|
+
const genBtn = page.locator('button:has-text("Generate")');
|
|
381
|
+
await genBtn.first().click();
|
|
382
|
+
await page.waitForTimeout(2e3);
|
|
383
|
+
let appToken = await page.evaluate(() => {
|
|
384
|
+
const els = [...document.querySelectorAll("input, code, span, div")];
|
|
385
|
+
for (const el of els) {
|
|
386
|
+
const val = el.value || el.textContent || "";
|
|
387
|
+
if (val.trim().startsWith("xapp-")) return val.trim();
|
|
388
|
+
}
|
|
389
|
+
return "";
|
|
390
|
+
});
|
|
391
|
+
if (!appToken) {
|
|
392
|
+
warn("Could not auto-extract App-Level Token. Please copy it from the browser.");
|
|
393
|
+
appToken = await ask(" Paste your App-Level Token (xapp-...): ");
|
|
394
|
+
}
|
|
395
|
+
const doneBtn = page.locator('button:has-text("Done")');
|
|
396
|
+
if (await doneBtn.first().isVisible({ timeout: 2e3 }).catch(() => false)) {
|
|
397
|
+
await doneBtn.first().click();
|
|
398
|
+
await page.waitForTimeout(500);
|
|
399
|
+
}
|
|
400
|
+
success(`App-Level Token: ${mask(appToken)}`);
|
|
401
|
+
log("Installing app to workspace...");
|
|
402
|
+
const installLink = page.locator('a:has-text("Install App")');
|
|
403
|
+
if (await installLink.first().isVisible({ timeout: 3e3 }).catch(() => false)) {
|
|
404
|
+
await installLink.first().click();
|
|
405
|
+
} else {
|
|
406
|
+
await page.goto(`https://api.slack.com/apps/${appId}/install-on-team`);
|
|
407
|
+
}
|
|
408
|
+
await page.waitForTimeout(1500);
|
|
409
|
+
const installBtn = page.locator(
|
|
410
|
+
'button:has-text("Install to Workspace"), a:has-text("Install to Workspace")'
|
|
411
|
+
);
|
|
412
|
+
await installBtn.first().waitFor({ state: "visible", timeout: 1e4 });
|
|
413
|
+
await installBtn.first().click();
|
|
414
|
+
await page.waitForTimeout(2e3);
|
|
415
|
+
const allowBtn = page.locator('button:has-text("Allow")');
|
|
416
|
+
if (await allowBtn.first().isVisible({ timeout: 5e3 }).catch(() => false)) {
|
|
417
|
+
await allowBtn.first().click();
|
|
418
|
+
await page.waitForTimeout(2e3);
|
|
419
|
+
}
|
|
420
|
+
await page.waitForSelector("text=Bot User OAuth Token", { timeout: 15e3 });
|
|
421
|
+
let botToken = await page.evaluate(() => {
|
|
422
|
+
const els = [...document.querySelectorAll("input")];
|
|
423
|
+
for (const el of els) {
|
|
424
|
+
if (el.value?.startsWith("xoxb-")) return el.value;
|
|
425
|
+
}
|
|
426
|
+
return "";
|
|
427
|
+
});
|
|
428
|
+
if (!botToken) {
|
|
429
|
+
const showBotBtn = page.locator('button:has-text("Show"), a:has-text("Show")').first();
|
|
430
|
+
if (await showBotBtn.isVisible({ timeout: 2e3 }).catch(() => false)) {
|
|
431
|
+
await showBotBtn.click();
|
|
432
|
+
await page.waitForTimeout(500);
|
|
433
|
+
botToken = await page.evaluate(() => {
|
|
434
|
+
const els = [...document.querySelectorAll("input")];
|
|
435
|
+
for (const el of els) {
|
|
436
|
+
if (el.value?.startsWith("xoxb-")) return el.value;
|
|
437
|
+
}
|
|
438
|
+
return "";
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (!botToken) {
|
|
443
|
+
warn("Could not auto-extract Bot Token. Please copy it from the browser.");
|
|
444
|
+
botToken = await ask(" Paste your Bot User OAuth Token (xoxb-...): ");
|
|
445
|
+
}
|
|
446
|
+
success(`Bot Token: ${mask(botToken)}`);
|
|
447
|
+
log("A few more settings to configure...\n");
|
|
448
|
+
const claudeCwd = await promptOptional(
|
|
449
|
+
"CLAUDE_CWD -- working directory for Claude sessions",
|
|
450
|
+
process.cwd()
|
|
451
|
+
);
|
|
452
|
+
const claudeConfigDir = await promptOptional(
|
|
453
|
+
"CLAUDE_CONFIG_DIR -- Claude config directory (e.g. ~/.claude-custie)"
|
|
454
|
+
);
|
|
455
|
+
const botName = await promptOptional(
|
|
456
|
+
"BOT_NAME -- display name in system prompt",
|
|
457
|
+
"Custie"
|
|
458
|
+
);
|
|
459
|
+
console.log(
|
|
460
|
+
`
|
|
461
|
+
\x1B[2mTip: Find your Slack user ID by clicking your profile > ... > Copy member ID\x1B[0m`
|
|
462
|
+
);
|
|
463
|
+
const ownerUserId = await promptOptional(
|
|
464
|
+
"OWNER_USER_ID -- your Slack user ID for mention monitoring"
|
|
465
|
+
);
|
|
466
|
+
const defaultAllowed = ownerUserId || "";
|
|
467
|
+
const allowedUserIds = await promptOptional(
|
|
468
|
+
"ALLOWED_USER_IDS -- comma-separated user IDs",
|
|
469
|
+
defaultAllowed
|
|
470
|
+
);
|
|
471
|
+
if (!allowedUserIds) {
|
|
472
|
+
warn(
|
|
473
|
+
"No ALLOWED_USER_IDS set -- the bot runs with --dangerously-skip-permissions, so anyone in the workspace can execute commands. Consider restricting access."
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
writeConfigFile({
|
|
477
|
+
botToken: botToken.trim(),
|
|
478
|
+
appToken: appToken.trim(),
|
|
479
|
+
signingSecret: signingSecret.trim(),
|
|
480
|
+
claudeCwd,
|
|
481
|
+
claudeConfigDir,
|
|
482
|
+
botName,
|
|
483
|
+
ownerUserId,
|
|
484
|
+
allowedUserIds
|
|
485
|
+
});
|
|
486
|
+
copyDefaultPrompt();
|
|
487
|
+
printNextSteps();
|
|
488
|
+
if (!DEBUG) {
|
|
489
|
+
await context.close();
|
|
490
|
+
} else {
|
|
491
|
+
log("Debug mode: browser stays open. Press Ctrl+C to exit.");
|
|
492
|
+
await new Promise(() => {
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
} catch (err) {
|
|
496
|
+
error(`Setup failed: ${err.message}`);
|
|
497
|
+
if (process.argv.includes("--debug")) console.error(err);
|
|
498
|
+
try {
|
|
499
|
+
const pages = context?.pages();
|
|
500
|
+
if (pages?.[0]) {
|
|
501
|
+
const screenshotPath = resolve3(paths.PACKAGE_ROOT, "setup-error.png");
|
|
502
|
+
await pages[0].screenshot({ path: screenshotPath, fullPage: true });
|
|
503
|
+
warn(`Screenshot saved to: ${screenshotPath}`);
|
|
504
|
+
}
|
|
505
|
+
} catch {
|
|
506
|
+
}
|
|
507
|
+
warn("Browser left open for manual inspection. Press Ctrl+C to exit.");
|
|
508
|
+
await new Promise(() => {
|
|
509
|
+
});
|
|
510
|
+
} finally {
|
|
511
|
+
rl.close();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function writeConfigFile(config) {
|
|
515
|
+
const envContent = [
|
|
516
|
+
`# Slack Bot Token (xoxb-...)`,
|
|
517
|
+
`SLACK_BOT_TOKEN=${config.botToken}`,
|
|
518
|
+
``,
|
|
519
|
+
`# Slack App-Level Token for Socket Mode (xapp-...)`,
|
|
520
|
+
`SLACK_APP_TOKEN=${config.appToken}`,
|
|
521
|
+
``,
|
|
522
|
+
`# Slack Signing Secret`,
|
|
523
|
+
`SLACK_SIGNING_SECRET=${config.signingSecret}`,
|
|
524
|
+
``,
|
|
525
|
+
`# Default working directory for Claude sessions (optional)`,
|
|
526
|
+
`CLAUDE_CWD=${config.claudeCwd}`,
|
|
527
|
+
``,
|
|
528
|
+
`# Claude config directory for session storage (optional, e.g. ~/.claude-custie)`,
|
|
529
|
+
`CLAUDE_CONFIG_DIR=${config.claudeConfigDir}`,
|
|
530
|
+
``,
|
|
531
|
+
`# Bot display name used in system prompt (default: Custie)`,
|
|
532
|
+
`BOT_NAME=${config.botName}`,
|
|
533
|
+
``,
|
|
534
|
+
`# Comma-separated Slack user IDs allowed to interact (empty = everyone)`,
|
|
535
|
+
`# If set, the owner is automatically included -- no need to duplicate`,
|
|
536
|
+
`ALLOWED_USER_IDS=${config.allowedUserIds}`,
|
|
537
|
+
``,
|
|
538
|
+
`# Owner's Slack user ID -- bot reacts with eyes when owner is mentioned`,
|
|
539
|
+
`# Does not restrict access on its own; only ALLOWED_USER_IDS controls who can use the bot`,
|
|
540
|
+
`OWNER_USER_ID=${config.ownerUserId}`,
|
|
541
|
+
``
|
|
542
|
+
].join("\n");
|
|
543
|
+
writeFileSync2(paths.CONFIG_FILE, envContent);
|
|
544
|
+
success(`Config written to ${paths.CONFIG_FILE}`);
|
|
545
|
+
}
|
|
546
|
+
function printNextSteps() {
|
|
547
|
+
log("Setup complete! Next steps:\n");
|
|
548
|
+
console.log(" 1. Set your bot avatar:");
|
|
549
|
+
console.log(" Go to https://api.slack.com/apps \u2192 your app \u2192 Basic Information");
|
|
550
|
+
console.log(' Scroll to "Display Information" and upload an app icon.\n');
|
|
551
|
+
console.log(" 2. Customise the system prompt (optional):");
|
|
552
|
+
console.log(` Run \x1B[1mcustie prompt\x1B[0m to edit ${paths.PROMPT_FILE}
|
|
553
|
+
`);
|
|
554
|
+
console.log(" 3. Start the bot:");
|
|
555
|
+
console.log(" Run \x1B[1mcustie start\x1B[0m\n");
|
|
556
|
+
console.log(" 4. Install as a background service (optional):");
|
|
557
|
+
console.log(" Run \x1B[1mcustie install\x1B[0m\n");
|
|
558
|
+
}
|
|
559
|
+
function copyDefaultPrompt() {
|
|
560
|
+
if (!existsSync3(paths.PROMPT_FILE)) {
|
|
561
|
+
const defaultPrompt = resolve3(paths.PACKAGE_ROOT, "system.default.md");
|
|
562
|
+
if (existsSync3(defaultPrompt)) {
|
|
563
|
+
copyFileSync2(defaultPrompt, paths.PROMPT_FILE);
|
|
564
|
+
success(`Default prompt copied to ${paths.PROMPT_FILE}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async function runSetup(args) {
|
|
569
|
+
console.log("\n\x1B[1mCustie Setup\x1B[0m\n");
|
|
570
|
+
ensureDirs();
|
|
571
|
+
if (args.includes("--manual")) {
|
|
572
|
+
await manualSetup();
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
await import("playwright");
|
|
577
|
+
await browserSetup();
|
|
578
|
+
} catch {
|
|
579
|
+
warn(
|
|
580
|
+
"Playwright not installed -- falling back to manual setup.\n For automated browser setup, run:\n pnpm add -D playwright && pnpx playwright install chromium\n"
|
|
581
|
+
);
|
|
582
|
+
await manualSetup();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/commands/install.ts
|
|
587
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync } from "fs";
|
|
588
|
+
import { execSync } from "child_process";
|
|
589
|
+
import { dirname, join, resolve as resolve4 } from "path";
|
|
590
|
+
import { homedir, platform } from "os";
|
|
591
|
+
var PLIST_NAME = "io.flycoder.custie.plist";
|
|
592
|
+
var SYSTEMD_SERVICE = "custie.service";
|
|
593
|
+
function log2(msg) {
|
|
594
|
+
console.log(`
|
|
595
|
+
\x1B[36m>\x1B[0m ${msg}`);
|
|
596
|
+
}
|
|
597
|
+
function success2(msg) {
|
|
598
|
+
console.log(`\x1B[32m+\x1B[0m ${msg}`);
|
|
599
|
+
}
|
|
600
|
+
function warn2(msg) {
|
|
601
|
+
console.log(`\x1B[33m!\x1B[0m ${msg}`);
|
|
602
|
+
}
|
|
603
|
+
function run(cmd, opts = {}) {
|
|
604
|
+
return execSync(cmd, {
|
|
605
|
+
encoding: "utf-8",
|
|
606
|
+
stdio: opts.silent ? "pipe" : "inherit"
|
|
607
|
+
}).toString().trim();
|
|
608
|
+
}
|
|
609
|
+
function detectCustieBin() {
|
|
610
|
+
const argv1 = process.argv[1];
|
|
611
|
+
if (argv1) {
|
|
612
|
+
return resolve4(argv1);
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
return run("which custie", { silent: true });
|
|
616
|
+
} catch {
|
|
617
|
+
return "custie";
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function readEnvFile() {
|
|
621
|
+
if (!existsSync4(paths.CONFIG_FILE)) return {};
|
|
622
|
+
const vars = {};
|
|
623
|
+
for (const line of readFileSync3(paths.CONFIG_FILE, "utf-8").split("\n")) {
|
|
624
|
+
const trimmed = line.trim();
|
|
625
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
626
|
+
const eq = trimmed.indexOf("=");
|
|
627
|
+
if (eq === -1) continue;
|
|
628
|
+
const key = trimmed.slice(0, eq).trim();
|
|
629
|
+
const val = trimmed.slice(eq + 1).trim();
|
|
630
|
+
if (val) vars[key] = val;
|
|
631
|
+
}
|
|
632
|
+
return vars;
|
|
633
|
+
}
|
|
634
|
+
function escapeXml(str) {
|
|
635
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
636
|
+
}
|
|
637
|
+
async function installMacOS() {
|
|
638
|
+
log2("Setting up macOS LaunchAgent...");
|
|
639
|
+
const custieBin = detectCustieBin();
|
|
640
|
+
const nodePath = process.execPath;
|
|
641
|
+
const logDir = paths.LOG_DIR;
|
|
642
|
+
mkdirSync(logDir, { recursive: true });
|
|
643
|
+
const envVars = readEnvFile();
|
|
644
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
645
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
646
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
647
|
+
<plist version="1.0">
|
|
648
|
+
<dict>
|
|
649
|
+
<key>Label</key>
|
|
650
|
+
<string>io.flycoder.custie</string>
|
|
651
|
+
|
|
652
|
+
<key>ProgramArguments</key>
|
|
653
|
+
<array>
|
|
654
|
+
<string>${custieBin}</string>
|
|
655
|
+
<string>start</string>
|
|
656
|
+
</array>
|
|
657
|
+
|
|
658
|
+
<key>EnvironmentVariables</key>
|
|
659
|
+
<dict>
|
|
660
|
+
<key>PATH</key>
|
|
661
|
+
<string>${dirname(nodePath)}:/usr/local/bin:/usr/bin:/bin</string>
|
|
662
|
+
${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>
|
|
663
|
+
<string>${escapeXml(v)}</string>`).join("\n")}
|
|
664
|
+
</dict>
|
|
665
|
+
|
|
666
|
+
<key>RunAtLoad</key>
|
|
667
|
+
<true/>
|
|
668
|
+
|
|
669
|
+
<key>KeepAlive</key>
|
|
670
|
+
<true/>
|
|
671
|
+
|
|
672
|
+
<key>StandardOutPath</key>
|
|
673
|
+
<string>${join(logDir, "custie.log")}</string>
|
|
674
|
+
|
|
675
|
+
<key>StandardErrorPath</key>
|
|
676
|
+
<string>${join(logDir, "custie-error.log")}</string>
|
|
677
|
+
</dict>
|
|
678
|
+
</plist>`;
|
|
679
|
+
const agentDir = join(homedir(), "Library", "LaunchAgents");
|
|
680
|
+
mkdirSync(agentDir, { recursive: true });
|
|
681
|
+
const plistPath = join(agentDir, PLIST_NAME);
|
|
682
|
+
try {
|
|
683
|
+
run(`launchctl unload "${plistPath}"`, { silent: true });
|
|
684
|
+
} catch {
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
unlinkSync(plistPath);
|
|
688
|
+
} catch {
|
|
689
|
+
}
|
|
690
|
+
writeFileSync3(plistPath, plist);
|
|
691
|
+
run(`launchctl load "${plistPath}"`, { silent: true });
|
|
692
|
+
success2("LaunchAgent installed and loaded.");
|
|
693
|
+
console.log(` Logs: ${logDir}`);
|
|
694
|
+
console.log(` Check: launchctl list | grep custie`);
|
|
695
|
+
}
|
|
696
|
+
async function installLinux() {
|
|
697
|
+
log2("Setting up systemd user service...");
|
|
698
|
+
const custieBin = detectCustieBin();
|
|
699
|
+
const nodePath = process.execPath;
|
|
700
|
+
const logDir = paths.LOG_DIR;
|
|
701
|
+
mkdirSync(logDir, { recursive: true });
|
|
702
|
+
const envVars = readEnvFile();
|
|
703
|
+
const envLines = Object.entries(envVars).map(([k, v]) => `Environment="${k}=${v}"`).join("\n");
|
|
704
|
+
const unit = `[Unit]
|
|
705
|
+
Description=Custie Slack Bot
|
|
706
|
+
After=network-online.target
|
|
707
|
+
Wants=network-online.target
|
|
708
|
+
|
|
709
|
+
[Service]
|
|
710
|
+
Type=simple
|
|
711
|
+
ExecStart=${custieBin} start
|
|
712
|
+
Restart=on-failure
|
|
713
|
+
RestartSec=5
|
|
714
|
+
${envLines}
|
|
715
|
+
Environment="PATH=${dirname(nodePath)}:/usr/local/bin:/usr/bin:/bin"
|
|
716
|
+
|
|
717
|
+
StandardOutput=append:${join(logDir, "custie.log")}
|
|
718
|
+
StandardError=append:${join(logDir, "custie-error.log")}
|
|
719
|
+
|
|
720
|
+
[Install]
|
|
721
|
+
WantedBy=default.target
|
|
722
|
+
`;
|
|
723
|
+
const unitDir = join(homedir(), ".config", "systemd", "user");
|
|
724
|
+
mkdirSync(unitDir, { recursive: true });
|
|
725
|
+
writeFileSync3(join(unitDir, SYSTEMD_SERVICE), unit);
|
|
726
|
+
run("systemctl --user daemon-reload", { silent: true });
|
|
727
|
+
run("systemctl --user enable --now custie", { silent: true });
|
|
728
|
+
success2("systemd user service installed and started.");
|
|
729
|
+
console.log(` Logs: ${logDir}`);
|
|
730
|
+
console.log(` Check: systemctl --user status custie`);
|
|
731
|
+
}
|
|
732
|
+
async function runInstall() {
|
|
733
|
+
ensureDirs();
|
|
734
|
+
const os = platform();
|
|
735
|
+
if (os === "darwin") {
|
|
736
|
+
await installMacOS();
|
|
737
|
+
} else if (os === "linux") {
|
|
738
|
+
await installLinux();
|
|
739
|
+
} else {
|
|
740
|
+
warn2(`Unsupported platform: ${os}. You'll need to configure autostart manually.`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/commands/uninstall.ts
|
|
745
|
+
import { unlinkSync as unlinkSync2 } from "fs";
|
|
746
|
+
import { execSync as execSync2 } from "child_process";
|
|
747
|
+
import { join as join2 } from "path";
|
|
748
|
+
import { homedir as homedir2, platform as platform2 } from "os";
|
|
749
|
+
var PLIST_NAME2 = "io.flycoder.custie.plist";
|
|
750
|
+
var SYSTEMD_SERVICE2 = "custie.service";
|
|
751
|
+
function log3(msg) {
|
|
752
|
+
console.log(`
|
|
753
|
+
\x1B[36m>\x1B[0m ${msg}`);
|
|
754
|
+
}
|
|
755
|
+
function success3(msg) {
|
|
756
|
+
console.log(`\x1B[32m+\x1B[0m ${msg}`);
|
|
757
|
+
}
|
|
758
|
+
function warn3(msg) {
|
|
759
|
+
console.log(`\x1B[33m!\x1B[0m ${msg}`);
|
|
760
|
+
}
|
|
761
|
+
function run2(cmd) {
|
|
762
|
+
execSync2(cmd, { encoding: "utf-8", stdio: "pipe" });
|
|
763
|
+
}
|
|
764
|
+
async function runUninstall() {
|
|
765
|
+
const os = platform2();
|
|
766
|
+
log3("Uninstalling Custie service...");
|
|
767
|
+
if (os === "darwin") {
|
|
768
|
+
const plistPath = join2(homedir2(), "Library", "LaunchAgents", PLIST_NAME2);
|
|
769
|
+
try {
|
|
770
|
+
run2(`launchctl unload "${plistPath}"`);
|
|
771
|
+
} catch {
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
unlinkSync2(plistPath);
|
|
775
|
+
} catch {
|
|
776
|
+
}
|
|
777
|
+
success3("LaunchAgent removed.");
|
|
778
|
+
} else if (os === "linux") {
|
|
779
|
+
try {
|
|
780
|
+
run2("systemctl --user disable --now custie");
|
|
781
|
+
} catch {
|
|
782
|
+
}
|
|
783
|
+
const unitPath = join2(homedir2(), ".config", "systemd", "user", SYSTEMD_SERVICE2);
|
|
784
|
+
try {
|
|
785
|
+
unlinkSync2(unitPath);
|
|
786
|
+
} catch {
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
run2("systemctl --user daemon-reload");
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
792
|
+
success3("systemd user service removed.");
|
|
793
|
+
} else {
|
|
794
|
+
warn3(`Unsupported platform: ${os}`);
|
|
795
|
+
}
|
|
796
|
+
console.log("\n Config and data directories were NOT removed.");
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/cli.ts
|
|
800
|
+
var USAGE = `
|
|
801
|
+
Usage: custie <command> [options]
|
|
802
|
+
|
|
803
|
+
Commands:
|
|
804
|
+
start Start the Slack bot server
|
|
805
|
+
setup Interactive first-time setup (--browser for Playwright automation)
|
|
806
|
+
install Install as a system service (launchd / systemd)
|
|
807
|
+
uninstall Remove the system service
|
|
808
|
+
prompt Edit the system prompt in $EDITOR
|
|
809
|
+
config Show resolved config (--edit to edit, --path for file path)
|
|
810
|
+
|
|
811
|
+
Options:
|
|
812
|
+
-h, --help Show this help message
|
|
813
|
+
`;
|
|
814
|
+
async function main() {
|
|
815
|
+
const args = process.argv.slice(2);
|
|
816
|
+
const command = args[0];
|
|
817
|
+
switch (command) {
|
|
818
|
+
case "start":
|
|
819
|
+
await runStart();
|
|
820
|
+
break;
|
|
821
|
+
case "setup":
|
|
822
|
+
await runSetup(args.slice(1));
|
|
823
|
+
break;
|
|
824
|
+
case "install":
|
|
825
|
+
await runInstall();
|
|
826
|
+
break;
|
|
827
|
+
case "uninstall":
|
|
828
|
+
await runUninstall();
|
|
829
|
+
break;
|
|
830
|
+
case "prompt":
|
|
831
|
+
await runPrompt();
|
|
832
|
+
break;
|
|
833
|
+
case "config":
|
|
834
|
+
await runConfig(args.slice(1));
|
|
835
|
+
break;
|
|
836
|
+
case "-h":
|
|
837
|
+
case "--help":
|
|
838
|
+
case void 0:
|
|
839
|
+
console.log(USAGE);
|
|
840
|
+
break;
|
|
841
|
+
default:
|
|
842
|
+
console.error(`Unknown command: ${command}`);
|
|
843
|
+
console.log(USAGE);
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
main().catch((err) => {
|
|
848
|
+
console.error("[custie] Fatal error:", err);
|
|
849
|
+
process.exit(1);
|
|
850
|
+
});
|