@naia-team/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/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @naia/cli
2
+
3
+ Naia platform CLI for local agents (Codex, Claude Code, Claude Desktop via MCP).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx @naia/cli install
9
+ ```
10
+
11
+ ## Login
12
+
13
+ ```bash
14
+ npx @naia/cli auth login --org-slug <org-slug> --webapp-url <webapp-url>
15
+ ```
16
+
17
+ ## Verify
18
+
19
+ ```bash
20
+ npx @naia/cli doctor --runtime-url <runtime-url>
21
+ ```
22
+
23
+ ## Platform commands
24
+
25
+ ```bash
26
+ npx @naia/cli platform invoices list
27
+ npx @naia/cli platform invoices create-draft --issue-date 2026-04-06 --description "CLI draft" --amount 100 --entity-id <id>
28
+ npx @naia/cli platform invoices issue --invoice-id <id>
29
+ ```
30
+
31
+ ## MCP server
32
+
33
+ ```bash
34
+ npx @naia/cli mcp serve
35
+ ```
36
+
package/bin/naia.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../src/main.js";
3
+
4
+ main().catch((error) => {
5
+ console.error(error instanceof Error ? error.message : String(error));
6
+ process.exit(1);
7
+ });
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@naia-team/cli",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "bin": {
7
+ "naia": "bin/naia.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src"
12
+ ],
13
+ "license": "UNLICENSED"
14
+ }
@@ -0,0 +1,119 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { URL } from "node:url";
4
+ import { writeAuthProfile } from "../lib/auth-store.js";
5
+ import { parseArgVector } from "../lib/args.js";
6
+ import { readCliConfig } from "../lib/config-store.js";
7
+
8
+ export async function runAuthLogin(args) {
9
+ const config = await parseConfig(args);
10
+ const state = randomUUID();
11
+ const callback = await waitForCallback(config, state);
12
+ await writeAuthProfile({
13
+ profile: config.profile,
14
+ data: {
15
+ token: callback.token,
16
+ organizationId: callback.organizationId,
17
+ memberId: callback.memberId,
18
+ webappBaseUrl: config.webappBaseUrl,
19
+ orgSlug: config.orgSlug,
20
+ createdAt: Date.now(),
21
+ },
22
+ });
23
+
24
+ console.log("CLI login completed.");
25
+ console.log(`profile: ${config.profile}`);
26
+ console.log(`organizationId: ${callback.organizationId}`);
27
+ console.log(`memberId: ${callback.memberId}`);
28
+ }
29
+
30
+ async function waitForCallback(config, state) {
31
+ return new Promise((resolve, reject) => {
32
+ const server = createServer((req, res) => {
33
+ try {
34
+ const host = req.headers.host ?? "127.0.0.1";
35
+ const requestUrl = new URL(req.url ?? "/", `http://${host}`);
36
+ if (requestUrl.pathname !== "/callback") {
37
+ res.statusCode = 404;
38
+ res.end("Not found");
39
+ return;
40
+ }
41
+
42
+ const receivedState = requestUrl.searchParams.get("state") ?? "";
43
+ const token = requestUrl.searchParams.get("token") ?? "";
44
+ const organizationId = requestUrl.searchParams.get("organizationId") ?? "";
45
+ const memberId = requestUrl.searchParams.get("memberId") ?? "";
46
+
47
+ if (!token || !organizationId || !memberId || receivedState !== state) {
48
+ res.statusCode = 400;
49
+ res.end("Invalid auth callback");
50
+ cleanup();
51
+ reject(new Error("Invalid callback payload"));
52
+ return;
53
+ }
54
+
55
+ res.statusCode = 200;
56
+ res.setHeader("content-type", "text/html; charset=utf-8");
57
+ res.end("<html><body><h3>Naia CLI login completed.</h3><p>You can close this tab.</p></body></html>");
58
+ cleanup();
59
+ resolve({ token, organizationId, memberId });
60
+ } catch (error) {
61
+ cleanup();
62
+ reject(error instanceof Error ? error : new Error("Unknown callback error"));
63
+ }
64
+ });
65
+
66
+ server.listen(0, "127.0.0.1", async () => {
67
+ const address = server.address();
68
+ if (!address || typeof address === "string") {
69
+ cleanup();
70
+ reject(new Error("Could not bind local callback server"));
71
+ return;
72
+ }
73
+ const callbackUrl = `http://127.0.0.1:${address.port}/callback`;
74
+ const target = new URL(`${config.webappBaseUrl.replace(/\/+$/, "")}/app/${encodeURIComponent(config.orgSlug)}/cli/auth`);
75
+ target.searchParams.set("callback", callbackUrl);
76
+ target.searchParams.set("state", state);
77
+ await openBrowser(target.toString());
78
+ console.log("Opening browser for Naia login...");
79
+ console.log(target.toString());
80
+ });
81
+
82
+ const timeout = setTimeout(() => {
83
+ cleanup();
84
+ reject(new Error(`Login timeout after ${config.timeoutMs}ms`));
85
+ }, config.timeoutMs);
86
+
87
+ function cleanup() {
88
+ clearTimeout(timeout);
89
+ server.close();
90
+ }
91
+ });
92
+ }
93
+
94
+ async function parseConfig(args) {
95
+ const { options } = parseArgVector(args);
96
+ const defaults = await readCliConfig();
97
+ const webappBaseUrl = options.webappUrl ?? process.env.NAIA_WEBAPP_URL ?? defaults.webappUrl ?? "http://localhost:5173";
98
+ const orgSlug = options.orgSlug ?? "";
99
+ const profile = options.profile ?? defaults.defaultProfile ?? "default";
100
+ const timeoutMs = Number.parseInt(options.timeoutMs ?? "180000", 10);
101
+ if (!orgSlug) {
102
+ throw new Error("Missing required --org-slug");
103
+ }
104
+ return { webappBaseUrl, orgSlug, profile, timeoutMs };
105
+ }
106
+
107
+ async function openBrowser(url) {
108
+ const platform = process.platform;
109
+ const { spawn } = await import("node:child_process");
110
+ if (platform === "darwin") {
111
+ spawn("open", [url], { stdio: "ignore", detached: true }).unref();
112
+ return;
113
+ }
114
+ if (platform === "win32") {
115
+ spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
116
+ return;
117
+ }
118
+ spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
119
+ }
@@ -0,0 +1,40 @@
1
+ import { parseArgVector } from "../lib/args.js";
2
+ import { readAuthProfile } from "../lib/auth-store.js";
3
+ import { readCliConfig } from "../lib/config-store.js";
4
+ import { loadPlatformRuntimeContext, platformApiCall } from "../lib/platform-client.js";
5
+
6
+ export async function runDoctor(args) {
7
+ const { options } = parseArgVector(args);
8
+ const config = await readCliConfig();
9
+ const profile = options.profile ?? config.defaultProfile ?? "default";
10
+ const runtimeUrl = options.runtimeUrl ?? config.runtimeUrl;
11
+
12
+ console.log(`profile: ${profile}`);
13
+ console.log(`runtime: ${runtimeUrl}`);
14
+
15
+ const authProfile = await readAuthProfile(profile);
16
+ if (!authProfile) {
17
+ throw new Error(`No auth profile '${profile}'. Run: naia auth login --org-slug <slug>`);
18
+ }
19
+ console.log("auth: OK");
20
+ console.log(`organizationId: ${authProfile.organizationId}`);
21
+ console.log(`memberId: ${authProfile.memberId}`);
22
+
23
+ const healthResponse = await fetch(`${runtimeUrl.replace(/\/+$/, "")}/health`);
24
+ if (!healthResponse.ok) {
25
+ throw new Error(`runtime health failed with status ${healthResponse.status}`);
26
+ }
27
+ console.log("runtime health: OK");
28
+
29
+ const context = await loadPlatformRuntimeContext({
30
+ profile,
31
+ runtimeUrl,
32
+ executorId: options.executorId,
33
+ executorName: options.executorName,
34
+ executorType: options.executorType,
35
+ });
36
+ await platformApiCall(context, "/api/agents/v1/platform/invoices/list?limit=1", { method: "GET" });
37
+ console.log("platform auth+api: OK");
38
+ console.log("doctor: PASS");
39
+ }
40
+
@@ -0,0 +1,296 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readFile, writeFile, copyFile } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { spawnSync } from "node:child_process";
7
+ import { parseArgVector } from "../lib/args.js";
8
+ import { defaultCliConfig, getConfigPath, readCliConfig, writeCliConfig } from "../lib/config-store.js";
9
+ import { getAuthStorePath } from "../lib/auth-store.js";
10
+ import { buildClaudeDesktopJsonSnippet, buildCodexTomlSnippet, buildMcpServerDefinition } from "../lib/mcp-config.js";
11
+
12
+ const INSTALL_TARGETS = [
13
+ { id: "codex", label: "Codex CLI" },
14
+ { id: "claude-code", label: "Claude Code" },
15
+ { id: "claude-desktop", label: "Claude Desktop" },
16
+ { id: "manual", label: "Manual config only" },
17
+ ];
18
+
19
+ export async function runInstall(args) {
20
+ const parsed = parseArgVector(args);
21
+ const options = parsed.options;
22
+ const current = await readCliConfig();
23
+ const base = defaultCliConfig();
24
+
25
+ let runtimeUrl = options.runtimeUrl ?? current.runtimeUrl ?? base.runtimeUrl;
26
+ let webappUrl = options.webappUrl ?? current.webappUrl ?? base.webappUrl;
27
+ let profile = options.profile ?? current.defaultProfile ?? base.defaultProfile;
28
+ let executorName = options.executorName ?? current.executor.name;
29
+ let executorId = options.executorId ?? current.executor.id;
30
+ let executorType = options.executorType === "user" ? "user" : current.executor.type;
31
+ let targets = parseTargets(options.targets);
32
+
33
+ if (shouldPrompt(options, targets)) {
34
+ const interactive = await runInteractiveWizard({
35
+ runtimeUrl,
36
+ webappUrl,
37
+ profile,
38
+ executorName,
39
+ executorId,
40
+ executorType,
41
+ targets,
42
+ });
43
+ runtimeUrl = interactive.runtimeUrl;
44
+ webappUrl = interactive.webappUrl;
45
+ profile = interactive.profile;
46
+ executorName = interactive.executorName;
47
+ executorId = interactive.executorId;
48
+ executorType = interactive.executorType;
49
+ targets = interactive.targets;
50
+ }
51
+
52
+ const config = {
53
+ ...current,
54
+ runtimeUrl,
55
+ webappUrl,
56
+ defaultProfile: profile,
57
+ executor: {
58
+ id: executorId,
59
+ name: executorName,
60
+ type: executorType === "user" ? "user" : "agent",
61
+ },
62
+ };
63
+ await writeCliConfig(config);
64
+
65
+ const installResult = await applyIntegrationTargets(targets, config);
66
+ printSummary(config, targets, installResult);
67
+ }
68
+
69
+ function shouldPrompt(options, targets) {
70
+ if (options.nonInteractive === "true") return false;
71
+ return !options.runtimeUrl && !options.webappUrl && !options.targets && targets.length === 0;
72
+ }
73
+
74
+ function parseTargets(raw) {
75
+ if (!raw) return [];
76
+ return raw
77
+ .split(",")
78
+ .map((item) => item.trim().toLowerCase())
79
+ .filter((item) => INSTALL_TARGETS.some((target) => target.id === item));
80
+ }
81
+
82
+ async function runInteractiveWizard(defaults) {
83
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
84
+ try {
85
+ console.log("Naia CLI installer");
86
+ console.log("");
87
+ const runtimeUrl = (await rl.question(`Runtime URL (${defaults.runtimeUrl}): `)).trim() || defaults.runtimeUrl;
88
+ const webappUrl = (await rl.question(`Webapp URL (${defaults.webappUrl}): `)).trim() || defaults.webappUrl;
89
+ const profile = (await rl.question(`Default profile (${defaults.profile}): `)).trim() || defaults.profile;
90
+ const executorName = (await rl.question(`Executor display name (${defaults.executorName}): `)).trim() || defaults.executorName;
91
+ const executorId = (await rl.question(`Executor id (${defaults.executorId}): `)).trim() || defaults.executorId;
92
+ const executorTypeInput = (await rl.question(`Executor type [agent|user] (${defaults.executorType}): `)).trim().toLowerCase();
93
+ const executorType = executorTypeInput === "user" ? "user" : defaults.executorType;
94
+
95
+ console.log("");
96
+ console.log("Select targets (comma-separated numbers):");
97
+ INSTALL_TARGETS.forEach((target, index) => {
98
+ console.log(`${index + 1}) ${target.label}`);
99
+ });
100
+ const selection = (await rl.question("Targets (1,2): ")).trim();
101
+ const targets = parseTargetSelection(selection);
102
+
103
+ return {
104
+ runtimeUrl,
105
+ webappUrl,
106
+ profile,
107
+ executorName,
108
+ executorId,
109
+ executorType,
110
+ targets: targets.length > 0 ? targets : ["manual"],
111
+ };
112
+ } finally {
113
+ rl.close();
114
+ }
115
+ }
116
+
117
+ function parseTargetSelection(input) {
118
+ if (!input) return [];
119
+ const picked = new Set();
120
+ for (const token of input.split(",")) {
121
+ const index = Number.parseInt(token.trim(), 10);
122
+ if (!Number.isFinite(index) || index < 1 || index > INSTALL_TARGETS.length) continue;
123
+ picked.add(INSTALL_TARGETS[index - 1].id);
124
+ }
125
+ return [...picked];
126
+ }
127
+
128
+ async function applyIntegrationTargets(targets, config) {
129
+ const result = [];
130
+ const selected = targets.length > 0 ? targets : ["manual"];
131
+ for (const target of selected) {
132
+ if (target === "codex") {
133
+ result.push(await installCodex(config));
134
+ continue;
135
+ }
136
+ if (target === "claude-code") {
137
+ result.push(await installClaudeCode(config));
138
+ continue;
139
+ }
140
+ if (target === "claude-desktop") {
141
+ result.push(await installClaudeDesktop(config));
142
+ continue;
143
+ }
144
+ if (target === "manual") {
145
+ result.push({ target, status: "manual", detail: "No file changes applied." });
146
+ continue;
147
+ }
148
+ }
149
+ return result;
150
+ }
151
+
152
+ async function installCodex(config) {
153
+ const filePath = join(homedir(), ".codex", "config.toml");
154
+ const snippet = buildCodexTomlSnippet({
155
+ runtimeUrl: config.runtimeUrl,
156
+ executorName: config.executor.name,
157
+ executorId: config.executor.id,
158
+ executorType: config.executor.type,
159
+ });
160
+ await ensureDir(dirname(filePath));
161
+ const existing = existsSync(filePath) ? await readFile(filePath, "utf8") : "";
162
+ const cleaned = stripCodexNaiaBlock(existing);
163
+ const merged = `${cleaned.trimEnd()}\n\n${snippet}\n`;
164
+ if (existsSync(filePath)) {
165
+ await backupFile(filePath);
166
+ }
167
+ await writeFile(filePath, merged, "utf8");
168
+ return { target: "codex", status: "updated", detail: filePath };
169
+ }
170
+
171
+ async function installClaudeDesktop(config) {
172
+ const filePath = resolveClaudeDesktopPath();
173
+ await ensureDir(dirname(filePath));
174
+ const existing = existsSync(filePath) ? JSON.parse(await readFile(filePath, "utf8")) : {};
175
+ if (!existing.mcpServers || typeof existing.mcpServers !== "object") {
176
+ existing.mcpServers = {};
177
+ }
178
+ existing.mcpServers.naia = buildMcpServerDefinition({
179
+ runtimeUrl: config.runtimeUrl,
180
+ executorName: config.executor.name,
181
+ executorId: config.executor.id,
182
+ executorType: config.executor.type,
183
+ });
184
+ if (existsSync(filePath)) {
185
+ await backupFile(filePath);
186
+ }
187
+ await writeFile(filePath, JSON.stringify(existing, null, 2), "utf8");
188
+ return { target: "claude-desktop", status: "updated", detail: filePath };
189
+ }
190
+
191
+ async function installClaudeCode(config) {
192
+ const env = {
193
+ ...process.env,
194
+ NAIA_RUNTIME_URL: config.runtimeUrl,
195
+ NAIA_EXECUTOR_NAME: config.executor.name,
196
+ NAIA_EXECUTOR_ID: config.executor.id,
197
+ NAIA_EXECUTOR_TYPE: config.executor.type,
198
+ };
199
+ const result = spawnSync("claude", ["mcp", "add", "naia", "--", "naia", "mcp", "serve"], {
200
+ stdio: "pipe",
201
+ encoding: "utf8",
202
+ env,
203
+ });
204
+ if (result.status === 0) {
205
+ return { target: "claude-code", status: "updated", detail: "Registered with `claude mcp add`." };
206
+ }
207
+ return {
208
+ target: "claude-code",
209
+ status: "manual",
210
+ detail: "Could not run `claude mcp add`. Run manually:\nclaude mcp add naia -- naia mcp serve",
211
+ };
212
+ }
213
+
214
+ function resolveClaudeDesktopPath() {
215
+ if (process.platform === "darwin") {
216
+ return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
217
+ }
218
+ if (process.platform === "win32") {
219
+ const appData = process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
220
+ return join(appData, "Claude", "claude_desktop_config.json");
221
+ }
222
+ return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
223
+ }
224
+
225
+ function stripCodexNaiaBlock(source) {
226
+ const lines = source.split("\n");
227
+ const result = [];
228
+ let skipping = false;
229
+ for (const line of lines) {
230
+ const trimmed = line.trim();
231
+ if (trimmed === "[mcp_servers.naia]" || trimmed === "[mcp_servers.naia.env]") {
232
+ skipping = true;
233
+ continue;
234
+ }
235
+ if (skipping && trimmed.startsWith("[") && trimmed.endsWith("]")) {
236
+ skipping = false;
237
+ }
238
+ if (!skipping) {
239
+ result.push(line);
240
+ }
241
+ }
242
+ return result.join("\n").trim();
243
+ }
244
+
245
+ async function ensureDir(path) {
246
+ await mkdir(path, { recursive: true });
247
+ }
248
+
249
+ async function backupFile(path) {
250
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
251
+ await copyFile(path, `${path}.${timestamp}.bak`);
252
+ }
253
+
254
+ function printSummary(config, targets, installResult) {
255
+ const configPath = getConfigPath();
256
+ const authPath = getAuthStorePath();
257
+
258
+ console.log("Naia CLI install completed.");
259
+ console.log(`config: ${configPath}`);
260
+ console.log(`auth store: ${authPath}`);
261
+ console.log("");
262
+ console.log("Configured values:");
263
+ console.log(`- runtime: ${config.runtimeUrl}`);
264
+ console.log(`- webapp: ${config.webappUrl}`);
265
+ console.log(`- profile: ${config.defaultProfile}`);
266
+ console.log(`- executor: ${config.executor.name} (${config.executor.id}, ${config.executor.type})`);
267
+ console.log("");
268
+ console.log(`Targets: ${(targets.length > 0 ? targets : ["manual"]).join(", ")}`);
269
+ for (const item of installResult) {
270
+ console.log(`- ${item.target}: ${item.status}${item.detail ? ` -> ${item.detail}` : ""}`);
271
+ }
272
+ console.log("");
273
+ console.log("Next steps:");
274
+ console.log(`1) naia auth login --org-slug <orgSlug> --webapp-url ${config.webappUrl}`);
275
+ console.log(`2) naia doctor --runtime-url ${config.runtimeUrl}`);
276
+ console.log(`3) naia platform invoices list --runtime-url ${config.runtimeUrl}`);
277
+ console.log("");
278
+ console.log("Manual snippets:");
279
+ console.log("");
280
+ console.log("Codex (~/.codex/config.toml):");
281
+ console.log(buildCodexTomlSnippet({
282
+ runtimeUrl: config.runtimeUrl,
283
+ executorName: config.executor.name,
284
+ executorId: config.executor.id,
285
+ executorType: config.executor.type,
286
+ }));
287
+ console.log("");
288
+ console.log("Claude Desktop (claude_desktop_config.json):");
289
+ console.log(buildClaudeDesktopJsonSnippet({
290
+ runtimeUrl: config.runtimeUrl,
291
+ executorName: config.executor.name,
292
+ executorId: config.executor.id,
293
+ executorType: config.executor.type,
294
+ }));
295
+ }
296
+