@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 +36 -0
- package/bin/naia.js +7 -0
- package/package.json +14 -0
- package/src/commands/auth-login.js +119 -0
- package/src/commands/doctor.js +40 -0
- package/src/commands/install.js +296 -0
- package/src/commands/mcp-serve.js +224 -0
- package/src/commands/platform.js +116 -0
- package/src/commands/session-run.js +250 -0
- package/src/lib/args.js +24 -0
- package/src/lib/auth-store.js +49 -0
- package/src/lib/config-store.js +48 -0
- package/src/lib/mcp-config.js +34 -0
- package/src/lib/output.js +8 -0
- package/src/lib/platform-client.js +49 -0
- package/src/main.js +59 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { stdin, stdout } from "node:process";
|
|
2
|
+
import { loadPlatformRuntimeContext, platformApiCall } from "../lib/platform-client.js";
|
|
3
|
+
|
|
4
|
+
const TOOLS = [
|
|
5
|
+
{
|
|
6
|
+
name: "naia_invoices_list",
|
|
7
|
+
description: "List invoices from Naia platform.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
direction: { type: "string", enum: ["issued", "received", "third_party"] },
|
|
12
|
+
documentStatus: { type: "string", enum: ["draft", "issued", "voided", "rectified"] },
|
|
13
|
+
paymentStatus: { type: "string", enum: ["unpaid", "partial", "paid"] },
|
|
14
|
+
entityId: { type: "string" },
|
|
15
|
+
limit: { type: "number" },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "naia_entities_search",
|
|
21
|
+
description: "Search entities by name/taxId.",
|
|
22
|
+
inputSchema: { type: "object", properties: { query: { type: "string" }, taxId: { type: "string" }, limit: { type: "number" } } },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "naia_contacts_list",
|
|
26
|
+
description: "List contacts by entity or name.",
|
|
27
|
+
inputSchema: { type: "object", properties: { entityId: { type: "string" }, name: { type: "string" } } },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "naia_invoices_create_draft",
|
|
31
|
+
description: "Create invoice draft in Naia.",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
required: ["issueDate", "description", "amount"],
|
|
35
|
+
properties: {
|
|
36
|
+
issueDate: { type: "string" },
|
|
37
|
+
description: { type: "string" },
|
|
38
|
+
amount: { type: "number" },
|
|
39
|
+
entityId: { type: "string" },
|
|
40
|
+
entityName: { type: "string" },
|
|
41
|
+
entityTaxId: { type: "string" },
|
|
42
|
+
quantity: { type: "number" },
|
|
43
|
+
taxPercent: { type: "number" },
|
|
44
|
+
dueDate: { type: "string" },
|
|
45
|
+
invoiceNumber: { type: "string" },
|
|
46
|
+
notes: { type: "string" },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "naia_invoices_issue",
|
|
52
|
+
description: "Issue invoice by id.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
required: ["invoiceId"],
|
|
56
|
+
properties: { invoiceId: { type: "string" }, seriesId: { type: "string" } },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
let runtimeContextPromise = null;
|
|
62
|
+
let buffer = Buffer.alloc(0);
|
|
63
|
+
|
|
64
|
+
export async function runMcpServe() {
|
|
65
|
+
stdin.on("data", (chunk) => {
|
|
66
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
67
|
+
void drainBuffer();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function drainBuffer() {
|
|
72
|
+
while (true) {
|
|
73
|
+
const delimiterIndex = buffer.indexOf("\r\n\r\n");
|
|
74
|
+
if (delimiterIndex < 0) return;
|
|
75
|
+
const headerText = buffer.subarray(0, delimiterIndex).toString("utf8");
|
|
76
|
+
const contentLength = parseContentLength(headerText);
|
|
77
|
+
if (contentLength < 0) {
|
|
78
|
+
buffer = Buffer.alloc(0);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const totalLength = delimiterIndex + 4 + contentLength;
|
|
82
|
+
if (buffer.length < totalLength) return;
|
|
83
|
+
const bodyBuffer = buffer.subarray(delimiterIndex + 4, totalLength);
|
|
84
|
+
buffer = buffer.subarray(totalLength);
|
|
85
|
+
await handleMessage(bodyBuffer.toString("utf8"));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseContentLength(headerText) {
|
|
90
|
+
const lines = headerText.split("\r\n");
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
const [name, value] = line.split(":").map((part) => part.trim());
|
|
93
|
+
if (name.toLowerCase() === "content-length") {
|
|
94
|
+
const parsed = Number.parseInt(value ?? "", 10);
|
|
95
|
+
return Number.isFinite(parsed) ? parsed : -1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return -1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function handleMessage(raw) {
|
|
102
|
+
let request;
|
|
103
|
+
try {
|
|
104
|
+
request = JSON.parse(raw);
|
|
105
|
+
} catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!request.id && request.method === "notifications/initialized") return;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const result = await dispatch(request);
|
|
112
|
+
if (request.id !== undefined) {
|
|
113
|
+
writeResponse({ jsonrpc: "2.0", id: request.id ?? null, result });
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (request.id !== undefined) {
|
|
117
|
+
writeResponse({
|
|
118
|
+
jsonrpc: "2.0",
|
|
119
|
+
id: request.id ?? null,
|
|
120
|
+
error: { code: -32000, message: error instanceof Error ? error.message : String(error) },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function dispatch(request) {
|
|
127
|
+
if (request.method === "initialize") {
|
|
128
|
+
return {
|
|
129
|
+
protocolVersion: "2024-11-05",
|
|
130
|
+
capabilities: { tools: {} },
|
|
131
|
+
serverInfo: { name: "naia-mcp", version: "0.1.0" },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (request.method === "tools/list") {
|
|
135
|
+
return { tools: TOOLS };
|
|
136
|
+
}
|
|
137
|
+
if (request.method === "tools/call") {
|
|
138
|
+
const params = request.params ?? {};
|
|
139
|
+
const name = asString(params.name);
|
|
140
|
+
const args = params.arguments && typeof params.arguments === "object" ? params.arguments : {};
|
|
141
|
+
const data = await callTool(name, args);
|
|
142
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
143
|
+
}
|
|
144
|
+
if (request.method === "ping") {
|
|
145
|
+
return { ok: true };
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`Unsupported method: ${request.method}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function callTool(name, args) {
|
|
151
|
+
const context = await getRuntimeContext();
|
|
152
|
+
if (name === "naia_invoices_list") {
|
|
153
|
+
const query = new URLSearchParams();
|
|
154
|
+
if (asString(args.direction)) query.set("direction", asString(args.direction));
|
|
155
|
+
if (asString(args.documentStatus)) query.set("documentStatus", asString(args.documentStatus));
|
|
156
|
+
if (asString(args.paymentStatus)) query.set("paymentStatus", asString(args.paymentStatus));
|
|
157
|
+
if (asString(args.entityId)) query.set("entityId", asString(args.entityId));
|
|
158
|
+
if (asNumber(args.limit)) query.set("limit", String(asNumber(args.limit)));
|
|
159
|
+
return platformApiCall(context, `/api/agents/v1/platform/invoices/list?${query.toString()}`, { method: "GET" });
|
|
160
|
+
}
|
|
161
|
+
if (name === "naia_entities_search") {
|
|
162
|
+
const query = new URLSearchParams();
|
|
163
|
+
if (asString(args.query)) query.set("query", asString(args.query));
|
|
164
|
+
if (asString(args.taxId)) query.set("taxId", asString(args.taxId));
|
|
165
|
+
if (asNumber(args.limit)) query.set("limit", String(asNumber(args.limit)));
|
|
166
|
+
return platformApiCall(context, `/api/agents/v1/platform/entities/search?${query.toString()}`, { method: "GET" });
|
|
167
|
+
}
|
|
168
|
+
if (name === "naia_contacts_list") {
|
|
169
|
+
const query = new URLSearchParams();
|
|
170
|
+
if (asString(args.entityId)) query.set("entityId", asString(args.entityId));
|
|
171
|
+
if (asString(args.name)) query.set("name", asString(args.name));
|
|
172
|
+
return platformApiCall(context, `/api/agents/v1/platform/contacts/list?${query.toString()}`, { method: "GET" });
|
|
173
|
+
}
|
|
174
|
+
if (name === "naia_invoices_create_draft") {
|
|
175
|
+
const payload = {
|
|
176
|
+
issueDate: asString(args.issueDate),
|
|
177
|
+
description: asString(args.description),
|
|
178
|
+
amount: asNumber(args.amount),
|
|
179
|
+
};
|
|
180
|
+
if (!payload.issueDate || !payload.description || typeof payload.amount !== "number") {
|
|
181
|
+
throw new Error("naia_invoices_create_draft requires issueDate, description, amount");
|
|
182
|
+
}
|
|
183
|
+
for (const key of ["entityId", "entityName", "entityTaxId", "dueDate", "invoiceNumber", "notes"]) {
|
|
184
|
+
const value = asString(args[key]);
|
|
185
|
+
if (value) payload[key] = value;
|
|
186
|
+
}
|
|
187
|
+
for (const key of ["quantity", "taxPercent"]) {
|
|
188
|
+
const value = asNumber(args[key]);
|
|
189
|
+
if (typeof value === "number") payload[key] = value;
|
|
190
|
+
}
|
|
191
|
+
return platformApiCall(context, "/api/agents/v1/platform/invoices/create-draft", { method: "POST", body: payload });
|
|
192
|
+
}
|
|
193
|
+
if (name === "naia_invoices_issue") {
|
|
194
|
+
const invoiceId = asString(args.invoiceId);
|
|
195
|
+
if (!invoiceId) throw new Error("naia_invoices_issue requires invoiceId");
|
|
196
|
+
const payload = { invoiceId };
|
|
197
|
+
const seriesId = asString(args.seriesId);
|
|
198
|
+
if (seriesId) payload.seriesId = seriesId;
|
|
199
|
+
return platformApiCall(context, "/api/agents/v1/platform/invoices/issue", { method: "POST", body: payload });
|
|
200
|
+
}
|
|
201
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function getRuntimeContext() {
|
|
205
|
+
if (!runtimeContextPromise) {
|
|
206
|
+
runtimeContextPromise = loadPlatformRuntimeContext({});
|
|
207
|
+
}
|
|
208
|
+
return runtimeContextPromise;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function writeResponse(response) {
|
|
212
|
+
const body = Buffer.from(JSON.stringify(response), "utf8");
|
|
213
|
+
const headers = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, "utf8");
|
|
214
|
+
stdout.write(Buffer.concat([headers, body]));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function asString(value) {
|
|
218
|
+
return typeof value === "string" ? value.trim() : "";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function asNumber(value) {
|
|
222
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { parseArgVector } from "../lib/args.js";
|
|
2
|
+
import { loadPlatformRuntimeContext, platformApiCall } from "../lib/platform-client.js";
|
|
3
|
+
import { printJson } from "../lib/output.js";
|
|
4
|
+
|
|
5
|
+
export async function runPlatform(args) {
|
|
6
|
+
const { options, positionals } = parseArgVector(args);
|
|
7
|
+
const [domain, action, ...rest] = positionals;
|
|
8
|
+
const context = await loadPlatformRuntimeContext({
|
|
9
|
+
profile: options.profile,
|
|
10
|
+
runtimeUrl: options.runtimeUrl,
|
|
11
|
+
executorId: options.executorId,
|
|
12
|
+
executorName: options.executorName,
|
|
13
|
+
executorType: options.executorType,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (domain === "invoices" && action === "list") {
|
|
17
|
+
await listInvoices(context, rest);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (domain === "invoices" && action === "create-draft") {
|
|
21
|
+
await createInvoiceDraft(context, rest);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (domain === "invoices" && action === "issue") {
|
|
25
|
+
await issueInvoice(context, rest);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (domain === "entities" && action === "search") {
|
|
29
|
+
await searchEntities(context, rest);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (domain === "contacts" && action === "list") {
|
|
33
|
+
await listContacts(context, rest);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Unsupported platform command: ${[domain, action].filter(Boolean).join(" ")}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function listInvoices(context, args) {
|
|
40
|
+
const options = parseArgVector(args).options;
|
|
41
|
+
const query = new URLSearchParams();
|
|
42
|
+
if (options.direction) query.set("direction", options.direction);
|
|
43
|
+
if (options.documentStatus) query.set("documentStatus", options.documentStatus);
|
|
44
|
+
if (options.paymentStatus) query.set("paymentStatus", options.paymentStatus);
|
|
45
|
+
if (options.entityId) query.set("entityId", options.entityId);
|
|
46
|
+
if (options.limit) query.set("limit", options.limit);
|
|
47
|
+
const result = await platformApiCall(context, `/api/agents/v1/platform/invoices/list?${query.toString()}`, {
|
|
48
|
+
method: "GET",
|
|
49
|
+
});
|
|
50
|
+
printJson(result);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function createInvoiceDraft(context, args) {
|
|
54
|
+
const options = parseArgVector(args).options;
|
|
55
|
+
const issueDate = options.issueDate;
|
|
56
|
+
const description = options.description;
|
|
57
|
+
const amount = options.amount ? Number.parseFloat(options.amount) : NaN;
|
|
58
|
+
if (!issueDate || !description || !Number.isFinite(amount)) {
|
|
59
|
+
throw new Error("create-draft requires --issue-date YYYY-MM-DD --description --amount");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const payload = { issueDate, description, amount };
|
|
63
|
+
if (options.entityId) payload.entityId = options.entityId;
|
|
64
|
+
if (options.entityName) payload.entityName = options.entityName;
|
|
65
|
+
if (options.entityTaxId) payload.entityTaxId = options.entityTaxId;
|
|
66
|
+
if (options.quantity) payload.quantity = Number.parseFloat(options.quantity);
|
|
67
|
+
if (options.taxPercent) payload.taxPercent = Number.parseFloat(options.taxPercent);
|
|
68
|
+
if (options.dueDate) payload.dueDate = options.dueDate;
|
|
69
|
+
if (options.invoiceNumber) payload.invoiceNumber = options.invoiceNumber;
|
|
70
|
+
if (options.notes) payload.notes = options.notes;
|
|
71
|
+
|
|
72
|
+
const result = await platformApiCall(context, "/api/agents/v1/platform/invoices/create-draft", {
|
|
73
|
+
method: "POST",
|
|
74
|
+
body: payload,
|
|
75
|
+
});
|
|
76
|
+
printJson(result);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function issueInvoice(context, args) {
|
|
80
|
+
const options = parseArgVector(args).options;
|
|
81
|
+
if (!options.invoiceId) {
|
|
82
|
+
throw new Error("issue requires --invoice-id");
|
|
83
|
+
}
|
|
84
|
+
const result = await platformApiCall(context, "/api/agents/v1/platform/invoices/issue", {
|
|
85
|
+
method: "POST",
|
|
86
|
+
body: {
|
|
87
|
+
invoiceId: options.invoiceId,
|
|
88
|
+
seriesId: options.seriesId,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
printJson(result);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function searchEntities(context, args) {
|
|
95
|
+
const options = parseArgVector(args).options;
|
|
96
|
+
const query = new URLSearchParams();
|
|
97
|
+
if (options.query) query.set("query", options.query);
|
|
98
|
+
if (options.taxId) query.set("taxId", options.taxId);
|
|
99
|
+
if (options.limit) query.set("limit", options.limit);
|
|
100
|
+
const result = await platformApiCall(context, `/api/agents/v1/platform/entities/search?${query.toString()}`, {
|
|
101
|
+
method: "GET",
|
|
102
|
+
});
|
|
103
|
+
printJson(result);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function listContacts(context, args) {
|
|
107
|
+
const options = parseArgVector(args).options;
|
|
108
|
+
const query = new URLSearchParams();
|
|
109
|
+
if (options.entityId) query.set("entityId", options.entityId);
|
|
110
|
+
if (options.name) query.set("name", options.name);
|
|
111
|
+
const result = await platformApiCall(context, `/api/agents/v1/platform/contacts/list?${query.toString()}`, {
|
|
112
|
+
method: "GET",
|
|
113
|
+
});
|
|
114
|
+
printJson(result);
|
|
115
|
+
}
|
|
116
|
+
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
+
import { parseArgVector, asBool } from "../lib/args.js";
|
|
5
|
+
import { readAuthProfile } from "../lib/auth-store.js";
|
|
6
|
+
import { readCliConfig } from "../lib/config-store.js";
|
|
7
|
+
|
|
8
|
+
export async function runSession(args) {
|
|
9
|
+
const config = await parseConfig(args);
|
|
10
|
+
const headers = buildHeaders(config);
|
|
11
|
+
const sessionId = config.sessionId ?? await startSession(config, headers);
|
|
12
|
+
log(`session: ${sessionId}`);
|
|
13
|
+
|
|
14
|
+
const submit = await callApi(
|
|
15
|
+
`${config.runtimeUrl}/api/agents/v1/sessions/${encodeURIComponent(sessionId)}/turns`,
|
|
16
|
+
{
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers,
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
message: { content: config.message },
|
|
21
|
+
idempotencyKey: config.idempotencyKey,
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
{ retries: 2, retryDelayMs: 400 }
|
|
25
|
+
);
|
|
26
|
+
if (!submit.ok) {
|
|
27
|
+
throw new Error(`submitTurn failed: ${submit.error} ${submit.message ?? ""}`.trim());
|
|
28
|
+
}
|
|
29
|
+
log(`turn submitted (idempotencyKey=${config.idempotencyKey})`);
|
|
30
|
+
|
|
31
|
+
const startedAt = Date.now();
|
|
32
|
+
while (true) {
|
|
33
|
+
if (Date.now() - startedAt > config.timeoutMs) {
|
|
34
|
+
throw new Error(`timeout after ${config.timeoutMs}ms while waiting for session completion`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const session = await getSession(config, headers, sessionId);
|
|
38
|
+
const status = session.status ?? "unknown";
|
|
39
|
+
log(`status: ${status}`);
|
|
40
|
+
|
|
41
|
+
if (status === "completed") {
|
|
42
|
+
if (session.latestReply) printReply(session.latestReply);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (status === "failed" || status === "cancelled") {
|
|
46
|
+
throw new Error(`session finished with status=${status}${session.latestReply ? ` reply="${session.latestReply}"` : ""}`);
|
|
47
|
+
}
|
|
48
|
+
if (status === "awaiting_approval" || status === "awaiting_input" || status === "awaiting_auth") {
|
|
49
|
+
if (config.nonInteractive) {
|
|
50
|
+
throw new Error(`session requires response (${status}) but --non-interactive is set`);
|
|
51
|
+
}
|
|
52
|
+
await respondToSignal(config, headers, sessionId, status, session.signals ?? [], session.latestReply);
|
|
53
|
+
}
|
|
54
|
+
await sleep(config.pollMs);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function parseConfig(args) {
|
|
59
|
+
const { options } = parseArgVector(args);
|
|
60
|
+
const defaults = await readCliConfig();
|
|
61
|
+
const profile = options.profile ?? defaults.defaultProfile ?? "default";
|
|
62
|
+
const stored = await readAuthProfile(profile);
|
|
63
|
+
|
|
64
|
+
const runtimeUrl = (options.runtimeUrl ?? process.env.NAIA_RUNTIME_URL ?? defaults.runtimeUrl ?? "http://localhost:4310").replace(/\/+$/, "");
|
|
65
|
+
const organizationId = options.organizationId ?? process.env.NAIA_ORGANIZATION_ID ?? stored?.organizationId ?? "";
|
|
66
|
+
const memberId = options.memberId ?? process.env.NAIA_MEMBER_ID ?? stored?.memberId ?? "";
|
|
67
|
+
const bearerToken = options.token ?? process.env.NAIA_BEARER_TOKEN ?? stored?.token;
|
|
68
|
+
const apiKey = options.apiKey ?? process.env.NAIA_API_KEY;
|
|
69
|
+
const message = options.message ?? "";
|
|
70
|
+
|
|
71
|
+
if (!organizationId || !memberId || !message) {
|
|
72
|
+
throw new Error("missing required args: --organization-id --member-id --message (or run naia auth login first)");
|
|
73
|
+
}
|
|
74
|
+
if (!bearerToken && !apiKey) {
|
|
75
|
+
throw new Error("missing credentials: pass --token or --api-key");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
runtimeUrl,
|
|
80
|
+
organizationId,
|
|
81
|
+
memberId,
|
|
82
|
+
bearerToken,
|
|
83
|
+
apiKey,
|
|
84
|
+
message,
|
|
85
|
+
agentSlug: options.agentSlug ?? "naia",
|
|
86
|
+
channel: options.channel ?? "cli",
|
|
87
|
+
clientType: options.clientType ?? "codex-cli",
|
|
88
|
+
sessionId: options.sessionId,
|
|
89
|
+
pollMs: Number.parseInt(options.pollMs ?? "1200", 10),
|
|
90
|
+
timeoutMs: Number.parseInt(options.timeoutMs ?? "180000", 10),
|
|
91
|
+
nonInteractive: asBool(options.nonInteractive),
|
|
92
|
+
autoApprove: asBool(options.autoApprove),
|
|
93
|
+
idempotencyKey: options.idempotencyKey ?? `cli-${randomUUID()}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildHeaders(config) {
|
|
98
|
+
const headers = {
|
|
99
|
+
"content-type": "application/json",
|
|
100
|
+
"x-naia-organization-id": config.organizationId,
|
|
101
|
+
"x-naia-member-id": config.memberId,
|
|
102
|
+
};
|
|
103
|
+
if (config.bearerToken) {
|
|
104
|
+
headers.authorization = `Bearer ${config.bearerToken}`;
|
|
105
|
+
} else if (config.apiKey) {
|
|
106
|
+
headers["x-api-key"] = config.apiKey;
|
|
107
|
+
}
|
|
108
|
+
return headers;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function startSession(config, headers) {
|
|
112
|
+
const response = await callApi(
|
|
113
|
+
`${config.runtimeUrl}/api/agents/v1/sessions`,
|
|
114
|
+
{
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers,
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
agentSlug: config.agentSlug,
|
|
119
|
+
channel: config.channel,
|
|
120
|
+
clientType: config.clientType,
|
|
121
|
+
}),
|
|
122
|
+
},
|
|
123
|
+
{ retries: 2, retryDelayMs: 400 }
|
|
124
|
+
);
|
|
125
|
+
if (!response.ok || !response.agentSessionId) {
|
|
126
|
+
throw new Error(`startSession failed: ${response.error} ${response.message ?? ""}`.trim());
|
|
127
|
+
}
|
|
128
|
+
return response.agentSessionId;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function getSession(config, headers, sessionId) {
|
|
132
|
+
const response = await callApi(
|
|
133
|
+
`${config.runtimeUrl}/api/agents/v1/sessions/${encodeURIComponent(sessionId)}`,
|
|
134
|
+
{ method: "GET", headers },
|
|
135
|
+
{ retries: 1, retryDelayMs: 200 }
|
|
136
|
+
);
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
throw new Error(`getSession failed: ${response.error} ${response.message ?? ""}`.trim());
|
|
139
|
+
}
|
|
140
|
+
return response;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function respondToSignal(config, headers, sessionId, status, signals, latestReply) {
|
|
144
|
+
const primarySignal = signals[0]?.type
|
|
145
|
+
?? (status === "awaiting_approval" ? "approve_plan" : status === "awaiting_input" ? "clarify" : "auth");
|
|
146
|
+
|
|
147
|
+
if (config.autoApprove && primarySignal === "approve_plan") {
|
|
148
|
+
await postRespond(config, headers, sessionId, {
|
|
149
|
+
signal: "approve_plan",
|
|
150
|
+
decision: "approve",
|
|
151
|
+
value: "Approved from CLI auto-approve",
|
|
152
|
+
});
|
|
153
|
+
log("auto-approved pending plan");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (latestReply) printReply(latestReply);
|
|
158
|
+
|
|
159
|
+
const rl = createInterface({ input, output });
|
|
160
|
+
try {
|
|
161
|
+
if (primarySignal === "approve_plan") {
|
|
162
|
+
const answer = (await rl.question("Approval required. Type 'approve' or 'deny': ")).trim().toLowerCase();
|
|
163
|
+
if (answer !== "approve" && answer !== "deny") {
|
|
164
|
+
throw new Error("invalid decision, expected approve|deny");
|
|
165
|
+
}
|
|
166
|
+
await postRespond(config, headers, sessionId, {
|
|
167
|
+
signal: "approve_plan",
|
|
168
|
+
decision: answer,
|
|
169
|
+
value: answer === "approve" ? "Approved from CLI" : "Denied from CLI",
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (primarySignal === "clarify" || status === "awaiting_input") {
|
|
174
|
+
const answer = (await rl.question("Input required. Write response: ")).trim();
|
|
175
|
+
if (!answer) throw new Error("empty clarify response");
|
|
176
|
+
await postRespond(config, headers, sessionId, {
|
|
177
|
+
signal: "clarify",
|
|
178
|
+
decision: "provide_input",
|
|
179
|
+
value: answer,
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const answer = (await rl.question("Auth/selection required. Type 'resume' or 'stop': ")).trim().toLowerCase();
|
|
184
|
+
if (answer !== "resume" && answer !== "stop") {
|
|
185
|
+
throw new Error("invalid decision, expected resume|stop");
|
|
186
|
+
}
|
|
187
|
+
await postRespond(config, headers, sessionId, {
|
|
188
|
+
signal: answer,
|
|
189
|
+
decision: answer,
|
|
190
|
+
value: answer === "resume" ? "Continue from CLI" : "Stop from CLI",
|
|
191
|
+
});
|
|
192
|
+
} finally {
|
|
193
|
+
rl.close();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function postRespond(config, headers, sessionId, payload) {
|
|
198
|
+
const response = await callApi(
|
|
199
|
+
`${config.runtimeUrl}/api/agents/v1/sessions/${encodeURIComponent(sessionId)}/respond`,
|
|
200
|
+
{
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers,
|
|
203
|
+
body: JSON.stringify(payload),
|
|
204
|
+
},
|
|
205
|
+
{ retries: 1, retryDelayMs: 200 }
|
|
206
|
+
);
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
throw new Error(`respond failed: ${response.error} ${response.message ?? ""}`.trim());
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function callApi(url, init, retry) {
|
|
213
|
+
let attempt = 0;
|
|
214
|
+
while (true) {
|
|
215
|
+
try {
|
|
216
|
+
const response = await fetch(url, init);
|
|
217
|
+
const json = await response.json();
|
|
218
|
+
if (response.ok) return json;
|
|
219
|
+
if (attempt < retry.retries && (response.status >= 500 || json?.retryable)) {
|
|
220
|
+
attempt += 1;
|
|
221
|
+
await sleep(retry.retryDelayMs * attempt);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
return json;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (attempt < retry.retries) {
|
|
227
|
+
attempt += 1;
|
|
228
|
+
await sleep(retry.retryDelayMs * attempt);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
return { ok: false, error: "runtime_error", message: error instanceof Error ? error.message : "Unknown network error" };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function printReply(reply) {
|
|
237
|
+
console.log("\n--- latest reply ---");
|
|
238
|
+
console.log(reply);
|
|
239
|
+
console.log("--------------------\n");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function log(message) {
|
|
243
|
+
const timestamp = new Date().toISOString();
|
|
244
|
+
console.log(`[${timestamp}] ${message}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function sleep(ms) {
|
|
248
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
249
|
+
}
|
|
250
|
+
|
package/src/lib/args.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function parseArgVector(args) {
|
|
2
|
+
const options = {};
|
|
3
|
+
const positionals = [];
|
|
4
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
5
|
+
const token = args[index];
|
|
6
|
+
if (!token.startsWith("--")) {
|
|
7
|
+
positionals.push(token);
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const key = token.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
11
|
+
const next = args[index + 1];
|
|
12
|
+
if (next && !next.startsWith("--")) {
|
|
13
|
+
options[key] = next;
|
|
14
|
+
index += 1;
|
|
15
|
+
} else {
|
|
16
|
+
options[key] = "true";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { options, positionals };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function asBool(input) {
|
|
23
|
+
return input === "true" || input === "1" || input === "yes";
|
|
24
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
export function getAuthStorePath() {
|
|
6
|
+
return join(homedir(), ".config", "naia", "cli-auth.json");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function readAuthProfile(profile = "default") {
|
|
10
|
+
const filePath = getAuthStorePath();
|
|
11
|
+
try {
|
|
12
|
+
const raw = await readFile(filePath, "utf8");
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
const resolvedProfile = profile === "default"
|
|
15
|
+
? (parsed.defaultProfile || "default")
|
|
16
|
+
: profile;
|
|
17
|
+
const item = parsed.profiles?.[resolvedProfile];
|
|
18
|
+
if (!item?.token || !item.organizationId || !item.memberId) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return item;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function writeAuthProfile(input) {
|
|
28
|
+
const filePath = getAuthStorePath();
|
|
29
|
+
const profile = input.profile ?? "default";
|
|
30
|
+
const existing = await readStore(filePath);
|
|
31
|
+
const next = {
|
|
32
|
+
defaultProfile: existing?.defaultProfile || profile,
|
|
33
|
+
profiles: {
|
|
34
|
+
...(existing?.profiles ?? {}),
|
|
35
|
+
[profile]: input.data,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
39
|
+
await writeFile(filePath, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function readStore(filePath) {
|
|
43
|
+
try {
|
|
44
|
+
const raw = await readFile(filePath, "utf8");
|
|
45
|
+
return JSON.parse(raw);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|