@corvio/cli 0.1.0-beta.2

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/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # Corvio Workspace CLI
2
+
3
+ Official CLI for a user's Corvio Workspace. It supports an existing `CORVIO_API_KEY` and a browser device-link login.
4
+
5
+ ```bash
6
+ npm install --global @corvio/cli
7
+ export CORVIO_API_KEY='cvk_...'
8
+ corvio auth status
9
+ corvio ask --prompt 'Summarize recurring launch risks' --json
10
+ ```
11
+
12
+ When no environment key is configured, run `corvio auth login`. The environment key always takes precedence. See [the Corvio CLI guide](https://corvio.ai/developers/cli) for the supported command and receipt contract.
package/dist/cli.js ADDED
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ ApiClient,
5
+ CliError,
6
+ EXIT,
7
+ clearWorkspaceCredential,
8
+ idempotencyKey,
9
+ listOption,
10
+ normalizeApiBase,
11
+ numberOption,
12
+ openBrowser,
13
+ option,
14
+ readTextFile,
15
+ readWorkspaceCredential,
16
+ requireConfirmation,
17
+ runMain,
18
+ sleep,
19
+ storeWorkspaceCredential,
20
+ writeResult,
21
+ writeTextFile,
22
+ } from "./core.js";
23
+
24
+ const VERSION = "0.1.0-beta.2";
25
+ const DEFAULT_API_BASE = "https://api.corvio.ai/v1";
26
+ const DEFAULT_APP_BASE = "https://corvio.ai";
27
+ const USER_AGENT = `corvio-cli/${VERSION}`;
28
+
29
+ const HELP = `Corvio Workspace CLI ${VERSION}
30
+
31
+ Usage:
32
+ corvio auth login|status|logout
33
+ corvio capabilities
34
+ corvio ask --prompt <text> [--conversation-id <id>] [--allow-actions]
35
+ corvio questions list|get [id]
36
+ corvio conversations list|get [id]
37
+ corvio search <query>
38
+ corvio docs list|get|create|update|archive|share|open [id]
39
+
40
+ Global options:
41
+ --api-base <url> --json --no-input --yes --help --version
42
+
43
+ Authentication:
44
+ CORVIO_API_KEY=cvk_... takes precedence over the stored device-link login.
45
+ Run 'corvio auth login' when no environment key is configured.
46
+ `;
47
+
48
+ function baseUrl(options) {
49
+ return normalizeApiBase(options["api-base"] || process.env.CORVIO_API_BASE, DEFAULT_API_BASE);
50
+ }
51
+
52
+ async function client(options, { auth = true } = {}) {
53
+ const credential = auth ? await readWorkspaceCredential() : null;
54
+ if (auth && !credential) {
55
+ throw new CliError("No Corvio credential found. Set CORVIO_API_KEY or run `corvio auth login`.", {
56
+ code: "auth_required",
57
+ exitCode: EXIT.AUTH,
58
+ });
59
+ }
60
+ return {
61
+ api: new ApiClient({ baseUrl: baseUrl(options), credential: credential?.key, userAgent: USER_AGENT }),
62
+ credential,
63
+ };
64
+ }
65
+
66
+ async function authLogin(options) {
67
+ const current = await readWorkspaceCredential();
68
+ if (current?.source === "environment" && !options.force) {
69
+ const { api } = await client(options);
70
+ const identity = await api.request("/me");
71
+ writeResult({ authenticated: true, source: "environment", identity }, { json: options.json });
72
+ return;
73
+ }
74
+ const api = new ApiClient({ baseUrl: baseUrl(options), userAgent: USER_AGENT });
75
+ const authorization = await api.request("/auth/device-authorizations", {
76
+ method: "POST",
77
+ auth: false,
78
+ body: {
79
+ client_name: "Corvio Workspace CLI",
80
+ cli_version: VERSION,
81
+ scopes: [
82
+ "questions:read",
83
+ "questions:write",
84
+ "search:read",
85
+ "documents:read",
86
+ "documents:write",
87
+ "documents:share",
88
+ ],
89
+ },
90
+ });
91
+ const diagnostic = `Open ${authorization.verification_uri_complete}\nCode: ${authorization.user_code}\n`;
92
+ process.stderr.write(diagnostic);
93
+ if (!options["no-input"]) await openBrowser(authorization.verification_uri_complete);
94
+
95
+ const expiresAt = Date.now() + Number(authorization.expires_in || 600) * 1000;
96
+ let intervalSeconds = Math.max(5, Number(authorization.interval || 5));
97
+ let token;
98
+ while (Date.now() < expiresAt) {
99
+ await sleep(intervalSeconds * 1000);
100
+ try {
101
+ token = await api.request("/auth/device-token", {
102
+ method: "POST",
103
+ auth: false,
104
+ body: { device_code: authorization.device_code },
105
+ });
106
+ break;
107
+ } catch (error) {
108
+ if (!(error instanceof CliError)) throw error;
109
+ if (error.code === "authorization_pending") continue;
110
+ if (error.code === "slow_down") {
111
+ intervalSeconds = Math.min(30, intervalSeconds + 5);
112
+ continue;
113
+ }
114
+ throw error;
115
+ }
116
+ }
117
+ if (!token?.secret) {
118
+ throw new CliError("Device authorization expired; run `corvio auth login` again.", {
119
+ code: "expired_token",
120
+ exitCode: EXIT.AUTH,
121
+ });
122
+ }
123
+ const stored = await storeWorkspaceCredential(token);
124
+ const authenticatedApi = new ApiClient({ baseUrl: baseUrl(options), credential: token.secret, userAgent: USER_AGENT });
125
+ const identity = await authenticatedApi.request("/me");
126
+ writeResult({
127
+ authenticated: true,
128
+ source: "device_link",
129
+ profile: stored.profile,
130
+ workspace: identity.workspace,
131
+ scopes: identity.api_key?.scopes || [],
132
+ }, { json: options.json });
133
+ }
134
+
135
+ async function authStatus(options) {
136
+ const credential = await readWorkspaceCredential();
137
+ if (!credential) {
138
+ writeResult({ authenticated: false, sources: ["CORVIO_API_KEY", "device_link"] }, { json: options.json });
139
+ return;
140
+ }
141
+ const api = new ApiClient({ baseUrl: baseUrl(options), credential: credential.key, userAgent: USER_AGENT });
142
+ const identity = await api.request("/me");
143
+ writeResult({
144
+ authenticated: true,
145
+ source: credential.source,
146
+ profile: credential.profile,
147
+ user: identity.user,
148
+ workspace: identity.workspace,
149
+ api_key: identity.api_key,
150
+ }, { json: options.json });
151
+ }
152
+
153
+ async function authLogout(options) {
154
+ const credential = await readWorkspaceCredential();
155
+ if (!credential) {
156
+ writeResult({ authenticated: false, changed: false }, { json: options.json });
157
+ return;
158
+ }
159
+ if (credential.source === "environment") {
160
+ throw new CliError("CORVIO_API_KEY is managed by the parent environment and cannot be removed by the CLI.", {
161
+ code: "environment_credential_managed_externally",
162
+ exitCode: EXIT.AUTH,
163
+ });
164
+ }
165
+ const api = new ApiClient({ baseUrl: baseUrl(options), credential: credential.key, userAgent: USER_AGENT });
166
+ await api.request("/auth/credentials/current:revoke", { method: "POST" });
167
+ await clearWorkspaceCredential();
168
+ writeResult({ authenticated: false, revoked: true, profile: credential.profile }, { json: options.json });
169
+ }
170
+
171
+ async function capabilities(options) {
172
+ const { api, credential } = await client(options);
173
+ const [index, identity] = await Promise.all([api.request("", { auth: false }), api.request("/me")]);
174
+ writeResult({
175
+ cli_version: VERSION,
176
+ auth_source: credential.source,
177
+ api: index,
178
+ principal: identity,
179
+ unavailable: {
180
+ files: "Public Workspace Asset upload/organization is not advertised by /v1 yet.",
181
+ sync: "Public Local Sync adapters are not advertised by /v1 yet.",
182
+ },
183
+ }, { json: options.json });
184
+ }
185
+
186
+ async function ask(options, positionals) {
187
+ const prompt = String(options.prompt || positionals.join(" ")).trim();
188
+ if (!prompt) throw new CliError("Provide --prompt <text> or a positional prompt.");
189
+ const { api } = await client(options);
190
+ const result = await api.request("/questions", {
191
+ method: "POST",
192
+ headers: { "idempotency-key": idempotencyKey(options) },
193
+ body: {
194
+ prompt,
195
+ conversation_id: options["conversation-id"] || null,
196
+ mode: options["allow-actions"] ? "allow_actions" : "answer_only",
197
+ sources: listOption(options, "sources", ["workspace", "web", "memory"]),
198
+ },
199
+ });
200
+ writeResult(result, { json: options.json });
201
+ }
202
+
203
+ function pageQuery(options) {
204
+ return {
205
+ limit: numberOption(options, "limit", 20),
206
+ cursor: options.cursor,
207
+ conversation_id: options["conversation-id"],
208
+ };
209
+ }
210
+
211
+ async function questions(options, action, id) {
212
+ const { api } = await client(options);
213
+ const result = action === "list"
214
+ ? await api.request("/questions", { query: pageQuery(options) })
215
+ : await api.request(`/questions/${id || option(options, "id", { required: true })}`);
216
+ writeResult(result, { json: options.json });
217
+ }
218
+
219
+ async function conversations(options, action, id) {
220
+ const { api } = await client(options);
221
+ const result = action === "list"
222
+ ? await api.request("/conversations", { query: pageQuery(options) })
223
+ : await api.request(`/conversations/${id || option(options, "id", { required: true })}`);
224
+ writeResult(result, { json: options.json });
225
+ }
226
+
227
+ async function search(options, positionals) {
228
+ const query = String(options.query || positionals.join(" ")).trim();
229
+ if (!query) throw new CliError("Provide a search query.");
230
+ const { api } = await client(options);
231
+ const result = await api.request("/search", {
232
+ method: "POST",
233
+ body: {
234
+ query,
235
+ limit: numberOption(options, "limit", 10),
236
+ page_id: options["page-id"] || null,
237
+ node_id: options["node-id"] || null,
238
+ },
239
+ });
240
+ writeResult(result, { json: options.json });
241
+ }
242
+
243
+ async function docs(options, action, id) {
244
+ const { api } = await client(options);
245
+ const documentId = id || options.id;
246
+ let result;
247
+ if (action === "list") {
248
+ result = await api.request("/documents", { query: pageQuery(options) });
249
+ } else if (action === "get") {
250
+ result = await api.request(`/documents/${documentId || option(options, "id", { required: true })}`);
251
+ if (options.output) {
252
+ await writeTextFile(options.output, String(result.markdown || ""));
253
+ result = { ...result, output: options.output };
254
+ }
255
+ } else if (action === "create") {
256
+ const markdown = options.file ? await readTextFile(options.file) : String(options.markdown || "");
257
+ result = await api.request("/documents", {
258
+ method: "POST",
259
+ headers: { "idempotency-key": idempotencyKey(options) },
260
+ body: {
261
+ title: option(options, "title", { required: true }),
262
+ markdown,
263
+ parent_node_id: options["parent-node-id"] || null,
264
+ doc_type: "page",
265
+ },
266
+ });
267
+ } else if (action === "update") {
268
+ const markdown = options.file ? await readTextFile(options.file) : options.markdown;
269
+ result = await api.request(`/documents/${documentId || option(options, "id", { required: true })}`, {
270
+ method: "PUT",
271
+ body: {
272
+ ...(options.title ? { title: options.title } : {}),
273
+ ...(markdown !== undefined ? { markdown } : {}),
274
+ ...(options["expected-revision"] !== undefined
275
+ ? { expected_content_revision: Number.parseInt(options["expected-revision"], 10) }
276
+ : {}),
277
+ },
278
+ });
279
+ } else if (action === "archive") {
280
+ requireConfirmation(options, "Archiving a document");
281
+ result = await api.request(`/documents/${documentId || option(options, "id", { required: true })}`, { method: "DELETE" });
282
+ } else if (action === "share") {
283
+ requireConfirmation(options, "Creating a document share link");
284
+ result = await api.request(`/documents/${documentId || option(options, "id", { required: true })}/shares`, {
285
+ method: "POST",
286
+ body: { public_display_name: options["display-name"] || null },
287
+ });
288
+ } else if (action === "open") {
289
+ const target = `${String(process.env.CORVIO_APP_BASE || DEFAULT_APP_BASE).replace(/\/+$/, "")}/app/d/${documentId || option(options, "id", { required: true })}`;
290
+ const opened = options["no-input"] ? false : await openBrowser(target);
291
+ result = { document_id: documentId, url: target, opened };
292
+ } else {
293
+ throw new CliError("docs action must be list, get, create, update, archive, share, or open.");
294
+ }
295
+ writeResult(result, { json: options.json });
296
+ }
297
+
298
+ async function main({ options, positionals }) {
299
+ const [command, action, id, ...rest] = positionals;
300
+ if (options.version || command === "version") return writeResult(VERSION, { json: options.json });
301
+ if (options.help || !command || command === "help") return writeResult(HELP.trimEnd(), { json: false });
302
+ if (command === "auth") {
303
+ if (action === "login") return authLogin(options);
304
+ if (action === "status") return authStatus(options);
305
+ if (action === "logout") return authLogout(options);
306
+ throw new CliError("auth action must be login, status, or logout.");
307
+ }
308
+ if (command === "capabilities") return capabilities(options);
309
+ if (command === "ask") return ask(options, [action, id, ...rest].filter(Boolean));
310
+ if (command === "questions") return questions(options, action || "list", id);
311
+ if (command === "conversations") return conversations(options, action || "list", id);
312
+ if (command === "search") return search(options, [action, id, ...rest].filter(Boolean));
313
+ if (command === "docs") return docs(options, action || "list", id);
314
+ if (command === "files" || command === "sync") {
315
+ throw new CliError(`${command} is not available until the public /v1 capability is released.`, {
316
+ code: `${command}_capability_unavailable`,
317
+ exitCode: EXIT.UNAVAILABLE,
318
+ });
319
+ }
320
+ throw new CliError(`Unknown command: ${command}`);
321
+ }
322
+
323
+ await runMain(main);
package/dist/core.js ADDED
@@ -0,0 +1,324 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
+ import { homedir, platform } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+
7
+ export const EXIT = Object.freeze({
8
+ OK: 0,
9
+ USAGE: 2,
10
+ AUTH: 3,
11
+ UNAVAILABLE: 4,
12
+ CONFLICT: 5,
13
+ RATE_LIMIT: 6,
14
+ REMOTE: 7,
15
+ });
16
+
17
+ export class CliError extends Error {
18
+ constructor(message, { code = "cli_error", exitCode = EXIT.USAGE, status = null, details = null } = {}) {
19
+ super(message);
20
+ this.name = "CliError";
21
+ this.code = code;
22
+ this.exitCode = exitCode;
23
+ this.status = status;
24
+ this.details = details;
25
+ }
26
+ }
27
+
28
+ function exitCodeForStatus(status) {
29
+ if (status === 401 || status === 403) return EXIT.AUTH;
30
+ if (status === 409) return EXIT.CONFLICT;
31
+ if (status === 429) return EXIT.RATE_LIMIT;
32
+ return EXIT.REMOTE;
33
+ }
34
+
35
+ export function parseArgv(argv) {
36
+ const options = Object.create(null);
37
+ const positionals = [];
38
+ const booleanOptions = new Set([
39
+ "json",
40
+ "no-input",
41
+ "yes",
42
+ "allow-actions",
43
+ "private-link",
44
+ "force",
45
+ "help",
46
+ "version",
47
+ ]);
48
+ for (let index = 0; index < argv.length; index += 1) {
49
+ const value = argv[index];
50
+ if (!value.startsWith("--")) {
51
+ positionals.push(value);
52
+ continue;
53
+ }
54
+ const separator = value.indexOf("=");
55
+ const name = value.slice(2, separator === -1 ? undefined : separator);
56
+ if (booleanOptions.has(name)) {
57
+ options[name] = separator === -1 ? true : value.slice(separator + 1) !== "false";
58
+ continue;
59
+ }
60
+ if (separator !== -1) {
61
+ options[name] = value.slice(separator + 1);
62
+ continue;
63
+ }
64
+ const next = argv[index + 1];
65
+ if (next === undefined || next.startsWith("--")) {
66
+ throw new CliError(`Option --${name} requires a value.`, { code: "missing_option_value" });
67
+ }
68
+ options[name] = next;
69
+ index += 1;
70
+ }
71
+ return { options, positionals };
72
+ }
73
+
74
+ export function option(options, name, { required = false, fallback = undefined } = {}) {
75
+ const value = options[name] ?? fallback;
76
+ if (required && (value === undefined || String(value).trim() === "")) {
77
+ throw new CliError(`Missing required option --${name}.`, { code: "missing_required_option" });
78
+ }
79
+ return value;
80
+ }
81
+
82
+ export function listOption(options, name, fallback = []) {
83
+ const value = options[name];
84
+ if (value === undefined) return fallback;
85
+ return String(value).split(",").map((item) => item.trim()).filter(Boolean);
86
+ }
87
+
88
+ export function numberOption(options, name, fallback) {
89
+ const value = options[name];
90
+ if (value === undefined) return fallback;
91
+ const parsed = Number.parseInt(String(value), 10);
92
+ if (!Number.isFinite(parsed) || parsed < 1) {
93
+ throw new CliError(`Option --${name} must be a positive integer.`, { code: "invalid_number_option" });
94
+ }
95
+ return parsed;
96
+ }
97
+
98
+ export function normalizeApiBase(value, fallback) {
99
+ const raw = String(value || fallback).trim().replace(/\/+$/, "");
100
+ if (!/^https:\/\//i.test(raw) && !/^http:\/\/(127\.0\.0\.1|localhost)(:\d+)?/i.test(raw)) {
101
+ throw new CliError("Corvio API base must use HTTPS (localhost is allowed for development).", {
102
+ code: "insecure_api_base",
103
+ });
104
+ }
105
+ return raw.endsWith("/v1") ? raw : `${raw}/v1`;
106
+ }
107
+
108
+ export class ApiClient {
109
+ constructor({ baseUrl, credential = null, userAgent, fetchImpl = globalThis.fetch }) {
110
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
111
+ this.credential = credential;
112
+ this.userAgent = userAgent;
113
+ this.fetchImpl = fetchImpl;
114
+ }
115
+
116
+ async request(path, { method = "GET", body, query, headers = {}, auth = true } = {}) {
117
+ const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`);
118
+ for (const [key, value] of Object.entries(query || {})) {
119
+ if (value !== undefined && value !== null && value !== "") url.searchParams.set(key, String(value));
120
+ }
121
+ const requestHeaders = {
122
+ accept: "application/json",
123
+ "user-agent": this.userAgent,
124
+ ...headers,
125
+ };
126
+ if (body !== undefined) requestHeaders["content-type"] = "application/json";
127
+ if (auth) {
128
+ if (!this.credential) {
129
+ throw new CliError("Corvio authentication is required.", { code: "auth_required", exitCode: EXIT.AUTH });
130
+ }
131
+ requestHeaders.authorization = `Bearer ${this.credential}`;
132
+ }
133
+ let response;
134
+ try {
135
+ response = await this.fetchImpl(url, {
136
+ method,
137
+ headers: requestHeaders,
138
+ body: body === undefined ? undefined : JSON.stringify(body),
139
+ });
140
+ } catch (cause) {
141
+ throw new CliError(`Unable to reach Corvio: ${cause instanceof Error ? cause.message : String(cause)}`, {
142
+ code: "network_error",
143
+ exitCode: EXIT.REMOTE,
144
+ });
145
+ }
146
+ const text = await response.text();
147
+ let payload = null;
148
+ if (text) {
149
+ try {
150
+ payload = JSON.parse(text);
151
+ } catch {
152
+ payload = { message: text.slice(0, 500) };
153
+ }
154
+ }
155
+ if (!response.ok) {
156
+ const envelope = payload?.error || payload?.detail || payload || {};
157
+ throw new CliError(String(envelope.message || envelope.code || `Corvio request failed (${response.status}).`), {
158
+ code: String(envelope.code || `http_${response.status}`),
159
+ status: response.status,
160
+ exitCode: exitCodeForStatus(response.status),
161
+ details: envelope.details || null,
162
+ });
163
+ }
164
+ return payload;
165
+ }
166
+ }
167
+
168
+ function configRoot(env = process.env) {
169
+ if (env.CORVIO_CONFIG_DIR) return resolve(env.CORVIO_CONFIG_DIR);
170
+ if (env.XDG_CONFIG_HOME) return join(resolve(env.XDG_CONFIG_HOME), "corvio");
171
+ return join(homedir(), ".config", "corvio");
172
+ }
173
+
174
+ export function workspaceCredentialPath(env = process.env) {
175
+ return join(configRoot(env), "credentials.json");
176
+ }
177
+
178
+ export async function readWorkspaceCredential(env = process.env) {
179
+ const environmentKey = String(env.CORVIO_API_KEY || "").trim();
180
+ if (environmentKey) {
181
+ if (!environmentKey.startsWith("cvk_")) {
182
+ throw new CliError("CORVIO_API_KEY must be a Workspace cvk_ key.", {
183
+ code: "invalid_workspace_key",
184
+ exitCode: EXIT.AUTH,
185
+ });
186
+ }
187
+ return { key: environmentKey, source: "environment", profile: null, metadata: null };
188
+ }
189
+ const path = workspaceCredentialPath(env);
190
+ let parsed;
191
+ try {
192
+ parsed = JSON.parse(await readFile(path, "utf8"));
193
+ } catch (cause) {
194
+ if (cause?.code === "ENOENT") return null;
195
+ throw new CliError("Stored Corvio credentials are unreadable.", { code: "credential_store_invalid", exitCode: EXIT.AUTH });
196
+ }
197
+ const profileName = String(parsed.active_profile || "default");
198
+ const profile = parsed.profiles?.[profileName];
199
+ const key = String(profile?.api_key || "").trim();
200
+ if (!key.startsWith("cvk_")) {
201
+ throw new CliError("Stored Corvio credentials are invalid; run `corvio auth login` again.", {
202
+ code: "credential_store_invalid",
203
+ exitCode: EXIT.AUTH,
204
+ });
205
+ }
206
+ return { key, source: "device_link", profile: profileName, metadata: profile };
207
+ }
208
+
209
+ export async function storeWorkspaceCredential(payload, env = process.env) {
210
+ const path = workspaceCredentialPath(env);
211
+ const root = dirname(path);
212
+ await mkdir(root, { recursive: true, mode: 0o700 });
213
+ await chmod(root, 0o700).catch(() => {});
214
+ const profileName = String(payload.workspace_id || "default");
215
+ const document = {
216
+ version: 1,
217
+ active_profile: profileName,
218
+ profiles: {
219
+ [profileName]: {
220
+ api_key: payload.secret,
221
+ key_id: payload.key_id,
222
+ workspace_id: payload.workspace_id,
223
+ workspace_name: payload.workspace_name,
224
+ scopes: payload.scopes || [],
225
+ created_at: payload.created_at || new Date().toISOString(),
226
+ source: "device_link",
227
+ },
228
+ },
229
+ };
230
+ const temporary = `${path}.${randomUUID()}.tmp`;
231
+ await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600, flag: "wx" });
232
+ await chmod(temporary, 0o600);
233
+ await rename(temporary, path);
234
+ await chmod(path, 0o600);
235
+ return { path, profile: profileName };
236
+ }
237
+
238
+ export async function clearWorkspaceCredential(env = process.env) {
239
+ const path = workspaceCredentialPath(env);
240
+ await rm(path, { force: true });
241
+ return path;
242
+ }
243
+
244
+ export function readAdminCredential(env = process.env) {
245
+ const key = String(env.CORVIO_ADMIN_API_KEY || "").trim();
246
+ if (!key) return null;
247
+ if (!key.startsWith("cva_")) {
248
+ throw new CliError("CORVIO_ADMIN_API_KEY must be a platform administrator cva_ key.", {
249
+ code: "invalid_admin_key",
250
+ exitCode: EXIT.AUTH,
251
+ });
252
+ }
253
+ return { key, source: "environment" };
254
+ }
255
+
256
+ export function writeResult(value, { json = false, stdout = process.stdout } = {}) {
257
+ if (json || typeof value !== "string") {
258
+ stdout.write(`${JSON.stringify(value, null, json ? 2 : 0)}\n`);
259
+ } else {
260
+ stdout.write(`${value}\n`);
261
+ }
262
+ }
263
+
264
+ export function writeError(error, { json = false, stderr = process.stderr } = {}) {
265
+ const safe = {
266
+ error: {
267
+ code: error instanceof CliError ? error.code : "unexpected_error",
268
+ message: error instanceof Error ? error.message : String(error),
269
+ ...(error instanceof CliError && error.status ? { status: error.status } : {}),
270
+ ...(error instanceof CliError && error.details ? { details: error.details } : {}),
271
+ },
272
+ };
273
+ stderr.write(json ? `${JSON.stringify(safe)}\n` : `Error: ${safe.error.message}\n`);
274
+ }
275
+
276
+ export async function runMain(main, argv = process.argv.slice(2)) {
277
+ let json = argv.includes("--json");
278
+ try {
279
+ const parsed = parseArgv(argv);
280
+ json = Boolean(parsed.options.json);
281
+ await main(parsed);
282
+ } catch (error) {
283
+ writeError(error, { json });
284
+ process.exitCode = error instanceof CliError ? error.exitCode : 1;
285
+ }
286
+ }
287
+
288
+ export async function openBrowser(url) {
289
+ const target = String(url);
290
+ const command = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
291
+ const args = platform() === "win32" ? ["/c", "start", "", target] : [target];
292
+ try {
293
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
294
+ child.unref();
295
+ return true;
296
+ } catch {
297
+ return false;
298
+ }
299
+ }
300
+
301
+ export async function sleep(milliseconds) {
302
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
303
+ }
304
+
305
+ export async function readTextFile(path) {
306
+ return readFile(resolve(String(path)), "utf8");
307
+ }
308
+
309
+ export async function writeTextFile(path, content) {
310
+ const target = resolve(String(path));
311
+ await mkdir(dirname(target), { recursive: true });
312
+ await writeFile(target, content, "utf8");
313
+ return target;
314
+ }
315
+
316
+ export function requireConfirmation(options, action) {
317
+ if (!options.yes) {
318
+ throw new CliError(`${action} requires --yes.`, { code: "confirmation_required" });
319
+ }
320
+ }
321
+
322
+ export function idempotencyKey(options) {
323
+ return String(options["idempotency-key"] || randomUUID());
324
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@corvio/cli",
3
+ "version": "0.1.0-beta.2",
4
+ "description": "Official Corvio Workspace CLI for questions, knowledge search, documents, and Agent workflows.",
5
+ "type": "module",
6
+ "bin": {
7
+ "corvio": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "prepack": "node ../../scripts/build.mjs workspace"
15
+ },
16
+ "engines": {
17
+ "node": ">=20.11"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "license": "UNLICENSED",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/NeoFlux-AI/corvio.git",
26
+ "directory": "developer-cli/packages/workspace"
27
+ },
28
+ "homepage": "https://corvio.ai/developers/cli",
29
+ "bugs": {
30
+ "url": "https://github.com/NeoFlux-AI/corvio/issues"
31
+ },
32
+ "keywords": [
33
+ "corvio",
34
+ "cli",
35
+ "workspace",
36
+ "second-brain",
37
+ "codex",
38
+ "claude-code"
39
+ ]
40
+ }