ignis-agent-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 +41 -0
- package/dist/index.js +926 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Ignis
|
|
2
|
+
|
|
3
|
+
Node CLI for the Ultra-Mai Agent V2 CLI facade.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g ignis-agent-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configure
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
ignis login --base-url http://127.0.0.1:57988 --token <cli_token>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This stores config in `~/.ignis/config.json` and reuses the current working directory as the default session context.
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
ignis ask "帮我继续这个话题"
|
|
23
|
+
ignis ask --prompt-file prompt.md --attach image.png --json
|
|
24
|
+
ignis ask --file-id image_123.png "继续分析这张图"
|
|
25
|
+
ignis history
|
|
26
|
+
ignis skills --query video
|
|
27
|
+
ignis status <turn_id>
|
|
28
|
+
ignis resume <turn_id> --answer "继续"
|
|
29
|
+
ignis cancel <turn_id>
|
|
30
|
+
ignis upload ./demo.png
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Auth
|
|
34
|
+
|
|
35
|
+
The CLI sends:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
Authorization: Bearer <cli_token>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
It expects the backend `/api/cli/*` routes to be enabled and backed by the `cli_tokens` table.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command10 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/ask.ts
|
|
7
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
8
|
+
import crypto from "crypto";
|
|
9
|
+
import { Command as Command2, Option as Option2 } from "commander";
|
|
10
|
+
|
|
11
|
+
// src/client.ts
|
|
12
|
+
import { readFile } from "fs/promises";
|
|
13
|
+
import path from "path";
|
|
14
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "interrupted", "failed", "cancelled"]);
|
|
15
|
+
var IgnisCliError = class extends Error {
|
|
16
|
+
code;
|
|
17
|
+
statusCode;
|
|
18
|
+
details;
|
|
19
|
+
constructor(message, options) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "IgnisCliError";
|
|
22
|
+
this.code = options?.code ?? "cli_error";
|
|
23
|
+
this.statusCode = options?.statusCode;
|
|
24
|
+
this.details = options?.details;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var IgnisClient = class {
|
|
28
|
+
baseUrl;
|
|
29
|
+
token;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
32
|
+
this.token = options.token;
|
|
33
|
+
}
|
|
34
|
+
async createTurn(payload) {
|
|
35
|
+
return this.requestJson("/api/cli/turns", {
|
|
36
|
+
method: "POST",
|
|
37
|
+
body: JSON.stringify(payload),
|
|
38
|
+
headers: {
|
|
39
|
+
"Content-Type": "application/json"
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async getTurn(turnId) {
|
|
44
|
+
return this.requestJson(`/api/cli/turns/${encodeURIComponent(turnId)}`, {
|
|
45
|
+
method: "GET"
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async getLatestSessionTurn(sessionId) {
|
|
49
|
+
return this.requestJson(`/api/cli/sessions/${encodeURIComponent(sessionId)}/status`, {
|
|
50
|
+
method: "GET"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async getSessionHistory(sessionId, options) {
|
|
54
|
+
const params = new URLSearchParams();
|
|
55
|
+
if (typeof options?.limit === "number") {
|
|
56
|
+
params.set("limit", String(options.limit));
|
|
57
|
+
}
|
|
58
|
+
if (typeof options?.offset === "number") {
|
|
59
|
+
params.set("offset", String(options.offset));
|
|
60
|
+
}
|
|
61
|
+
if (options?.view) {
|
|
62
|
+
params.set("view", options.view);
|
|
63
|
+
}
|
|
64
|
+
const query = params.toString();
|
|
65
|
+
const suffix = query ? `?${query}` : "";
|
|
66
|
+
return this.requestJson(
|
|
67
|
+
`/api/cli/sessions/${encodeURIComponent(sessionId)}/history${suffix}`,
|
|
68
|
+
{ method: "GET" }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
async getSkills(options) {
|
|
72
|
+
const params = new URLSearchParams();
|
|
73
|
+
if (options?.query) {
|
|
74
|
+
params.set("query", options.query);
|
|
75
|
+
}
|
|
76
|
+
if (typeof options?.limit === "number") {
|
|
77
|
+
params.set("limit", String(options.limit));
|
|
78
|
+
}
|
|
79
|
+
if (typeof options?.offset === "number") {
|
|
80
|
+
params.set("offset", String(options.offset));
|
|
81
|
+
}
|
|
82
|
+
const query = params.toString();
|
|
83
|
+
const suffix = query ? `?${query}` : "";
|
|
84
|
+
return this.requestJson(`/api/cli/skills${suffix}`, {
|
|
85
|
+
method: "GET"
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async resumeTurn(turnId, payload) {
|
|
89
|
+
return this.requestJson(`/api/cli/turns/${encodeURIComponent(turnId)}/resume`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
body: JSON.stringify(payload),
|
|
92
|
+
headers: {
|
|
93
|
+
"Content-Type": "application/json"
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async cancelTurn(turnId) {
|
|
98
|
+
return this.requestJson(`/api/cli/turns/${encodeURIComponent(turnId)}/cancel`, {
|
|
99
|
+
method: "POST"
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async uploadFile(filePath) {
|
|
103
|
+
const content = await readFile(filePath);
|
|
104
|
+
const blob = new Blob([content]);
|
|
105
|
+
const form = new FormData();
|
|
106
|
+
form.append("file", blob, path.basename(filePath));
|
|
107
|
+
return this.requestJson("/api/cli/files", {
|
|
108
|
+
method: "POST",
|
|
109
|
+
body: form
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async waitForTurn(turn, options) {
|
|
113
|
+
const startedAt = Date.now();
|
|
114
|
+
let current = turn;
|
|
115
|
+
while (!TERMINAL_STATUSES.has(current.status)) {
|
|
116
|
+
if (Date.now() - startedAt >= options.timeoutMs) {
|
|
117
|
+
throw new IgnisCliError(
|
|
118
|
+
`Timed out waiting for turn ${current.turn_id}. Last status: ${current.status}`,
|
|
119
|
+
{ code: "timeout" }
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const delayMs = Math.max(250, Math.min(Number(current.poll_after_ms) || 1e3, 5e3));
|
|
123
|
+
await sleep(delayMs);
|
|
124
|
+
current = await this.getTurn(current.turn_id);
|
|
125
|
+
options.onPoll?.(current);
|
|
126
|
+
}
|
|
127
|
+
return current;
|
|
128
|
+
}
|
|
129
|
+
async requestJson(pathname, init) {
|
|
130
|
+
const response = await fetch(`${this.baseUrl}${pathname}`, {
|
|
131
|
+
...init,
|
|
132
|
+
headers: {
|
|
133
|
+
Accept: "application/json",
|
|
134
|
+
Authorization: `Bearer ${this.token}`,
|
|
135
|
+
...init.headers ?? {}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
const payload = await parseResponse(response);
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
const message = getObjectString(payload, "message") ?? getObjectString(payload, "detail") ?? `Request failed with status ${response.status}`;
|
|
141
|
+
const code = getObjectString(payload, "error") ?? getObjectString(payload, "code") ?? "http_error";
|
|
142
|
+
throw new IgnisCliError(message, {
|
|
143
|
+
code,
|
|
144
|
+
statusCode: response.status,
|
|
145
|
+
details: payload
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return payload;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
async function parseResponse(response) {
|
|
152
|
+
const text = await response.text();
|
|
153
|
+
if (!text) {
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(text);
|
|
158
|
+
} catch {
|
|
159
|
+
return { message: text };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function getObjectString(payload, key) {
|
|
163
|
+
if (!payload || typeof payload !== "object") {
|
|
164
|
+
return void 0;
|
|
165
|
+
}
|
|
166
|
+
const value = payload[key];
|
|
167
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
168
|
+
}
|
|
169
|
+
function sleep(ms) {
|
|
170
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/config.ts
|
|
174
|
+
import { chmod, mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
175
|
+
import os from "os";
|
|
176
|
+
import path2 from "path";
|
|
177
|
+
var CONFIG_DIR = path2.join(os.homedir(), ".ignis");
|
|
178
|
+
var CONFIG_PATH = path2.join(CONFIG_DIR, "config.json");
|
|
179
|
+
var LEGACY_CONFIG_PATH = path2.join(os.homedir(), ".mai", "config.json");
|
|
180
|
+
var DEFAULT_CONFIG = {
|
|
181
|
+
sessions: {}
|
|
182
|
+
};
|
|
183
|
+
function normalizeBaseUrl(baseUrl) {
|
|
184
|
+
return baseUrl.trim().replace(/\/+$/, "");
|
|
185
|
+
}
|
|
186
|
+
function getConfigPath() {
|
|
187
|
+
return CONFIG_PATH;
|
|
188
|
+
}
|
|
189
|
+
async function loadConfig() {
|
|
190
|
+
try {
|
|
191
|
+
const raw = await readConfigFile();
|
|
192
|
+
const parsed = JSON.parse(raw);
|
|
193
|
+
return {
|
|
194
|
+
baseUrl: typeof parsed.baseUrl === "string" ? normalizeBaseUrl(parsed.baseUrl) : void 0,
|
|
195
|
+
cliToken: typeof parsed.cliToken === "string" ? parsed.cliToken.trim() : void 0,
|
|
196
|
+
sessions: parsed.sessions && typeof parsed.sessions === "object" ? parsed.sessions : {}
|
|
197
|
+
};
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (error.code === "ENOENT") {
|
|
200
|
+
return { ...DEFAULT_CONFIG };
|
|
201
|
+
}
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function readConfigFile() {
|
|
206
|
+
try {
|
|
207
|
+
return await readFile2(CONFIG_PATH, "utf8");
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error.code !== "ENOENT") {
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return readFile2(LEGACY_CONFIG_PATH, "utf8");
|
|
214
|
+
}
|
|
215
|
+
async function saveConfig(config) {
|
|
216
|
+
await mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
217
|
+
await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}
|
|
218
|
+
`, {
|
|
219
|
+
encoding: "utf8",
|
|
220
|
+
mode: 384
|
|
221
|
+
});
|
|
222
|
+
await chmod(CONFIG_PATH, 384);
|
|
223
|
+
}
|
|
224
|
+
function resolveBaseUrl(config, override) {
|
|
225
|
+
const value = override ?? process.env.IGNIS_BASE_URL ?? process.env.MAI_BASE_URL ?? config.baseUrl;
|
|
226
|
+
if (!value || !value.trim()) {
|
|
227
|
+
throw new Error("Missing base URL. Run `ignis login --base-url ... --token ...` or pass --base-url.");
|
|
228
|
+
}
|
|
229
|
+
return normalizeBaseUrl(value);
|
|
230
|
+
}
|
|
231
|
+
function resolveCliToken(config, override) {
|
|
232
|
+
const value = override ?? process.env.IGNIS_CLI_TOKEN ?? process.env.MAI_CLI_TOKEN ?? config.cliToken;
|
|
233
|
+
if (!value || !value.trim()) {
|
|
234
|
+
throw new Error("Missing CLI token. Run `ignis login --base-url ... --token ...` or pass --token.");
|
|
235
|
+
}
|
|
236
|
+
return value.trim();
|
|
237
|
+
}
|
|
238
|
+
function getContextKey(cwd = process.cwd()) {
|
|
239
|
+
return path2.resolve(cwd);
|
|
240
|
+
}
|
|
241
|
+
function getSessionForContext(config, cwd = process.cwd()) {
|
|
242
|
+
return config.sessions[getContextKey(cwd)];
|
|
243
|
+
}
|
|
244
|
+
function setSessionForContext(config, sessionId, cwd = process.cwd()) {
|
|
245
|
+
const next = {
|
|
246
|
+
...config,
|
|
247
|
+
sessions: {
|
|
248
|
+
...config.sessions,
|
|
249
|
+
[getContextKey(cwd)]: sessionId
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
return next;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/output.ts
|
|
256
|
+
function printJson(value) {
|
|
257
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
258
|
+
`);
|
|
259
|
+
}
|
|
260
|
+
function printTurnHuman(turn) {
|
|
261
|
+
if (turn.status === "completed") {
|
|
262
|
+
const text = turn.assistant_message?.text?.trim() ?? "";
|
|
263
|
+
if (text) {
|
|
264
|
+
process.stdout.write(`${text}
|
|
265
|
+
`);
|
|
266
|
+
}
|
|
267
|
+
printTurnSummary(turn);
|
|
268
|
+
process.stderr.write(`turn=${turn.turn_id} session=${turn.session_id ?? "-"} status=completed
|
|
269
|
+
`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (turn.status === "interrupted") {
|
|
273
|
+
const partial = turn.assistant_message?.text?.trim() ?? "";
|
|
274
|
+
if (partial) {
|
|
275
|
+
process.stdout.write(`${partial}
|
|
276
|
+
`);
|
|
277
|
+
}
|
|
278
|
+
printTurnSummary(turn);
|
|
279
|
+
process.stderr.write(`turn=${turn.turn_id} session=${turn.session_id ?? "-"} status=interrupted
|
|
280
|
+
`);
|
|
281
|
+
process.stderr.write(`${JSON.stringify(turn.interrupt ?? {}, null, 2)}
|
|
282
|
+
`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (turn.status === "cancelled") {
|
|
286
|
+
process.stderr.write(`turn=${turn.turn_id} session=${turn.session_id ?? "-"} status=cancelled
|
|
287
|
+
`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (turn.status === "failed") {
|
|
291
|
+
const message = turn.error?.message ?? "Turn failed";
|
|
292
|
+
process.stderr.write(`turn=${turn.turn_id} session=${turn.session_id ?? "-"} status=failed: ${message}
|
|
293
|
+
`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
process.stderr.write(`turn=${turn.turn_id} session=${turn.session_id ?? "-"} status=${turn.status}
|
|
297
|
+
`);
|
|
298
|
+
}
|
|
299
|
+
function printUploadHuman(file) {
|
|
300
|
+
process.stdout.write(`${file.file_id}
|
|
301
|
+
`);
|
|
302
|
+
process.stderr.write(`uploaded ${file.original_filename} -> ${file.file_id}
|
|
303
|
+
`);
|
|
304
|
+
}
|
|
305
|
+
function printLoginHuman(configPath) {
|
|
306
|
+
process.stderr.write(`saved config to ${configPath}
|
|
307
|
+
`);
|
|
308
|
+
}
|
|
309
|
+
function printSkillsHuman(response) {
|
|
310
|
+
const skills = response.skills ?? [];
|
|
311
|
+
skills.forEach((skill, index) => {
|
|
312
|
+
const source = skill.source?.trim() || "unknown";
|
|
313
|
+
const enabled = skill.enabled === false ? "disabled" : "enabled";
|
|
314
|
+
process.stdout.write(`[${index + 1}] ${skill.skill_name} (${skill.skill_id}) ${source} ${enabled}
|
|
315
|
+
`);
|
|
316
|
+
const title = skill.title?.trim();
|
|
317
|
+
if (title) {
|
|
318
|
+
process.stdout.write(`title: ${title}
|
|
319
|
+
`);
|
|
320
|
+
}
|
|
321
|
+
const summary = skill.short_description?.trim() || skill.description?.trim() || "";
|
|
322
|
+
if (summary) {
|
|
323
|
+
process.stdout.write(`${summary}
|
|
324
|
+
`);
|
|
325
|
+
}
|
|
326
|
+
const tags = (skill.tags ?? []).filter((value) => value.trim());
|
|
327
|
+
if (tags.length > 0) {
|
|
328
|
+
process.stdout.write(`tags: ${tags.join(", ")}
|
|
329
|
+
`);
|
|
330
|
+
}
|
|
331
|
+
process.stdout.write("\n");
|
|
332
|
+
});
|
|
333
|
+
process.stderr.write(
|
|
334
|
+
`returned=${skills.length} total=${response.count} query=${response.query || "-"}
|
|
335
|
+
`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
function printHistoryHuman(history) {
|
|
339
|
+
const messages = history.messages ?? [];
|
|
340
|
+
messages.forEach((message, index) => {
|
|
341
|
+
const header = `[${index + 1}] ${labelForMessage(message)}`;
|
|
342
|
+
process.stdout.write(`${header}
|
|
343
|
+
`);
|
|
344
|
+
const body = renderMessageBody(message);
|
|
345
|
+
if (body) {
|
|
346
|
+
process.stdout.write(`${body}
|
|
347
|
+
`);
|
|
348
|
+
}
|
|
349
|
+
process.stdout.write("\n");
|
|
350
|
+
});
|
|
351
|
+
process.stderr.write(
|
|
352
|
+
`session=${history.session_id} returned=${messages.length} total=${history.total}
|
|
353
|
+
`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
function printTurnHistoryHuman(history) {
|
|
357
|
+
const turns = history.turns ?? [];
|
|
358
|
+
turns.forEach((turn, index) => {
|
|
359
|
+
const header = `[${index + 1}] turn ${turn.turn_id} status=${turn.status ?? "unknown"}`;
|
|
360
|
+
process.stdout.write(`${header}
|
|
361
|
+
`);
|
|
362
|
+
const canvasId = turn.canvas_id?.trim();
|
|
363
|
+
if (canvasId) {
|
|
364
|
+
process.stdout.write(`canvas_id=${canvasId}
|
|
365
|
+
`);
|
|
366
|
+
}
|
|
367
|
+
const inputText = turn.input?.message?.trim();
|
|
368
|
+
if (inputText) {
|
|
369
|
+
process.stdout.write(`user: ${inputText}
|
|
370
|
+
`);
|
|
371
|
+
}
|
|
372
|
+
const inputFileIds = (turn.input?.file_ids ?? []).filter((value) => value.trim());
|
|
373
|
+
if (inputFileIds.length > 0) {
|
|
374
|
+
process.stdout.write(`[input_file_ids] ${inputFileIds.join(", ")}
|
|
375
|
+
`);
|
|
376
|
+
}
|
|
377
|
+
const toolNames = (turn.tool_summary?.names ?? []).filter((value) => value.trim());
|
|
378
|
+
if (toolNames.length > 0) {
|
|
379
|
+
process.stdout.write(`[tools] ${toolNames.join(", ")}
|
|
380
|
+
`);
|
|
381
|
+
}
|
|
382
|
+
const artifactIds = (turn.artifact_summary?.file_ids ?? []).filter((value) => value.trim());
|
|
383
|
+
if (artifactIds.length > 0) {
|
|
384
|
+
process.stdout.write(`[artifact_file_ids] ${artifactIds.join(", ")}
|
|
385
|
+
`);
|
|
386
|
+
}
|
|
387
|
+
const assistantText = turn.assistant_message?.text?.trim();
|
|
388
|
+
if (assistantText) {
|
|
389
|
+
process.stdout.write(`assistant: ${assistantText}
|
|
390
|
+
`);
|
|
391
|
+
}
|
|
392
|
+
if (turn.error && Object.keys(turn.error).length > 0) {
|
|
393
|
+
process.stdout.write(`[error] ${JSON.stringify(turn.error)}
|
|
394
|
+
`);
|
|
395
|
+
}
|
|
396
|
+
if (turn.interrupt && Object.keys(turn.interrupt).length > 0) {
|
|
397
|
+
process.stdout.write(`[interrupt] ${JSON.stringify(turn.interrupt)}
|
|
398
|
+
`);
|
|
399
|
+
}
|
|
400
|
+
process.stdout.write("\n");
|
|
401
|
+
});
|
|
402
|
+
process.stderr.write(
|
|
403
|
+
`session=${history.session_id} canvas=${history.canvas_id ?? "-"} returned=${turns.length} total=${history.total}
|
|
404
|
+
`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
function labelForMessage(message) {
|
|
408
|
+
if (message.type === "user_message") {
|
|
409
|
+
return "user";
|
|
410
|
+
}
|
|
411
|
+
if (message.type === "agent_text") {
|
|
412
|
+
return "assistant";
|
|
413
|
+
}
|
|
414
|
+
if (message.type === "interrupt_question") {
|
|
415
|
+
return "interrupt";
|
|
416
|
+
}
|
|
417
|
+
if (message.type === "interrupt_response") {
|
|
418
|
+
return "resume";
|
|
419
|
+
}
|
|
420
|
+
if (message.type === "agent_attachment") {
|
|
421
|
+
return "attachment";
|
|
422
|
+
}
|
|
423
|
+
if (message.type === "system_error") {
|
|
424
|
+
return "error";
|
|
425
|
+
}
|
|
426
|
+
return message.type;
|
|
427
|
+
}
|
|
428
|
+
function renderMessageBody(message) {
|
|
429
|
+
const lines = [];
|
|
430
|
+
const content = message.content?.trim();
|
|
431
|
+
if (content) {
|
|
432
|
+
lines.push(content);
|
|
433
|
+
}
|
|
434
|
+
const references = Array.isArray(message.metadata?.references) ? message.metadata?.references : [];
|
|
435
|
+
const referenceIds = references.map((reference) => String(reference.id ?? "").trim()).filter(Boolean);
|
|
436
|
+
if (referenceIds.length > 0) {
|
|
437
|
+
lines.push(`[file_ids] ${referenceIds.join(", ")}`);
|
|
438
|
+
}
|
|
439
|
+
if (message.attachment_data) {
|
|
440
|
+
const fileId = String(message.attachment_data.file_id ?? "").trim();
|
|
441
|
+
const caption = String(message.attachment_data.caption ?? "").trim();
|
|
442
|
+
const attachmentLine = [fileId && `file_id=${fileId}`, caption && `caption=${caption}`].filter(Boolean).join(" ");
|
|
443
|
+
if (attachmentLine) {
|
|
444
|
+
lines.push(attachmentLine);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (message.tool_data) {
|
|
448
|
+
const toolName = String(message.tool_data.tool_name ?? "").trim();
|
|
449
|
+
const status = String(message.tool_data.status ?? "").trim();
|
|
450
|
+
const toolId = String(message.tool_data.tool_id ?? "").trim();
|
|
451
|
+
const toolMessage = String(message.tool_data.message ?? "").trim();
|
|
452
|
+
const outputFileIds = Array.isArray(message.tool_data.output_file_ids) ? message.tool_data.output_file_ids.map((value) => String(value).trim()).filter(Boolean) : [];
|
|
453
|
+
const toolLine = [
|
|
454
|
+
toolName && `tool=${toolName}`,
|
|
455
|
+
status && `status=${status}`,
|
|
456
|
+
toolId && `tool_id=${toolId}`,
|
|
457
|
+
toolMessage && `message=${toolMessage}`
|
|
458
|
+
].filter(Boolean).join(" ");
|
|
459
|
+
if (toolLine) {
|
|
460
|
+
lines.push(toolLine);
|
|
461
|
+
}
|
|
462
|
+
if (outputFileIds.length > 0) {
|
|
463
|
+
lines.push(`[output_file_ids] ${outputFileIds.join(", ")}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (lines.length === 0) {
|
|
467
|
+
const metadata = message.metadata && Object.keys(message.metadata).length > 0 ? JSON.stringify(message.metadata, null, 2) : "";
|
|
468
|
+
if (metadata) {
|
|
469
|
+
lines.push(metadata);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return lines.join("\n");
|
|
473
|
+
}
|
|
474
|
+
function printTurnSummary(turn) {
|
|
475
|
+
const canvasId = turn.canvas_id?.trim();
|
|
476
|
+
if (canvasId) {
|
|
477
|
+
process.stderr.write(`canvas=${canvasId}
|
|
478
|
+
`);
|
|
479
|
+
}
|
|
480
|
+
const inputFileIds = (turn.input?.file_ids ?? []).filter((value) => value.trim());
|
|
481
|
+
if (inputFileIds.length > 0) {
|
|
482
|
+
process.stderr.write(`input_file_ids=${inputFileIds.join(",")}
|
|
483
|
+
`);
|
|
484
|
+
}
|
|
485
|
+
const toolNames = (turn.tool_summary?.names ?? []).filter((value) => value.trim());
|
|
486
|
+
if (toolNames.length > 0) {
|
|
487
|
+
process.stderr.write(`tools=${toolNames.join(",")}
|
|
488
|
+
`);
|
|
489
|
+
}
|
|
490
|
+
const artifactIds = (turn.artifact_summary?.file_ids ?? []).filter((value) => value.trim());
|
|
491
|
+
if (artifactIds.length > 0) {
|
|
492
|
+
process.stderr.write(`artifact_file_ids=${artifactIds.join(",")}
|
|
493
|
+
`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/commands/common.ts
|
|
498
|
+
import { Option } from "commander";
|
|
499
|
+
function addRuntimeOptions(command, options) {
|
|
500
|
+
command.addOption(new Option("--base-url <url>", "Override configured backend base URL"));
|
|
501
|
+
command.addOption(new Option("--token <token>", "Override configured CLI token"));
|
|
502
|
+
if (options?.includeJson !== false) {
|
|
503
|
+
command.addOption(new Option("--json", "Print machine-readable JSON output"));
|
|
504
|
+
}
|
|
505
|
+
return command;
|
|
506
|
+
}
|
|
507
|
+
async function createClient(options) {
|
|
508
|
+
const config = await loadConfig();
|
|
509
|
+
const client = new IgnisClient({
|
|
510
|
+
baseUrl: resolveBaseUrl(config, options.baseUrl),
|
|
511
|
+
token: resolveCliToken(config, options.token)
|
|
512
|
+
});
|
|
513
|
+
return { client, config };
|
|
514
|
+
}
|
|
515
|
+
function collectValues(value, previous) {
|
|
516
|
+
return [...previous, value];
|
|
517
|
+
}
|
|
518
|
+
function mapTurnExitCode(status) {
|
|
519
|
+
if (status === "completed") {
|
|
520
|
+
return 0;
|
|
521
|
+
}
|
|
522
|
+
if (status === "interrupted") {
|
|
523
|
+
return 10;
|
|
524
|
+
}
|
|
525
|
+
if (status === "cancelled") {
|
|
526
|
+
return 130;
|
|
527
|
+
}
|
|
528
|
+
if (status === "failed") {
|
|
529
|
+
return 30;
|
|
530
|
+
}
|
|
531
|
+
return 1;
|
|
532
|
+
}
|
|
533
|
+
function printProgress(message) {
|
|
534
|
+
process.stderr.write(`${message}
|
|
535
|
+
`);
|
|
536
|
+
}
|
|
537
|
+
function wrapCommand(handler) {
|
|
538
|
+
return async (...args) => {
|
|
539
|
+
try {
|
|
540
|
+
const exitCode = await handler(...args);
|
|
541
|
+
process.exitCode = typeof exitCode === "number" ? exitCode : 0;
|
|
542
|
+
} catch (error) {
|
|
543
|
+
const cliError = normalizeError(error);
|
|
544
|
+
process.stderr.write(`${cliError.message}
|
|
545
|
+
`);
|
|
546
|
+
if (cliError.details) {
|
|
547
|
+
process.stderr.write(`${JSON.stringify(cliError.details, null, 2)}
|
|
548
|
+
`);
|
|
549
|
+
}
|
|
550
|
+
if (cliError.code === "pending_interrupt") {
|
|
551
|
+
process.stderr.write("Use `ignis resume <turn_id>` to continue, or `ignis ask --new ...` to start a fresh session.\n");
|
|
552
|
+
}
|
|
553
|
+
process.exitCode = inferExitCode(cliError);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function normalizeError(error) {
|
|
558
|
+
if (error instanceof IgnisCliError) {
|
|
559
|
+
return error;
|
|
560
|
+
}
|
|
561
|
+
if (error instanceof Error) {
|
|
562
|
+
return new IgnisCliError(error.message);
|
|
563
|
+
}
|
|
564
|
+
return new IgnisCliError(String(error));
|
|
565
|
+
}
|
|
566
|
+
function inferExitCode(error) {
|
|
567
|
+
if (error.code === "pending_interrupt") {
|
|
568
|
+
return 10;
|
|
569
|
+
}
|
|
570
|
+
if (error.code === "timeout") {
|
|
571
|
+
return 12;
|
|
572
|
+
}
|
|
573
|
+
if (error.statusCode && error.statusCode >= 500) {
|
|
574
|
+
return 30;
|
|
575
|
+
}
|
|
576
|
+
if (error.statusCode && [400, 401, 403, 404, 409, 422].includes(error.statusCode)) {
|
|
577
|
+
return 20;
|
|
578
|
+
}
|
|
579
|
+
return 1;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/commands/ask.ts
|
|
583
|
+
function buildAskCommand() {
|
|
584
|
+
const command = new Command2("ask");
|
|
585
|
+
command.description("Submit one turn").argument("[message]", "Message text").addOption(new Option2("--prompt-file <path>", "Read prompt text from file")).addOption(new Option2("--attach <path>", "Upload and attach a file").default([], void 0).argParser(collectValues)).addOption(new Option2("--file-id <id>", "Reuse an existing uploaded file_id").default([], void 0).argParser(collectValues)).addOption(new Option2("--session <id>", "Continue an existing session")).addOption(new Option2("--new", "Start a new session instead of reusing the cwd session")).addOption(new Option2("--canvas <id>", "Create a new session under an existing canvas")).addOption(new Option2("--agent <name>", "Override agent name")).addOption(new Option2("--skill <id>", "Skill ID").default("base")).addOption(new Option2("--async", "Return immediately after submission without polling")).addOption(new Option2("--wait-ms <ms>", "Per-request server wait window").default("25000")).addOption(new Option2("--timeout-ms <ms>", "Client-side polling timeout").default("300000")).addOption(new Option2("--no-session-cache", "Do not read or write cwd-scoped session cache")).hook("preAction", () => {
|
|
586
|
+
}).action(
|
|
587
|
+
wrapCommand(async (messageArg, options) => {
|
|
588
|
+
const message = await resolvePrompt(messageArg, options.promptFile);
|
|
589
|
+
if (options.new && options.session?.trim()) {
|
|
590
|
+
throw new IgnisCliError("--new cannot be combined with --session");
|
|
591
|
+
}
|
|
592
|
+
if (!message.trim() && options.attach.length === 0 && options.fileId.length === 0) {
|
|
593
|
+
throw new IgnisCliError("Message is empty. Pass text, --prompt-file, stdin, --attach, or --file-id.");
|
|
594
|
+
}
|
|
595
|
+
const { client, config } = await createClient(options);
|
|
596
|
+
const uploadedFileIds = await uploadAttachments(client, options.attach, Boolean(options.json));
|
|
597
|
+
const fileIds = [
|
|
598
|
+
...options.fileId.map((value) => value.trim()).filter(Boolean),
|
|
599
|
+
...uploadedFileIds
|
|
600
|
+
];
|
|
601
|
+
const sessionId = options.new ? void 0 : options.session?.trim() || (options.noSessionCache ? void 0 : getSessionForContext(config));
|
|
602
|
+
const payload = {
|
|
603
|
+
session_id: sessionId,
|
|
604
|
+
canvas_id: options.canvas?.trim() || void 0,
|
|
605
|
+
message,
|
|
606
|
+
file_ids: fileIds,
|
|
607
|
+
agent: options.agent?.trim() || void 0,
|
|
608
|
+
skill_id: options.skill,
|
|
609
|
+
wait_ms: options.async ? 0 : toPositiveInt(options.waitMs, 25e3),
|
|
610
|
+
idempotency_key: crypto.randomUUID()
|
|
611
|
+
};
|
|
612
|
+
let turn = await client.createTurn(payload);
|
|
613
|
+
if (!options.async && (turn.status === "running" || turn.status === "queued")) {
|
|
614
|
+
turn = await client.waitForTurn(turn, {
|
|
615
|
+
timeoutMs: toPositiveInt(options.timeoutMs, 3e5)
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
if (!options.noSessionCache && turn.session_id) {
|
|
619
|
+
await saveConfig(setSessionForContext(config, turn.session_id));
|
|
620
|
+
}
|
|
621
|
+
if (options.json) {
|
|
622
|
+
printJson(turn);
|
|
623
|
+
} else {
|
|
624
|
+
printTurnHuman(turn);
|
|
625
|
+
}
|
|
626
|
+
if (options.async && (turn.status === "running" || turn.status === "queued")) {
|
|
627
|
+
return 0;
|
|
628
|
+
}
|
|
629
|
+
return mapTurnExitCode(turn.status);
|
|
630
|
+
})
|
|
631
|
+
);
|
|
632
|
+
return addRuntimeOptions(command);
|
|
633
|
+
}
|
|
634
|
+
async function resolvePrompt(messageArg, promptFile) {
|
|
635
|
+
if (promptFile) {
|
|
636
|
+
return readFile3(promptFile, "utf8");
|
|
637
|
+
}
|
|
638
|
+
if (typeof messageArg === "string" && messageArg.trim()) {
|
|
639
|
+
return messageArg;
|
|
640
|
+
}
|
|
641
|
+
if (!process.stdin.isTTY) {
|
|
642
|
+
const chunks = [];
|
|
643
|
+
for await (const chunk of process.stdin) {
|
|
644
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
645
|
+
}
|
|
646
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
647
|
+
}
|
|
648
|
+
return "";
|
|
649
|
+
}
|
|
650
|
+
async function uploadAttachments(client, attachments, jsonOutput) {
|
|
651
|
+
if (attachments.length === 0) {
|
|
652
|
+
return [];
|
|
653
|
+
}
|
|
654
|
+
const fileIds = [];
|
|
655
|
+
for (const filePath of attachments) {
|
|
656
|
+
if (!jsonOutput) {
|
|
657
|
+
printProgress(`uploading ${filePath}`);
|
|
658
|
+
}
|
|
659
|
+
const uploaded = await client.uploadFile(filePath);
|
|
660
|
+
fileIds.push(uploaded.file_id);
|
|
661
|
+
}
|
|
662
|
+
return fileIds;
|
|
663
|
+
}
|
|
664
|
+
function toPositiveInt(value, fallbackValue) {
|
|
665
|
+
const parsed = Number.parseInt(value, 10);
|
|
666
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackValue;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/commands/cancel.ts
|
|
670
|
+
import { Command as Command3 } from "commander";
|
|
671
|
+
function buildCancelCommand() {
|
|
672
|
+
const command = new Command3("cancel");
|
|
673
|
+
command.description("Cancel a running turn").argument("<turn-id>", "Turn ID").action(
|
|
674
|
+
wrapCommand(async (turnId, options) => {
|
|
675
|
+
const { client } = await createClient(options);
|
|
676
|
+
const turn = await client.cancelTurn(turnId);
|
|
677
|
+
if (options.json) {
|
|
678
|
+
printJson(turn);
|
|
679
|
+
} else {
|
|
680
|
+
printTurnHuman(turn);
|
|
681
|
+
}
|
|
682
|
+
return mapTurnExitCode(turn.status);
|
|
683
|
+
})
|
|
684
|
+
);
|
|
685
|
+
return addRuntimeOptions(command);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/commands/history.ts
|
|
689
|
+
import { Command as Command4, Option as Option3 } from "commander";
|
|
690
|
+
var DEFAULT_VISIBLE_TYPES = /* @__PURE__ */ new Set([
|
|
691
|
+
"user_message",
|
|
692
|
+
"agent_text",
|
|
693
|
+
"agent_attachment",
|
|
694
|
+
"interrupt_question",
|
|
695
|
+
"interrupt_response",
|
|
696
|
+
"system_error"
|
|
697
|
+
]);
|
|
698
|
+
function buildHistoryCommand() {
|
|
699
|
+
const command = new Command4("history");
|
|
700
|
+
command.description("Show recent history for a session").addOption(new Option3("--session <id>", "Read history for an explicit session")).addOption(new Option3("--limit <n>", "Number of turns to fetch").default("20")).addOption(new Option3("--offset <n>", "Number of newest turns to skip").default("0")).addOption(new Option3("--messages", "Show raw message history instead of turn summaries")).addOption(new Option3("--all", "Include tool and system event messages when using --messages")).action(
|
|
701
|
+
wrapCommand(async (options) => {
|
|
702
|
+
const { client, config } = await createClient(options);
|
|
703
|
+
const sessionId = options.session?.trim() || getSessionForContext(config);
|
|
704
|
+
if (!sessionId) {
|
|
705
|
+
throw new IgnisCliError("No session selected. Pass --session or run `ignis ask` in this directory first.");
|
|
706
|
+
}
|
|
707
|
+
const history = await client.getSessionHistory(sessionId, {
|
|
708
|
+
limit: toNonNegativeInt(options.limit, 20),
|
|
709
|
+
offset: toNonNegativeInt(options.offset, 0),
|
|
710
|
+
view: options.messages ? "messages" : "turns"
|
|
711
|
+
});
|
|
712
|
+
const output = options.messages && !options.all ? filterHistory(history) : history;
|
|
713
|
+
if (options.json) {
|
|
714
|
+
printJson(output);
|
|
715
|
+
} else {
|
|
716
|
+
if (options.messages) {
|
|
717
|
+
printHistoryHuman(output);
|
|
718
|
+
} else {
|
|
719
|
+
printTurnHistoryHuman(output);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return 0;
|
|
723
|
+
})
|
|
724
|
+
);
|
|
725
|
+
return addRuntimeOptions(command);
|
|
726
|
+
}
|
|
727
|
+
function filterHistory(history) {
|
|
728
|
+
const filteredMessages = (history.messages ?? []).filter((message) => DEFAULT_VISIBLE_TYPES.has(message.type));
|
|
729
|
+
return {
|
|
730
|
+
...history,
|
|
731
|
+
messages: filteredMessages
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function toNonNegativeInt(value, fallbackValue) {
|
|
735
|
+
const parsed = Number.parseInt(value, 10);
|
|
736
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
737
|
+
return fallbackValue;
|
|
738
|
+
}
|
|
739
|
+
return parsed;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/commands/login.ts
|
|
743
|
+
import { Command as Command5 } from "commander";
|
|
744
|
+
function buildLoginCommand() {
|
|
745
|
+
const command = new Command5("login");
|
|
746
|
+
command.description("Store CLI token and base URL in ~/.ignis/config.json").requiredOption("--base-url <url>", "Backend base URL").requiredOption("--token <token>", "CLI token").option("--json", "Print machine-readable JSON output").action(
|
|
747
|
+
wrapCommand(async (options) => {
|
|
748
|
+
const config = await loadConfig();
|
|
749
|
+
const nextConfig = {
|
|
750
|
+
...config,
|
|
751
|
+
baseUrl: options.baseUrl.trim().replace(/\/+$/, ""),
|
|
752
|
+
cliToken: options.token.trim()
|
|
753
|
+
};
|
|
754
|
+
await saveConfig(nextConfig);
|
|
755
|
+
if (options.json) {
|
|
756
|
+
printJson({
|
|
757
|
+
ok: true,
|
|
758
|
+
config_path: getConfigPath(),
|
|
759
|
+
base_url: nextConfig.baseUrl
|
|
760
|
+
});
|
|
761
|
+
} else {
|
|
762
|
+
printLoginHuman(getConfigPath());
|
|
763
|
+
}
|
|
764
|
+
return 0;
|
|
765
|
+
})
|
|
766
|
+
);
|
|
767
|
+
return command;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/commands/resume.ts
|
|
771
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
772
|
+
import { Command as Command6, Option as Option4 } from "commander";
|
|
773
|
+
function buildResumeCommand() {
|
|
774
|
+
const command = new Command6("resume");
|
|
775
|
+
command.description("Resume an interrupted turn").argument("<turn-id>", "Interrupted turn ID").addOption(new Option4("--answer <text>", "Provide one answer value").default([], void 0).argParser(collectValues)).addOption(new Option4("--payload <json-or-@file>", "Raw resume payload JSON")).addOption(new Option4("--cancel", "Cancel the interrupted turn instead of answering")).addOption(new Option4("--async", "Return immediately after submission without polling")).addOption(new Option4("--wait-ms <ms>", "Per-request server wait window").default("25000")).addOption(new Option4("--timeout-ms <ms>", "Client-side polling timeout").default("300000")).action(
|
|
776
|
+
wrapCommand(async (turnId, options) => {
|
|
777
|
+
const { client } = await createClient(options);
|
|
778
|
+
const response = await buildResumePayload(options);
|
|
779
|
+
let turn = await client.resumeTurn(turnId, {
|
|
780
|
+
response,
|
|
781
|
+
wait_ms: options.async ? 0 : Number.parseInt(options.waitMs, 10) || 25e3
|
|
782
|
+
});
|
|
783
|
+
if (!options.async && (turn.status === "running" || turn.status === "queued")) {
|
|
784
|
+
turn = await client.waitForTurn(turn, {
|
|
785
|
+
timeoutMs: Number.parseInt(options.timeoutMs, 10) || 3e5
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
if (options.json) {
|
|
789
|
+
printJson(turn);
|
|
790
|
+
} else {
|
|
791
|
+
printTurnHuman(turn);
|
|
792
|
+
}
|
|
793
|
+
if (options.async && (turn.status === "running" || turn.status === "queued")) {
|
|
794
|
+
return 0;
|
|
795
|
+
}
|
|
796
|
+
return mapTurnExitCode(turn.status);
|
|
797
|
+
})
|
|
798
|
+
);
|
|
799
|
+
return addRuntimeOptions(command);
|
|
800
|
+
}
|
|
801
|
+
async function buildResumePayload(options) {
|
|
802
|
+
if (options.payload && (options.answer.length > 0 || options.cancel)) {
|
|
803
|
+
throw new IgnisCliError("--payload cannot be combined with --answer or --cancel");
|
|
804
|
+
}
|
|
805
|
+
if (options.payload) {
|
|
806
|
+
return parseJsonInput(options.payload);
|
|
807
|
+
}
|
|
808
|
+
if (options.cancel) {
|
|
809
|
+
return { cancelled: true };
|
|
810
|
+
}
|
|
811
|
+
if (options.answer.length === 0) {
|
|
812
|
+
throw new IgnisCliError("Provide at least one --answer, --payload, or --cancel");
|
|
813
|
+
}
|
|
814
|
+
return { answers: options.answer };
|
|
815
|
+
}
|
|
816
|
+
async function parseJsonInput(value) {
|
|
817
|
+
const raw = value.startsWith("@") ? await readFile4(value.slice(1), "utf8") : value;
|
|
818
|
+
let parsed;
|
|
819
|
+
try {
|
|
820
|
+
parsed = JSON.parse(raw);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
throw new IgnisCliError(`Invalid JSON payload: ${error.message}`);
|
|
823
|
+
}
|
|
824
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
825
|
+
throw new IgnisCliError("Resume payload must be a JSON object");
|
|
826
|
+
}
|
|
827
|
+
return parsed;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/commands/skills.ts
|
|
831
|
+
import { Command as Command7, Option as Option5 } from "commander";
|
|
832
|
+
function buildSkillsCommand() {
|
|
833
|
+
const command = new Command7("skills");
|
|
834
|
+
command.description("List/search available skills").addOption(new Option5("--query <text>", "Search skills by keyword").default("")).addOption(new Option5("--limit <n>", "Number of skills to fetch").default("50")).addOption(new Option5("--offset <n>", "Number of skills to skip").default("0")).action(wrapCommand(async (options) => handleSkillsList(options)));
|
|
835
|
+
addRuntimeOptions(command);
|
|
836
|
+
const listCommand = new Command7("list");
|
|
837
|
+
listCommand.description("List/search available skills").addOption(new Option5("--query <text>", "Search skills by keyword").default("")).addOption(new Option5("--limit <n>", "Number of skills to fetch").default("50")).addOption(new Option5("--offset <n>", "Number of skills to skip").default("0")).action(wrapCommand(async (options) => handleSkillsList(options)));
|
|
838
|
+
addRuntimeOptions(listCommand);
|
|
839
|
+
command.addCommand(listCommand);
|
|
840
|
+
return command;
|
|
841
|
+
}
|
|
842
|
+
async function handleSkillsList(options) {
|
|
843
|
+
const { client } = await createClient(options);
|
|
844
|
+
const response = await client.getSkills({
|
|
845
|
+
query: options.query?.trim() || "",
|
|
846
|
+
limit: toNonNegativeInt2(options.limit, 50),
|
|
847
|
+
offset: toNonNegativeInt2(options.offset, 0)
|
|
848
|
+
});
|
|
849
|
+
if (options.json) {
|
|
850
|
+
printJson(response);
|
|
851
|
+
} else {
|
|
852
|
+
printSkillsHuman(response);
|
|
853
|
+
}
|
|
854
|
+
return 0;
|
|
855
|
+
}
|
|
856
|
+
function toNonNegativeInt2(value, fallbackValue) {
|
|
857
|
+
const parsed = Number.parseInt(value, 10);
|
|
858
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
859
|
+
return fallbackValue;
|
|
860
|
+
}
|
|
861
|
+
return parsed;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/commands/status.ts
|
|
865
|
+
import { Command as Command8, Option as Option6 } from "commander";
|
|
866
|
+
function buildStatusCommand() {
|
|
867
|
+
const command = new Command8("status");
|
|
868
|
+
command.description("Fetch the latest state of a turn or session").argument("[turn-id]", "Turn ID").addOption(new Option6("--session <id>", "Resolve the latest turn from a session")).addOption(new Option6("--wait", "Poll until terminal state")).addOption(new Option6("--timeout-ms <ms>", "Client-side polling timeout").default("300000")).action(
|
|
869
|
+
wrapCommand(async (turnId, options) => {
|
|
870
|
+
const { client, config } = await createClient(options);
|
|
871
|
+
const sessionId = options.session?.trim() || getSessionForContext(config);
|
|
872
|
+
let turn;
|
|
873
|
+
if (turnId?.trim()) {
|
|
874
|
+
turn = await client.getTurn(turnId.trim());
|
|
875
|
+
} else if (sessionId) {
|
|
876
|
+
turn = await client.getLatestSessionTurn(sessionId);
|
|
877
|
+
} else {
|
|
878
|
+
throw new IgnisCliError("No turn or session selected. Pass <turn-id>, --session, or run `ignis ask` in this directory first.");
|
|
879
|
+
}
|
|
880
|
+
if (options.wait && (turn.status === "running" || turn.status === "queued")) {
|
|
881
|
+
turn = await client.waitForTurn(turn, {
|
|
882
|
+
timeoutMs: Number.parseInt(options.timeoutMs, 10) || 3e5
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
if (options.json) {
|
|
886
|
+
printJson(turn);
|
|
887
|
+
} else {
|
|
888
|
+
printTurnHuman(turn);
|
|
889
|
+
}
|
|
890
|
+
return mapTurnExitCode(turn.status);
|
|
891
|
+
})
|
|
892
|
+
);
|
|
893
|
+
return addRuntimeOptions(command);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// src/commands/upload.ts
|
|
897
|
+
import { Command as Command9 } from "commander";
|
|
898
|
+
function buildUploadCommand() {
|
|
899
|
+
const command = new Command9("upload");
|
|
900
|
+
command.description("Upload a file and return its file_id").argument("<path>", "Local file path").action(
|
|
901
|
+
wrapCommand(async (filePath, options) => {
|
|
902
|
+
const { client } = await createClient(options);
|
|
903
|
+
const uploaded = await client.uploadFile(filePath);
|
|
904
|
+
if (options.json) {
|
|
905
|
+
printJson(uploaded);
|
|
906
|
+
} else {
|
|
907
|
+
printUploadHuman(uploaded);
|
|
908
|
+
}
|
|
909
|
+
return 0;
|
|
910
|
+
})
|
|
911
|
+
);
|
|
912
|
+
return addRuntimeOptions(command);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/index.ts
|
|
916
|
+
var program = new Command10();
|
|
917
|
+
program.name("ignis").description("Ignis CLI for Ultra-Mai").version("0.1.0");
|
|
918
|
+
program.addCommand(buildLoginCommand());
|
|
919
|
+
program.addCommand(buildAskCommand());
|
|
920
|
+
program.addCommand(buildHistoryCommand());
|
|
921
|
+
program.addCommand(buildSkillsCommand());
|
|
922
|
+
program.addCommand(buildStatusCommand());
|
|
923
|
+
program.addCommand(buildResumeCommand());
|
|
924
|
+
program.addCommand(buildCancelCommand());
|
|
925
|
+
program.addCommand(buildUploadCommand());
|
|
926
|
+
await program.parseAsync(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ignis-agent-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Stateless CLI for the Ultra-Mai Agent V2 service",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/adtech-crypto/mai-2-tmp.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/adtech-crypto/mai-2-tmp/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/adtech-crypto/mai-2-tmp/tree/test/ignis",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"ignis",
|
|
17
|
+
"cli",
|
|
18
|
+
"agent",
|
|
19
|
+
"funplus",
|
|
20
|
+
"fastapi"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"bin": {
|
|
27
|
+
"ignis": "dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup src/index.ts --format esm --target node18 --out-dir dist --clean",
|
|
31
|
+
"dev": "node --loader ts-node/esm src/index.ts",
|
|
32
|
+
"prepack": "npm run build",
|
|
33
|
+
"typecheck": "tsc --noEmit"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"commander": "^13.1.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.13.10",
|
|
46
|
+
"ts-node": "^10.9.2",
|
|
47
|
+
"tsup": "^8.4.0",
|
|
48
|
+
"typescript": "^5.8.2"
|
|
49
|
+
}
|
|
50
|
+
}
|