kelocan 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.
@@ -0,0 +1,278 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { hostname } from "node:os";
3
+ import { request } from "./api.js";
4
+ import { flagStr } from "./args.js";
5
+ import { clearConfig, readConfig, writeConfig } from "./config.js";
6
+ import { usageError } from "./errors.js";
7
+ import { browserLogin, saveLogin } from "./login.js";
8
+ import { emit, line, outputOpts } from "./output.js";
9
+ import { isUuid, resolveBoard, resolveColumn, resolveLabels } from "./resolve.js";
10
+ const PRIORITIES = ["low", "medium", "high", "urgent"];
11
+ const KINDS = ["task", "idea", "feedback"];
12
+ const OUTCOMES = ["succeeded", "failed", "cancelled"];
13
+ // ---------- auth ----------
14
+ export async function cmdLogin(args) {
15
+ const url = flagStr(args.flags, "url");
16
+ const webUrl = flagStr(args.flags, "web-url");
17
+ const inlineKey = flagStr(args.flags, "api-key");
18
+ if (inlineKey) {
19
+ const user = await saveLogin({ url, webUrl, apiKey: inlineKey });
20
+ line(`Logged in as ${user.name} <${user.email ?? ""}>`);
21
+ return;
22
+ }
23
+ const name = flagStr(args.flags, "name") ?? hostname();
24
+ const { key } = await browserLogin(name);
25
+ const user = await saveLogin({ url, webUrl, apiKey: key });
26
+ line(`Logged in as ${user.name} <${user.email ?? ""}>`);
27
+ }
28
+ export async function cmdLogout() {
29
+ const config = readConfig();
30
+ if (!config.api_key) {
31
+ line("Not logged in.");
32
+ return;
33
+ }
34
+ delete config.api_key;
35
+ if (Object.keys(config).length === 0)
36
+ clearConfig();
37
+ else
38
+ writeConfig(config);
39
+ line("Logged out — API key removed from config.");
40
+ }
41
+ export async function cmdWhoami(args) {
42
+ const user = await request("/users/me");
43
+ emit(user, outputOpts(args), () => line(`${user.name} <${user.email ?? ""}> (${user.id})`));
44
+ }
45
+ // ---------- boards & tasks (read) ----------
46
+ export async function cmdBoardShow(args) {
47
+ const ref = args._[0];
48
+ if (!ref)
49
+ throw usageError("Usage: kelocan board show <board>");
50
+ const board = await resolveBoard(ref);
51
+ emit(board, outputOpts(args), () => {
52
+ line(`Board: ${board.name} (${board.id})`);
53
+ if (board.description)
54
+ line(` ${board.description}`);
55
+ if (board.purpose)
56
+ line(` purpose: ${board.purpose}`);
57
+ if (board.sprint_context?.enabled) {
58
+ const s = board.sprint_context.active_sprint;
59
+ line(` sprint: ${s ? `active=${s.name}` : "no active sprint"}`);
60
+ }
61
+ for (const c of board.columns ?? []) {
62
+ line(` ${c.name} [${c.workflow_role}] — ${c.tasks?.length ?? 0} task(s)`);
63
+ }
64
+ });
65
+ }
66
+ export async function cmdTaskList(args) {
67
+ const ref = args._[0];
68
+ if (!ref)
69
+ throw usageError("Usage: kelocan task list <board> [--column X] [--label Y] [--sprint Z]");
70
+ const board = await resolveBoard(ref);
71
+ let tasks = collectTasks(board);
72
+ const colRef = flagStr(args.flags, "column");
73
+ if (colRef) {
74
+ const col = resolveColumn(board, colRef);
75
+ tasks = tasks.filter((t) => t.column_id === col.id);
76
+ }
77
+ const labelRef = flagStr(args.flags, "label");
78
+ if (labelRef) {
79
+ const wanted = labelRef.split(",").map((s) => s.trim().toLowerCase());
80
+ tasks = tasks.filter((t) => (t.labels ?? []).some((l) => wanted.includes(l.name.toLowerCase()) || wanted.includes(l.id.toLowerCase())));
81
+ }
82
+ const sprint = flagStr(args.flags, "sprint");
83
+ if (sprint)
84
+ tasks = filterSprint(tasks, board, sprint);
85
+ emit(tasks, outputOpts(args), () => {
86
+ for (const t of tasks) {
87
+ const labels = (t.labels ?? []).map((l) => l.name).join(",");
88
+ line(`${t.id} ${t.title} [${t.priority}]` +
89
+ `${t.column_name ? ` <${t.column_name}>` : ""}${labels ? ` {${labels}}` : ""}`);
90
+ }
91
+ if (tasks.length === 0)
92
+ line("(no tasks)");
93
+ });
94
+ }
95
+ function collectTasks(board) {
96
+ const out = [];
97
+ for (const c of board.columns ?? []) {
98
+ for (const t of c.tasks ?? [])
99
+ out.push({ ...t, column_name: c.name });
100
+ }
101
+ return out;
102
+ }
103
+ function filterSprint(tasks, board, sprint) {
104
+ const lower = sprint.toLowerCase();
105
+ if (lower === "backlog")
106
+ return tasks.filter((t) => !t.sprint_id);
107
+ if (lower === "active") {
108
+ const active = board.sprint_context?.active_sprint;
109
+ if (!active) {
110
+ throw usageError(`Board "${board.name}" has no active sprint (or sprint is disabled).`);
111
+ }
112
+ return tasks.filter((t) => t.sprint_id === active.id);
113
+ }
114
+ if (isUuid(sprint))
115
+ return tasks.filter((t) => t.sprint_id === sprint);
116
+ throw usageError(`Invalid --sprint value "${sprint}". Valid: active | backlog | <sprint UUID>`);
117
+ }
118
+ export async function cmdTaskShow(args) {
119
+ const id = args._[0];
120
+ if (!id)
121
+ throw usageError("Usage: kelocan task show <task-id>");
122
+ const task = await request(`/tasks/${id}`);
123
+ emit(task, outputOpts(args), () => {
124
+ line(`${task.title} (${task.id})`);
125
+ line(` column: ${task.column_id} kind: ${task.kind} priority: ${task.priority}`);
126
+ if (task.is_blocked)
127
+ line(` BLOCKED: ${task.blocked_reason ?? ""}`);
128
+ if (task.active_run)
129
+ line(` active run: ${task.active_run.agent_name} (${task.active_run.id})`);
130
+ if ((task.subtask_total ?? 0) > 0)
131
+ line(` subtasks: ${task.subtask_done}/${task.subtask_total}`);
132
+ if ((task.labels ?? []).length)
133
+ line(` labels: ${(task.labels ?? []).map((l) => l.name).join(", ")}`);
134
+ if ((task.assignees ?? []).length)
135
+ line(` assignees: ${(task.assignees ?? []).map((a) => a.name).join(", ")}`);
136
+ if (task.description)
137
+ line(`\n${task.description}`);
138
+ });
139
+ }
140
+ export async function cmdTaskPrompt(args) {
141
+ const id = args._[0];
142
+ if (!id)
143
+ throw usageError("Usage: kelocan task prompt <task-id>");
144
+ const prompt = await request(`/tasks/${id}/agent-prompt`);
145
+ emit(prompt, outputOpts(args), () => process.stdout.write(prompt.prompt + "\n"));
146
+ }
147
+ // ---------- tasks (write) ----------
148
+ export async function cmdTaskCreate(args) {
149
+ const ref = args._[0];
150
+ const title = flagStr(args.flags, "title");
151
+ if (!ref || !title) {
152
+ throw usageError('Usage: kelocan task create <board> --title "..." [--column X]');
153
+ }
154
+ const board = await resolveBoard(ref);
155
+ const colRef = flagStr(args.flags, "column");
156
+ const column = colRef
157
+ ? resolveColumn(board, colRef)
158
+ : (board.columns.find((c) => c.workflow_role === "backlog") ?? board.columns[0]);
159
+ if (!column)
160
+ throw usageError(`Board "${board.name}" has no columns.`);
161
+ const kind = flagStr(args.flags, "kind") ?? "task";
162
+ if (!KINDS.includes(kind))
163
+ throw usageError(`Invalid --kind "${kind}". Valid: ${KINDS.join(", ")}`);
164
+ const priority = flagStr(args.flags, "priority") ?? "medium";
165
+ if (!PRIORITIES.includes(priority)) {
166
+ throw usageError(`Invalid --priority "${priority}". Valid: ${PRIORITIES.join(", ")}`);
167
+ }
168
+ const labelRefs = flagStr(args.flags, "label")?.split(",").map((s) => s.trim()) ?? [];
169
+ const label_ids = await resolveLabels(board.id, labelRefs);
170
+ const sprint = flagStr(args.flags, "sprint");
171
+ const body = {
172
+ column_id: column.id,
173
+ title,
174
+ kind,
175
+ priority,
176
+ label_ids,
177
+ };
178
+ const description = flagStr(args.flags, "description");
179
+ if (description !== undefined)
180
+ body.description = description;
181
+ if (sprint !== undefined) {
182
+ if (sprint !== "active" && sprint !== "backlog" && !isUuid(sprint)) {
183
+ throw usageError(`Invalid --sprint "${sprint}". Valid: active | backlog | <sprint UUID>`);
184
+ }
185
+ body.sprint = sprint;
186
+ }
187
+ const task = await request("/tasks", { body });
188
+ emit(task, outputOpts(args), () => line(`Created task ${task.id} — "${task.title}" in ${column.name}`));
189
+ }
190
+ export async function cmdTaskMove(args) {
191
+ const id = args._[0];
192
+ const to = flagStr(args.flags, "to");
193
+ if (!id || !to)
194
+ throw usageError("Usage: kelocan task move <task-id> --to <column|role|uuid>");
195
+ const task = await request(`/tasks/${id}`);
196
+ const column = await request(`/columns/${task.column_id}`);
197
+ const board = await request(`/boards/${column.board_id}`);
198
+ const target = resolveColumn(board, to);
199
+ const posRaw = flagStr(args.flags, "position");
200
+ const position = posRaw !== undefined ? Number(posRaw) : undefined;
201
+ if (position !== undefined && (!Number.isInteger(position) || position < 1)) {
202
+ throw usageError(`Invalid --position "${posRaw}". Must be an integer >= 1.`);
203
+ }
204
+ const moved = await request(`/tasks/${id}/move`, {
205
+ method: "PATCH",
206
+ body: { target_column_id: target.id, position },
207
+ });
208
+ emit(moved, outputOpts(args), () => line(`Moved "${moved.title}" → ${target.name} [${target.workflow_role}]`));
209
+ }
210
+ export async function cmdTaskComment(args) {
211
+ const id = args._[0];
212
+ const message = flagStr(args.flags, "message");
213
+ const file = flagStr(args.flags, "file");
214
+ if (!id || (!message && !file)) {
215
+ throw usageError('Usage: kelocan task comment <task-id> (-m "text" | --file path)');
216
+ }
217
+ if (message && file) {
218
+ throw usageError("Use either -m/--message or --file, not both.");
219
+ }
220
+ const form = new FormData();
221
+ if (message) {
222
+ form.append("content", message);
223
+ }
224
+ else {
225
+ const buf = readFileSync(file);
226
+ form.append("files", new Blob([buf]), file.split("/").pop());
227
+ }
228
+ const comment = await request(`/tasks/${id}/comments`, { form });
229
+ emit(comment, outputOpts(args), () => line(`Comment added to task ${id}`));
230
+ }
231
+ // ---------- agent runs & subtasks ----------
232
+ export async function cmdRunStart(args) {
233
+ const taskId = args._[0];
234
+ const agent = flagStr(args.flags, "agent");
235
+ if (!taskId || !agent) {
236
+ throw usageError('Usage: kelocan run start <task-id> --agent "Devin" [--status "..."]');
237
+ }
238
+ const run = await request(`/tasks/${taskId}/agent-runs`, {
239
+ body: { agent_name: agent, status_text: flagStr(args.flags, "status") },
240
+ });
241
+ emit(run, outputOpts(args), () => {
242
+ line(`run_id: ${run.id}`);
243
+ line(`started: ${run.agent_name} on task ${run.task_id}`);
244
+ });
245
+ }
246
+ export async function cmdRunFinish(args) {
247
+ const runId = args._[0];
248
+ const outcome = flagStr(args.flags, "outcome");
249
+ if (!runId || !outcome) {
250
+ throw usageError("Usage: kelocan run finish <run-id> --outcome <succeeded|failed|cancelled> [-m ...]");
251
+ }
252
+ if (!OUTCOMES.includes(outcome)) {
253
+ throw usageError(`Invalid --outcome "${outcome}". Valid: ${OUTCOMES.join(", ")}`);
254
+ }
255
+ const run = await request(`/agent-runs/${runId}/finish`, {
256
+ body: { outcome, summary: flagStr(args.flags, "message") },
257
+ });
258
+ emit(run, outputOpts(args), () => line(`Run ${run.id} finished: ${run.status}`));
259
+ }
260
+ export async function cmdSubtaskList(args) {
261
+ const taskId = args._[0];
262
+ if (!taskId)
263
+ throw usageError("Usage: kelocan subtask list <task-id>");
264
+ const subs = await request(`/tasks/${taskId}/subtasks`);
265
+ emit(subs, outputOpts(args), () => {
266
+ for (const s of subs)
267
+ line(`${s.is_completed ? "[x]" : "[ ]"} ${s.id} ${s.title}`);
268
+ if (subs.length === 0)
269
+ line("(no subtasks)");
270
+ });
271
+ }
272
+ export async function cmdSubtaskToggle(args) {
273
+ const id = args._[0];
274
+ if (!id)
275
+ throw usageError("Usage: kelocan subtask toggle <subtask-id>");
276
+ const sub = await request(`/subtasks/${id}/toggle`, { method: "PATCH" });
277
+ emit(sub, outputOpts(args), () => line(`${sub.is_completed ? "[x]" : "[ ]"} ${sub.title}`));
278
+ }
package/dist/config.js ADDED
@@ -0,0 +1,70 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { homedir, platform } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ export const DEFAULT_SERVER_URL = "https://api.kanban.sakze.com";
5
+ export function configDir() {
6
+ if (platform() === "win32") {
7
+ return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "kelocan");
8
+ }
9
+ return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "kelocan");
10
+ }
11
+ export function configPath() {
12
+ return join(configDir(), "config.json");
13
+ }
14
+ export function readConfig() {
15
+ const path = configPath();
16
+ if (!existsSync(path))
17
+ return {};
18
+ try {
19
+ return JSON.parse(readFileSync(path, "utf8"));
20
+ }
21
+ catch {
22
+ return {};
23
+ }
24
+ }
25
+ export function writeConfig(config) {
26
+ const path = configPath();
27
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
28
+ writeFileSync(path, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
29
+ // Enforce 600 even if the file already existed with looser perms.
30
+ try {
31
+ chmodSync(path, 0o600);
32
+ }
33
+ catch {
34
+ /* best effort on non-POSIX filesystems */
35
+ }
36
+ }
37
+ export function clearConfig() {
38
+ const path = configPath();
39
+ if (existsSync(path))
40
+ rmSync(path);
41
+ }
42
+ /** API base URL including /api/v1 suffix. Env KELOCAN_URL wins over config. */
43
+ export function apiBase(config = readConfig()) {
44
+ let url = (process.env.KELOCAN_URL ?? config.url ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
45
+ if (!url.endsWith("/api/v1"))
46
+ url += "/api/v1";
47
+ return url;
48
+ }
49
+ /** Web app base URL for /cli/authorize. Env KELOCAN_WEB_URL wins over config. */
50
+ export function webBase(config = readConfig()) {
51
+ if (process.env.KELOCAN_WEB_URL)
52
+ return process.env.KELOCAN_WEB_URL.replace(/\/+$/, "");
53
+ if (config.web_url)
54
+ return config.web_url.replace(/\/+$/, "");
55
+ const api = apiBase(config);
56
+ // Derive: strip /api/v1 suffix and a leading "api." subdomain.
57
+ const origin = api.replace(/\/api\/v1$/, "");
58
+ try {
59
+ const u = new URL(origin);
60
+ u.hostname = u.hostname.replace(/^api\./, "");
61
+ return u.origin;
62
+ }
63
+ catch {
64
+ return origin;
65
+ }
66
+ }
67
+ /** API key: env KELOCAN_API_KEY wins over config. */
68
+ export function apiKey(config = readConfig()) {
69
+ return process.env.KELOCAN_API_KEY ?? config.api_key;
70
+ }
package/dist/doctor.js ADDED
@@ -0,0 +1,198 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { detectAll } from "./agents.js";
4
+ import { apiBase, apiKey, readConfig } from "./config.js";
5
+ import { mcpUrl, probeMcp } from "./install.js";
6
+ import { parseJsonc } from "./jsonc.js";
7
+ import { verifyKey } from "./login.js";
8
+ import { SKILL_NAMES, skillStatus } from "./skills.js";
9
+ const EXPECTED_URL_HINT = "kelocan mcp install --agent <name>";
10
+ function checkConfigEntry(det, expectedUrl) {
11
+ const out = [];
12
+ const { def, configPath } = det;
13
+ if (!existsSync(configPath)) {
14
+ out.push({
15
+ level: "fail",
16
+ message: `config ${configPath} not found`,
17
+ fix: `kelocan mcp install --agent ${def.id}`,
18
+ });
19
+ return out;
20
+ }
21
+ const text = readFileSync(configPath, "utf8");
22
+ if (def.format === "json") {
23
+ let parsed;
24
+ try {
25
+ parsed = parseJsonc(text);
26
+ }
27
+ catch (e) {
28
+ out.push({
29
+ level: "fail",
30
+ message: `config ${configPath} is invalid JSON: ${e instanceof Error ? e.message : e}`,
31
+ fix: `restore ${configPath}.bak or run ${EXPECTED_URL_HINT.replace("<name>", def.id)}`,
32
+ });
33
+ return out;
34
+ }
35
+ const container = parsed[def.containerKey];
36
+ let entry = container?.kelocan;
37
+ let entryNote = "";
38
+ if (!entry && def.id === "claude") {
39
+ // `claude mcp add` may store the entry under projects.<dir>.mcpServers
40
+ const projects = parsed.projects;
41
+ for (const [dir, proj] of Object.entries(projects ?? {})) {
42
+ const e = proj.mcpServers?.kelocan;
43
+ if (e) {
44
+ entry = e;
45
+ entryNote = ` (local scope under project ${dir})`;
46
+ break;
47
+ }
48
+ }
49
+ }
50
+ if (!entry) {
51
+ out.push({
52
+ level: "fail",
53
+ message: `no ${def.containerKey}.kelocan entry in ${configPath}`,
54
+ fix: `kelocan mcp install --agent ${def.id}`,
55
+ });
56
+ return out;
57
+ }
58
+ const url = (entry.url ?? entry.serverUrl);
59
+ if (url !== expectedUrl) {
60
+ out.push({
61
+ level: "warn",
62
+ message: `kelocan MCP url is "${url ?? "(missing)"}", expected "${expectedUrl}"`,
63
+ fix: `kelocan mcp install --agent ${def.id}`,
64
+ });
65
+ }
66
+ else {
67
+ out.push({ level: "ok", message: `MCP entry ok (${configPath}${entryNote})` });
68
+ }
69
+ }
70
+ else if (def.format === "toml") {
71
+ if (!text.includes("[mcp_servers.kelocan]")) {
72
+ out.push({
73
+ level: "fail",
74
+ message: `no [mcp_servers.kelocan] section in ${configPath}`,
75
+ fix: `kelocan mcp install --agent ${def.id}`,
76
+ });
77
+ }
78
+ else if (!text.includes(`url = "${expectedUrl}"`)) {
79
+ out.push({
80
+ level: "warn",
81
+ message: `kelocan MCP url in ${configPath} does not match ${expectedUrl}`,
82
+ fix: `kelocan mcp install --agent ${def.id}`,
83
+ });
84
+ }
85
+ else {
86
+ out.push({ level: "ok", message: `MCP entry ok (${configPath})` });
87
+ }
88
+ }
89
+ else {
90
+ // yaml
91
+ if (!/^\s+kelocan\s*:/m.test(text)) {
92
+ out.push({
93
+ level: "fail",
94
+ message: `no mcp_servers.kelocan entry in ${configPath}`,
95
+ fix: `kelocan mcp install --agent ${def.id}`,
96
+ });
97
+ }
98
+ else if (!text.includes(`url: "${expectedUrl}"`) && !text.includes(`url: ${expectedUrl}`)) {
99
+ out.push({
100
+ level: "warn",
101
+ message: `kelocan MCP url in ${configPath} does not match ${expectedUrl}`,
102
+ fix: `kelocan mcp install --agent ${def.id}`,
103
+ });
104
+ }
105
+ else {
106
+ out.push({ level: "ok", message: `MCP entry ok (${configPath})` });
107
+ }
108
+ }
109
+ return out;
110
+ }
111
+ function checkSkills(det) {
112
+ const out = [];
113
+ for (const dir of det.skillDirs) {
114
+ for (const name of SKILL_NAMES) {
115
+ const st = skillStatus(dir, name);
116
+ if (st === "missing") {
117
+ out.push({
118
+ level: "fail",
119
+ message: `skill "${name}" missing in ${dir}`,
120
+ fix: `kelocan skills install --agent ${det.def.id}`,
121
+ });
122
+ }
123
+ else if (st === "current") {
124
+ out.push({ level: "ok", message: `skill "${name}" installed (${dir})` });
125
+ }
126
+ else {
127
+ out.push({
128
+ level: "warn",
129
+ message: `skill "${name}" in ${dir} differs from bundled copy (${st})`,
130
+ fix: `kelocan skills install --agent ${det.def.id}`,
131
+ });
132
+ }
133
+ }
134
+ }
135
+ return out;
136
+ }
137
+ export async function runDoctor(home = homedir()) {
138
+ const expectedUrl = mcpUrl();
139
+ const server = [];
140
+ // connectivity (REST base) + auth
141
+ const base = apiBase(readConfig());
142
+ const key = apiKey();
143
+ try {
144
+ const res = await fetch(base.replace(/\/api\/v1$/, "") || base, { method: "HEAD" }).catch(() => null);
145
+ server.push({
146
+ level: res ? "ok" : "warn",
147
+ message: `server ${base} ${res ? "reachable" : "did not answer HEAD"}`,
148
+ });
149
+ }
150
+ catch {
151
+ server.push({ level: "warn", message: `server ${base} unreachable` });
152
+ }
153
+ if (key) {
154
+ try {
155
+ const user = await verifyKey(base, key);
156
+ server.push({ level: "ok", message: `authenticated as ${user.name}` });
157
+ }
158
+ catch (e) {
159
+ server.push({
160
+ level: "fail",
161
+ message: `auth failed: ${e instanceof Error ? e.message : e}`,
162
+ fix: "kelocan login",
163
+ });
164
+ }
165
+ }
166
+ else {
167
+ server.push({ level: "warn", message: "no API key configured", fix: "kelocan login" });
168
+ }
169
+ try {
170
+ const status = await probeMcp(expectedUrl, key);
171
+ server.push({ level: "ok", message: `MCP ${expectedUrl} ${status}` });
172
+ }
173
+ catch (e) {
174
+ server.push({
175
+ level: "fail",
176
+ message: `MCP ${expectedUrl}: ${e instanceof Error ? e.message : e}`,
177
+ });
178
+ }
179
+ const agents = detectAll(home).map((det) => {
180
+ if (!det.detected) {
181
+ return {
182
+ agent: det.def.id,
183
+ name: det.def.name,
184
+ detected: false,
185
+ findings: [{ level: "info", message: "not installed — skipped" }],
186
+ };
187
+ }
188
+ return {
189
+ agent: det.def.id,
190
+ name: det.def.name,
191
+ detected: true,
192
+ findings: [...checkConfigEntry(det, expectedUrl), ...checkSkills(det)],
193
+ };
194
+ });
195
+ const ok = !server.some((f) => f.level === "fail") &&
196
+ !agents.some((a) => a.findings.some((f) => f.level === "fail"));
197
+ return { server, agents, ok };
198
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,15 @@
1
+ /** Exit codes: 0 ok, 1 validation/client error, 2 auth, 3 network/server. */
2
+ export class CliError extends Error {
3
+ exitCode;
4
+ constructor(message, exitCode = 1) {
5
+ super(message);
6
+ this.exitCode = exitCode;
7
+ this.name = "CliError";
8
+ }
9
+ }
10
+ export function usageError(message) {
11
+ return new CliError(message, 1);
12
+ }
13
+ export function authError(message) {
14
+ return new CliError(message, 2);
15
+ }