niahere 0.4.6 → 0.5.1
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/package.json +2 -2
- package/src/agent/backends/claude-normalize.ts +5 -6
- package/src/agent/backends/claude.ts +6 -4
- package/src/agent/backends/codex-normalize.ts +19 -3
- package/src/agent/backends/codex.ts +100 -35
- package/src/agent/chain.ts +60 -0
- package/src/agent/failure.ts +90 -0
- package/src/agent/health.ts +41 -0
- package/src/agent/index.ts +3 -1
- package/src/agent/mcp-endpoint.ts +5 -3
- package/src/agent/models.ts +59 -0
- package/src/agent/registry.ts +54 -29
- package/src/agent/types.ts +9 -4
- package/src/channels/common/chat-session.ts +60 -0
- package/src/channels/phone/consult.ts +2 -1
- package/src/channels/phone/index.ts +5 -3
- package/src/channels/phone/relay.ts +15 -5
- package/src/channels/slack.ts +24 -34
- package/src/channels/sms.ts +16 -36
- package/src/channels/telegram.ts +22 -34
- package/src/channels/twilio/media-cache.ts +4 -3
- package/src/channels/twilio/rest.ts +0 -31
- package/src/channels/twilio/server.ts +0 -5
- package/src/channels/twilio/shared.ts +36 -0
- package/src/channels/whatsapp.ts +24 -58
- package/src/chat/engine.ts +83 -34
- package/src/chat/gap-marker.ts +63 -0
- package/src/chat/repl.ts +5 -5
- package/src/cli/config.ts +71 -0
- package/src/cli/index.ts +23 -288
- package/src/cli/job.ts +1 -2
- package/src/cli/logs.ts +55 -0
- package/src/cli/run.ts +74 -0
- package/src/cli/skills.ts +13 -0
- package/src/cli/status.ts +0 -1
- package/src/cli/test.ts +32 -0
- package/src/cli/update.ts +79 -0
- package/src/commands/backup.ts +1 -2
- package/src/commands/init.ts +1 -2
- package/src/commands/validate.ts +17 -13
- package/src/core/alive.ts +6 -5
- package/src/core/consolidator.ts +78 -31
- package/src/core/daemon.ts +3 -2
- package/src/core/finalizer.ts +9 -5
- package/src/core/runner.ts +64 -47
- package/src/core/scheduler.ts +19 -4
- package/src/core/skills.ts +0 -4
- package/src/db/migrations/017_sessions_consolidated_count.ts +9 -0
- package/src/db/models/active_engine.ts +0 -7
- package/src/db/models/job.ts +3 -2
- package/src/db/models/message.ts +10 -0
- package/src/db/models/session.ts +12 -0
- package/src/mcp/gate.ts +46 -0
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools/misc.ts +2 -1
- package/src/mcp/tools/send.ts +5 -4
- package/src/types/config.ts +4 -4
- package/src/utils/config.ts +6 -8
- package/src/utils/errors.ts +26 -0
- package/src/utils/retry.ts +0 -45
package/src/cli/index.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { mkdirSync, readFileSync } from "fs";
|
|
3
3
|
import { isRunning, readPid, runDaemon, startDaemon, stopDaemon } from "../core/daemon";
|
|
4
4
|
import { getConfig } from "../utils/config";
|
|
5
|
-
import { localTime } from "../utils/time";
|
|
6
5
|
import { startRepl } from "../chat/repl";
|
|
7
|
-
import { Message } from "../db/models";
|
|
8
|
-
import { withDb } from "../db/with-db";
|
|
9
6
|
import { getNiaHome, getPaths } from "../utils/paths";
|
|
10
|
-
import { errMsg } from "../utils/errors";
|
|
11
7
|
import { fail, ICON_PASS, ICON_WARN } from "../utils/cli";
|
|
12
8
|
import { jobCommand } from "./job";
|
|
13
9
|
import { statusCommand } from "./status";
|
|
@@ -19,7 +15,7 @@ import { rulesCommand, memoryCommand } from "./self";
|
|
|
19
15
|
import { watchCommand } from "./watch";
|
|
20
16
|
import { agentCommand } from "./agent";
|
|
21
17
|
import { employeeCommand } from "./employee";
|
|
22
|
-
import { guardActiveEngines, parseGuardFlags
|
|
18
|
+
import { guardActiveEngines, parseGuardFlags } from "../core/engine-guard";
|
|
23
19
|
|
|
24
20
|
// Set LOG_LEVEL from config before anything else logs
|
|
25
21
|
try {
|
|
@@ -58,7 +54,6 @@ async function awaitStartup(timeout = 60_000): Promise<void> {
|
|
|
58
54
|
|
|
59
55
|
if (expecting.size === 0) return;
|
|
60
56
|
|
|
61
|
-
const { readFileSync } = await import("fs");
|
|
62
57
|
const ready = new Set<string>();
|
|
63
58
|
let logOffset = 0;
|
|
64
59
|
try {
|
|
@@ -167,80 +162,13 @@ switch (command) {
|
|
|
167
162
|
|
|
168
163
|
case "run": {
|
|
169
164
|
const prompt = process.argv.slice(3).join(" ");
|
|
170
|
-
if (prompt) {
|
|
171
|
-
const { createChatEngine } = await import("../chat/engine");
|
|
172
|
-
const { getMcpServers } = await import("../mcp");
|
|
173
|
-
const { DIM, RESET: RST, CLEAR_LINE, SPINNER: FRAMES } = await import("../utils/cli");
|
|
174
|
-
let frame = 0;
|
|
175
|
-
let statusText = "thinking";
|
|
176
|
-
let spinTimer: ReturnType<typeof setInterval> | null = null;
|
|
177
|
-
let streamedLen = 0;
|
|
178
|
-
let streaming = false;
|
|
179
|
-
|
|
180
|
-
const renderSpinner = () => {
|
|
181
|
-
process.stderr.write(`${CLEAR_LINE}${DIM} ${FRAMES[frame]} ${statusText}${RST}`);
|
|
182
|
-
frame = (frame + 1) % FRAMES.length;
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
await withDb(async () => {
|
|
186
|
-
const engine = await createChatEngine({
|
|
187
|
-
room: "cli-run",
|
|
188
|
-
channel: "terminal",
|
|
189
|
-
resume: false,
|
|
190
|
-
mcpServers: getMcpServers(),
|
|
191
|
-
});
|
|
192
|
-
spinTimer = setInterval(renderSpinner, 80);
|
|
193
|
-
renderSpinner();
|
|
194
|
-
|
|
195
|
-
const { result, costUsd, turns } = await engine.send(prompt, {
|
|
196
|
-
onStream(textSoFar) {
|
|
197
|
-
if (!streaming) {
|
|
198
|
-
if (spinTimer) {
|
|
199
|
-
clearInterval(spinTimer);
|
|
200
|
-
spinTimer = null;
|
|
201
|
-
}
|
|
202
|
-
process.stderr.write("\x1b[2K\r");
|
|
203
|
-
streaming = true;
|
|
204
|
-
}
|
|
205
|
-
const chunk = textSoFar.slice(streamedLen);
|
|
206
|
-
if (chunk) {
|
|
207
|
-
process.stdout.write(chunk);
|
|
208
|
-
streamedLen = textSoFar.length;
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
onActivity(text) {
|
|
212
|
-
if (!streaming) statusText = text;
|
|
213
|
-
},
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
if (spinTimer) {
|
|
217
|
-
clearInterval(spinTimer);
|
|
218
|
-
spinTimer = null;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (!streaming && result.trim()) {
|
|
222
|
-
process.stderr.write("\x1b[2K\r");
|
|
223
|
-
process.stdout.write(result.trim());
|
|
224
|
-
} else if (streaming) {
|
|
225
|
-
const rest = result.slice(streamedLen);
|
|
226
|
-
if (rest.trim()) process.stdout.write(rest);
|
|
227
|
-
} else {
|
|
228
|
-
process.stderr.write("\x1b[2K\r");
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const costStr = costUsd > 0 ? `$${costUsd.toFixed(4)}` : "";
|
|
232
|
-
const turnsStr = turns > 0 ? `${turns} turn${turns !== 1 ? "s" : ""}` : "";
|
|
233
|
-
const meta = [costStr, turnsStr].filter(Boolean).join(" · ");
|
|
234
|
-
if (meta) process.stderr.write(`\n${DIM}${meta}${RST}`);
|
|
235
|
-
process.stdout.write("\n");
|
|
236
|
-
|
|
237
|
-
await engine.close();
|
|
238
|
-
});
|
|
239
|
-
process.exit(0);
|
|
240
|
-
} else {
|
|
165
|
+
if (!prompt) {
|
|
241
166
|
await runDaemon();
|
|
167
|
+
break;
|
|
242
168
|
}
|
|
243
|
-
|
|
169
|
+
const { runPrompt } = await import("./run");
|
|
170
|
+
await runPrompt(prompt);
|
|
171
|
+
process.exit(0);
|
|
244
172
|
}
|
|
245
173
|
|
|
246
174
|
case "job": {
|
|
@@ -264,52 +192,14 @@ switch (command) {
|
|
|
264
192
|
}
|
|
265
193
|
|
|
266
194
|
case "history": {
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
await withDb(async () => {
|
|
270
|
-
const messages = await Message.getRecent(20, room);
|
|
271
|
-
if (messages.length === 0) {
|
|
272
|
-
console.log("No messages yet.");
|
|
273
|
-
} else {
|
|
274
|
-
for (const m of messages) {
|
|
275
|
-
const time = localTime(new Date(m.createdAt));
|
|
276
|
-
const prefix = m.sender === "user" ? "you" : m.sender;
|
|
277
|
-
const roomTag = room ? "" : `[${m.room}] `;
|
|
278
|
-
const snippet = m.content.length > 120 ? m.content.slice(0, 120) + "..." : m.content;
|
|
279
|
-
console.log(` ${roomTag}${time} ${prefix} > ${snippet.replace(/\n/g, " ")}`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
} catch (err) {
|
|
284
|
-
fail(`Failed: ${errMsg(err)}`);
|
|
285
|
-
}
|
|
195
|
+
const { historyCommand } = await import("./logs");
|
|
196
|
+
await historyCommand(process.argv[3]);
|
|
286
197
|
break;
|
|
287
198
|
}
|
|
288
199
|
|
|
289
200
|
case "logs": {
|
|
290
|
-
const {
|
|
291
|
-
|
|
292
|
-
const logArgs = process.argv.slice(3);
|
|
293
|
-
const follow = logArgs.includes("-f") || logArgs.includes("--follow");
|
|
294
|
-
// --channel <name> filters logs by channel/component via grep
|
|
295
|
-
const chIdx = logArgs.indexOf("--channel");
|
|
296
|
-
const channelFilter = chIdx !== -1 && logArgs[chIdx + 1] ? logArgs[chIdx + 1] : null;
|
|
297
|
-
|
|
298
|
-
if (channelFilter) {
|
|
299
|
-
// Pipe through grep to filter by channel name in structured logs
|
|
300
|
-
const tailArgs = follow ? ["tail", "-f", daemonLog] : ["tail", "-200", daemonLog];
|
|
301
|
-
const tail = Bun.spawn(tailArgs, {
|
|
302
|
-
stdio: ["ignore", "pipe", "inherit"],
|
|
303
|
-
});
|
|
304
|
-
const grep = Bun.spawn(["grep", "-i", channelFilter], {
|
|
305
|
-
stdio: [tail.stdout, "inherit", "inherit"],
|
|
306
|
-
});
|
|
307
|
-
await grep.exited;
|
|
308
|
-
} else {
|
|
309
|
-
const args = follow ? ["tail", "-f", daemonLog] : ["tail", "-50", daemonLog];
|
|
310
|
-
const proc = Bun.spawn(args, { stdio: ["ignore", "inherit", "inherit"] });
|
|
311
|
-
await proc.exited;
|
|
312
|
-
}
|
|
201
|
+
const { logsCommand } = await import("./logs");
|
|
202
|
+
await logsCommand(process.argv.slice(3));
|
|
313
203
|
break;
|
|
314
204
|
}
|
|
315
205
|
|
|
@@ -352,20 +242,8 @@ switch (command) {
|
|
|
352
242
|
}
|
|
353
243
|
|
|
354
244
|
case "skills": {
|
|
355
|
-
const {
|
|
356
|
-
|
|
357
|
-
let skills = loadSkills();
|
|
358
|
-
if (filter) {
|
|
359
|
-
skills = skills.filter((s) => s.source === filter);
|
|
360
|
-
}
|
|
361
|
-
if (skills.length === 0) {
|
|
362
|
-
console.log(filter ? `No skills found in "${filter}".` : "No skills found.");
|
|
363
|
-
} else {
|
|
364
|
-
for (const s of skills) {
|
|
365
|
-
const tag = filter ? "" : ` [${s.source}]`;
|
|
366
|
-
console.log(` ${s.name}${tag}`);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
245
|
+
const { skillsCommand } = await import("./skills");
|
|
246
|
+
skillsCommand(process.argv[3]);
|
|
369
247
|
break;
|
|
370
248
|
}
|
|
371
249
|
|
|
@@ -390,90 +268,19 @@ switch (command) {
|
|
|
390
268
|
}
|
|
391
269
|
|
|
392
270
|
case "config": {
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
const configVal = process.argv.slice(5).join(" ");
|
|
396
|
-
const { readRawConfig, updateRawConfig } = await import("../utils/config");
|
|
397
|
-
|
|
398
|
-
if (configSub === "set" && configKey) {
|
|
399
|
-
if (!configVal) fail("Usage: nia config set <key> <value>");
|
|
400
|
-
// Support dot notation for nested keys (e.g. channels.default)
|
|
401
|
-
const parts = configKey.split(".");
|
|
402
|
-
let obj: Record<string, unknown> = {};
|
|
403
|
-
let cursor = obj;
|
|
404
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
405
|
-
cursor[parts[i]] = {};
|
|
406
|
-
cursor = cursor[parts[i]] as Record<string, unknown>;
|
|
407
|
-
}
|
|
408
|
-
// Auto-detect booleans and numbers
|
|
409
|
-
let parsed: unknown = configVal;
|
|
410
|
-
if (configVal === "true") parsed = true;
|
|
411
|
-
else if (configVal === "false") parsed = false;
|
|
412
|
-
else if (/^\d+$/.test(configVal)) parsed = Number(configVal);
|
|
413
|
-
cursor[parts[parts.length - 1]] = parsed;
|
|
414
|
-
updateRawConfig(obj);
|
|
415
|
-
console.log(`${configKey} = ${configVal}`);
|
|
416
|
-
} else if (configSub === "get" && configKey) {
|
|
417
|
-
const raw = readRawConfig();
|
|
418
|
-
const parts = configKey.split(".");
|
|
419
|
-
let val: unknown = raw;
|
|
420
|
-
for (const p of parts) {
|
|
421
|
-
if (val && typeof val === "object") val = (val as Record<string, unknown>)[p];
|
|
422
|
-
else {
|
|
423
|
-
val = undefined;
|
|
424
|
-
break;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
if (val === undefined) {
|
|
428
|
-
console.log(`${configKey}: (not set)`);
|
|
429
|
-
} else if (typeof val === "object") {
|
|
430
|
-
const yaml = (await import("js-yaml")).default;
|
|
431
|
-
console.log(yaml.dump(val, { lineWidth: -1 }).trim());
|
|
432
|
-
} else {
|
|
433
|
-
console.log(`${configKey} = ${val}`);
|
|
434
|
-
}
|
|
435
|
-
} else if (!configSub || configSub === "list") {
|
|
436
|
-
const raw = readRawConfig();
|
|
437
|
-
const yaml = (await import("js-yaml")).default;
|
|
438
|
-
console.log(yaml.dump(raw, { lineWidth: -1 }).trim());
|
|
439
|
-
} else {
|
|
440
|
-
console.log("Usage: nia config <set|get|list>");
|
|
441
|
-
console.log(" nia config set <key> <value> — set a config value");
|
|
442
|
-
console.log(" nia config get <key> — get a config value");
|
|
443
|
-
console.log(" nia config list — show all config");
|
|
444
|
-
}
|
|
271
|
+
const { configCommand } = await import("./config");
|
|
272
|
+
await configCommand(process.argv[3], process.argv[4], process.argv.slice(5).join(" "));
|
|
445
273
|
break;
|
|
446
274
|
}
|
|
447
275
|
|
|
448
276
|
case "channels": {
|
|
449
277
|
const sub = process.argv[3];
|
|
450
|
-
|
|
451
|
-
const { updateRawConfig } = await import("../utils/config");
|
|
452
|
-
if (sub === "on" || sub === "off") {
|
|
453
|
-
const enabled = sub === "on";
|
|
454
|
-
if (target) {
|
|
455
|
-
const supported = new Set(["telegram", "slack", "phone", "sms", "whatsapp"]);
|
|
456
|
-
if (!supported.has(target)) fail("Usage: nia channels <on|off> [telegram|slack|phone|sms|whatsapp]");
|
|
457
|
-
updateRawConfig({ channels: { ...(enabled ? { enabled: true } : {}), [target]: { enabled } } });
|
|
458
|
-
} else {
|
|
459
|
-
updateRawConfig({ channels: { enabled } });
|
|
460
|
-
}
|
|
461
|
-
const pid = readPid();
|
|
462
|
-
if (pid && isRunning()) {
|
|
463
|
-
process.kill(pid, "SIGHUP");
|
|
464
|
-
console.log(
|
|
465
|
-
target ? `${target} ${enabled ? "enabled" : "disabled"}` : `channels ${enabled ? "enabled" : "disabled"}`,
|
|
466
|
-
);
|
|
467
|
-
} else {
|
|
468
|
-
console.log(
|
|
469
|
-
target
|
|
470
|
-
? `${target} ${enabled ? "enabled" : "disabled"} — start nia to apply`
|
|
471
|
-
: `channels ${enabled ? "enabled" : "disabled"} — start nia to apply`,
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
} else {
|
|
278
|
+
if (sub !== "on" && sub !== "off") {
|
|
475
279
|
console.log(`channels: ${getConfig().channels.enabled ? "on" : "off"}`);
|
|
280
|
+
break;
|
|
476
281
|
}
|
|
282
|
+
const { channelsToggleCommand } = await import("./update");
|
|
283
|
+
await channelsToggleCommand(sub, process.argv[4]);
|
|
477
284
|
break;
|
|
478
285
|
}
|
|
479
286
|
|
|
@@ -484,35 +291,8 @@ switch (command) {
|
|
|
484
291
|
}
|
|
485
292
|
|
|
486
293
|
case "test": {
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
const proc = Bun.spawn(["bun", "test", ...extraArgs], {
|
|
490
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
491
|
-
cwd: import.meta.dir + "/../..",
|
|
492
|
-
env: { ...process.env, LOG_LEVEL: "silent" },
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
|
496
|
-
const exitCode = await proc.exited;
|
|
497
|
-
const output = stdout + stderr;
|
|
498
|
-
|
|
499
|
-
if (verbose) {
|
|
500
|
-
process.stdout.write(output);
|
|
501
|
-
} else {
|
|
502
|
-
for (const line of output.split("\n")) {
|
|
503
|
-
if (
|
|
504
|
-
/^\s*\d+ pass/.test(line) ||
|
|
505
|
-
/^\s*\d+ fail/.test(line) ||
|
|
506
|
-
/^Ran \d+ tests/.test(line) ||
|
|
507
|
-
/expect\(\) calls/.test(line)
|
|
508
|
-
) {
|
|
509
|
-
console.log(line);
|
|
510
|
-
} else if (/^✗|FAIL|error:/i.test(line.trim())) {
|
|
511
|
-
console.log(line);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
process.exit(exitCode);
|
|
294
|
+
const { testCommand } = await import("./test");
|
|
295
|
+
await testCommand(process.argv.slice(3));
|
|
516
296
|
}
|
|
517
297
|
|
|
518
298
|
case "backup": {
|
|
@@ -530,53 +310,8 @@ switch (command) {
|
|
|
530
310
|
}
|
|
531
311
|
|
|
532
312
|
case "update": {
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
console.log(`Current: v${currentVersion}`);
|
|
536
|
-
|
|
537
|
-
// Check active engines before doing anything destructive
|
|
538
|
-
if (isRunning() && !(await guardActiveEngines("update", updateGuard))) process.exit(1);
|
|
539
|
-
|
|
540
|
-
// Auto-backup before update
|
|
541
|
-
try {
|
|
542
|
-
const { createBackup } = await import("../commands/backup");
|
|
543
|
-
console.log("Backing up...");
|
|
544
|
-
await createBackup(true);
|
|
545
|
-
console.log("✓ pre-update backup created");
|
|
546
|
-
} catch (err) {
|
|
547
|
-
console.log(`⚠ backup skipped: ${errMsg(err)}`);
|
|
548
|
-
}
|
|
549
|
-
console.log("Updating...");
|
|
550
|
-
const install = Bun.spawn(["npm", "i", "-g", "niahere@latest"], {
|
|
551
|
-
stdio: ["ignore", "inherit", "inherit"],
|
|
552
|
-
});
|
|
553
|
-
const installExit = await install.exited;
|
|
554
|
-
if (installExit !== 0) {
|
|
555
|
-
fail("Update failed.");
|
|
556
|
-
}
|
|
557
|
-
// Get new version
|
|
558
|
-
const check = Bun.spawn(["npm", "view", "niahere", "version"], {
|
|
559
|
-
stdout: "pipe",
|
|
560
|
-
stderr: "pipe",
|
|
561
|
-
});
|
|
562
|
-
const newVersion = (await new Response(check.stdout).text()).trim();
|
|
563
|
-
await check.exited;
|
|
564
|
-
if (newVersion === currentVersion) {
|
|
565
|
-
console.log("Already on latest.");
|
|
566
|
-
} else {
|
|
567
|
-
console.log(`Updated: v${currentVersion} → v${newVersion}`);
|
|
568
|
-
if (isRunning()) {
|
|
569
|
-
console.log("Restarting daemon...");
|
|
570
|
-
const { isServiceInstalled, restartService } = await import("../commands/service");
|
|
571
|
-
if (isServiceInstalled()) {
|
|
572
|
-
await restartService({ force: updateGuard.force });
|
|
573
|
-
} else {
|
|
574
|
-
stopDaemon({ force: updateGuard.force });
|
|
575
|
-
startDaemon();
|
|
576
|
-
}
|
|
577
|
-
console.log("Restarted.");
|
|
578
|
-
}
|
|
579
|
-
}
|
|
313
|
+
const { updateCommand } = await import("./update");
|
|
314
|
+
await updateCommand(process.argv.slice(3));
|
|
580
315
|
break;
|
|
581
316
|
}
|
|
582
317
|
|
package/src/cli/job.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as readline from "readline";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
3
|
import { readState, readAudit } from "../utils/logger";
|
|
3
4
|
import { getConfig } from "../utils/config";
|
|
4
5
|
import { runJob } from "../core/runner";
|
|
@@ -145,7 +146,6 @@ export async function jobCommand(): Promise<void> {
|
|
|
145
146
|
const promptFile = args.getString("prompt-file");
|
|
146
147
|
let prompt: string;
|
|
147
148
|
if (promptFile) {
|
|
148
|
-
const { existsSync, readFileSync } = await import("fs");
|
|
149
149
|
if (!existsSync(promptFile)) fail(`File not found: ${promptFile}`);
|
|
150
150
|
prompt = readFileSync(promptFile, "utf8").trim();
|
|
151
151
|
} else if (promptFlag) {
|
|
@@ -243,7 +243,6 @@ export async function jobCommand(): Promise<void> {
|
|
|
243
243
|
const promptFile = args.getString("prompt-file");
|
|
244
244
|
let prompt = args.getString("prompt");
|
|
245
245
|
if (promptFile) {
|
|
246
|
-
const { existsSync, readFileSync } = await import("fs");
|
|
247
246
|
if (!existsSync(promptFile)) fail(`File not found: ${promptFile}`);
|
|
248
247
|
prompt = readFileSync(promptFile, "utf8").trim();
|
|
249
248
|
}
|
package/src/cli/logs.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { getPaths } from "../utils/paths";
|
|
3
|
+
import { withDb } from "../db/with-db";
|
|
4
|
+
import { Message } from "../db/models";
|
|
5
|
+
import { localTime } from "../utils/time";
|
|
6
|
+
import { errMsg } from "../utils/errors";
|
|
7
|
+
|
|
8
|
+
const SNIPPET = 120;
|
|
9
|
+
|
|
10
|
+
/** `nia history [room]` — recent messages, newest last. */
|
|
11
|
+
export async function historyCommand(room?: string): Promise<void> {
|
|
12
|
+
try {
|
|
13
|
+
await withDb(async () => {
|
|
14
|
+
const messages = await Message.getRecent(20, room);
|
|
15
|
+
if (messages.length === 0) {
|
|
16
|
+
console.log("No messages yet.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
for (const m of messages) {
|
|
20
|
+
const time = localTime(new Date(m.createdAt));
|
|
21
|
+
const prefix = m.sender === "user" ? "you" : m.sender;
|
|
22
|
+
const roomTag = room ? "" : `[${m.room}] `;
|
|
23
|
+
const snippet = m.content.length > SNIPPET ? m.content.slice(0, SNIPPET) + "..." : m.content;
|
|
24
|
+
console.log(` ${roomTag}${time} ${prefix} > ${snippet.replace(/\n/g, " ")}`);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(`Failed: ${errMsg(err)}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `nia logs [-f] [--channel <name>]` — tail the daemon log. */
|
|
34
|
+
export async function logsCommand(args: string[]): Promise<void> {
|
|
35
|
+
const { daemonLog } = getPaths();
|
|
36
|
+
if (!existsSync(daemonLog)) {
|
|
37
|
+
console.error("No daemon log found. Is nia running?");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const follow = args.includes("-f") || args.includes("--follow");
|
|
42
|
+
const chIdx = args.indexOf("--channel");
|
|
43
|
+
const channelFilter = chIdx !== -1 && args[chIdx + 1] ? args[chIdx + 1] : null;
|
|
44
|
+
const tailArgs = follow ? ["tail", "-f", daemonLog] : ["tail", channelFilter ? "-200" : "-50", daemonLog];
|
|
45
|
+
|
|
46
|
+
if (!channelFilter) {
|
|
47
|
+
const proc = Bun.spawn(tailArgs, { stdio: ["ignore", "inherit", "inherit"] });
|
|
48
|
+
await proc.exited;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const tail = Bun.spawn(tailArgs, { stdio: ["ignore", "pipe", "inherit"] });
|
|
53
|
+
const grep = Bun.spawn(["grep", "-i", channelFilter], { stdio: [tail.stdout, "inherit", "inherit"] });
|
|
54
|
+
await grep.exited;
|
|
55
|
+
}
|
package/src/cli/run.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { withDb } from "../db/with-db";
|
|
2
|
+
|
|
3
|
+
/** `nia run <prompt>` — one-shot terminal turn with a spinner and streamed output. */
|
|
4
|
+
export async function runPrompt(prompt: string): Promise<void> {
|
|
5
|
+
const { createChatEngine } = await import("../chat/engine");
|
|
6
|
+
const { getMcpServers } = await import("../mcp");
|
|
7
|
+
const { DIM, RESET, CLEAR_LINE, SPINNER } = await import("../utils/cli");
|
|
8
|
+
|
|
9
|
+
let frame = 0;
|
|
10
|
+
let statusText = "thinking";
|
|
11
|
+
let spinTimer: ReturnType<typeof setInterval> | null = null;
|
|
12
|
+
let streamedLen = 0;
|
|
13
|
+
let streaming = false;
|
|
14
|
+
|
|
15
|
+
const renderSpinner = () => {
|
|
16
|
+
process.stderr.write(`${CLEAR_LINE}${DIM} ${SPINNER[frame]} ${statusText}${RESET}`);
|
|
17
|
+
frame = (frame + 1) % SPINNER.length;
|
|
18
|
+
};
|
|
19
|
+
const stopSpinner = () => {
|
|
20
|
+
if (spinTimer) {
|
|
21
|
+
clearInterval(spinTimer);
|
|
22
|
+
spinTimer = null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
await withDb(async () => {
|
|
27
|
+
const engine = await createChatEngine({
|
|
28
|
+
room: "cli-run",
|
|
29
|
+
channel: "terminal",
|
|
30
|
+
resume: false,
|
|
31
|
+
mcpServers: getMcpServers(),
|
|
32
|
+
});
|
|
33
|
+
spinTimer = setInterval(renderSpinner, 80);
|
|
34
|
+
renderSpinner();
|
|
35
|
+
|
|
36
|
+
const { result, costUsd, turns } = await engine.send(prompt, {
|
|
37
|
+
onStream(textSoFar) {
|
|
38
|
+
if (!streaming) {
|
|
39
|
+
stopSpinner();
|
|
40
|
+
process.stderr.write("\x1b[2K\r");
|
|
41
|
+
streaming = true;
|
|
42
|
+
}
|
|
43
|
+
const chunk = textSoFar.slice(streamedLen);
|
|
44
|
+
if (chunk) {
|
|
45
|
+
process.stdout.write(chunk);
|
|
46
|
+
streamedLen = textSoFar.length;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
onActivity(text) {
|
|
50
|
+
if (!streaming) statusText = text;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
stopSpinner();
|
|
55
|
+
|
|
56
|
+
if (!streaming && result.trim()) {
|
|
57
|
+
process.stderr.write("\x1b[2K\r");
|
|
58
|
+
process.stdout.write(result.trim());
|
|
59
|
+
} else if (streaming) {
|
|
60
|
+
const rest = result.slice(streamedLen);
|
|
61
|
+
if (rest.trim()) process.stdout.write(rest);
|
|
62
|
+
} else {
|
|
63
|
+
process.stderr.write("\x1b[2K\r");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const costStr = costUsd > 0 ? `$${costUsd.toFixed(4)}` : "";
|
|
67
|
+
const turnsStr = turns > 0 ? `${turns} turn${turns !== 1 ? "s" : ""}` : "";
|
|
68
|
+
const meta = [costStr, turnsStr].filter(Boolean).join(" · ");
|
|
69
|
+
if (meta) process.stderr.write(`\n${DIM}${meta}${RESET}`);
|
|
70
|
+
process.stdout.write("\n");
|
|
71
|
+
|
|
72
|
+
await engine.close();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { scanSkills } from "../core/skills";
|
|
2
|
+
|
|
3
|
+
/** `nia skills [source]` — list discovered skills, optionally filtered by source. */
|
|
4
|
+
export function skillsCommand(filter?: string): void {
|
|
5
|
+
const skills = filter ? scanSkills().filter((s) => s.source === filter) : scanSkills();
|
|
6
|
+
if (skills.length === 0) {
|
|
7
|
+
console.log(filter ? `No skills found in "${filter}".` : "No skills found.");
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
for (const s of skills) {
|
|
11
|
+
console.log(` ${s.name}${filter ? "" : ` [${s.source}]`}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/cli/status.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isRunning, readPid } from "../core/daemon";
|
|
2
2
|
import { readState } from "../utils/logger";
|
|
3
3
|
import { getConfig } from "../utils/config";
|
|
4
|
-
import { localTime } from "../utils/time";
|
|
5
4
|
import { maskToken, safeDate, dateSortValue, formatTimeLine } from "../utils/format";
|
|
6
5
|
import { Message, ActiveEngine, Job } from "../db/models";
|
|
7
6
|
import type { ScheduleType, JobStateStatus, RoomStats } from "../types";
|
package/src/cli/test.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** `nia test` — run the suite, showing only the summary unless -v. */
|
|
2
|
+
|
|
3
|
+
const SUMMARY = [/^\s*\d+ pass/, /^\s*\d+ fail/, /^Ran \d+ tests/, /expect\(\) calls/];
|
|
4
|
+
const FAILURE = /^✗|FAIL|error:/i;
|
|
5
|
+
|
|
6
|
+
function isWorthShowing(line: string): boolean {
|
|
7
|
+
return SUMMARY.some((p) => p.test(line)) || FAILURE.test(line.trim());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function testCommand(argv: string[]): Promise<never> {
|
|
11
|
+
const verbose = argv.includes("-v") || argv.includes("--verbose");
|
|
12
|
+
const extraArgs = argv.filter((a) => a !== "-v" && a !== "--verbose");
|
|
13
|
+
|
|
14
|
+
const proc = Bun.spawn(["bun", "test", ...extraArgs], {
|
|
15
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
16
|
+
cwd: import.meta.dir + "/../..",
|
|
17
|
+
env: { ...process.env, LOG_LEVEL: "silent" },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
|
21
|
+
const exitCode = await proc.exited;
|
|
22
|
+
const output = stdout + stderr;
|
|
23
|
+
|
|
24
|
+
if (verbose) {
|
|
25
|
+
process.stdout.write(output);
|
|
26
|
+
} else {
|
|
27
|
+
for (const line of output.split("\n")) {
|
|
28
|
+
if (isWorthShowing(line)) console.log(line);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
process.exit(exitCode);
|
|
32
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isRunning, readPid } from "../utils/pid";
|
|
2
|
+
import { errMsg } from "../utils/errors";
|
|
3
|
+
import { guardActiveEngines, parseGuardFlags, withDefaultWait } from "../core/engine-guard";
|
|
4
|
+
import { startDaemon, stopDaemon } from "../core/daemon";
|
|
5
|
+
|
|
6
|
+
/** `nia update` — upgrade the global install, then restart if the daemon is up. */
|
|
7
|
+
export async function updateCommand(argv: string[]): Promise<void> {
|
|
8
|
+
const guard = withDefaultWait(parseGuardFlags(argv), 1);
|
|
9
|
+
const { version: currentVersion } = await import("../../package.json");
|
|
10
|
+
console.log(`Current: v${currentVersion}`);
|
|
11
|
+
|
|
12
|
+
// Never upgrade out from under a live turn.
|
|
13
|
+
if (isRunning() && !(await guardActiveEngines("update", guard))) process.exit(1);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const { createBackup } = await import("../commands/backup");
|
|
17
|
+
console.log("Backing up...");
|
|
18
|
+
await createBackup(true);
|
|
19
|
+
console.log("✓ pre-update backup created");
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.log(`⚠ backup skipped: ${errMsg(err)}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log("Updating...");
|
|
25
|
+
const install = Bun.spawn(["npm", "i", "-g", "niahere@latest"], { stdio: ["ignore", "inherit", "inherit"] });
|
|
26
|
+
if ((await install.exited) !== 0) {
|
|
27
|
+
console.error("Update failed.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const check = Bun.spawn(["npm", "view", "niahere", "version"], { stdout: "pipe", stderr: "pipe" });
|
|
32
|
+
const newVersion = (await new Response(check.stdout).text()).trim();
|
|
33
|
+
await check.exited;
|
|
34
|
+
|
|
35
|
+
if (newVersion === currentVersion) {
|
|
36
|
+
console.log("Already on latest.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(`Updated: v${currentVersion} → v${newVersion}`);
|
|
41
|
+
if (!isRunning()) return;
|
|
42
|
+
|
|
43
|
+
console.log("Restarting daemon...");
|
|
44
|
+
const { isServiceInstalled, restartService } = await import("../commands/service");
|
|
45
|
+
if (isServiceInstalled()) {
|
|
46
|
+
await restartService({ force: guard.force });
|
|
47
|
+
} else {
|
|
48
|
+
stopDaemon({ force: guard.force });
|
|
49
|
+
startDaemon();
|
|
50
|
+
}
|
|
51
|
+
console.log("Restarted.");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `nia channels <on|off> [name]` — toggle channels and nudge a running daemon. */
|
|
55
|
+
export async function channelsToggleCommand(sub: string, target?: string): Promise<void> {
|
|
56
|
+
const { updateRawConfig } = await import("../utils/config");
|
|
57
|
+
const SUPPORTED = new Set(["telegram", "slack", "phone", "sms", "whatsapp"]);
|
|
58
|
+
const enabled = sub === "on";
|
|
59
|
+
|
|
60
|
+
if (target) {
|
|
61
|
+
if (!SUPPORTED.has(target)) {
|
|
62
|
+
console.error(`Usage: nia channels <on|off> [${[...SUPPORTED].join("|")}]`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
updateRawConfig({ channels: { ...(enabled ? { enabled: true } : {}), [target]: { enabled } } });
|
|
66
|
+
} else {
|
|
67
|
+
updateRawConfig({ channels: { enabled } });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const what = target ?? "channels";
|
|
71
|
+
const state = enabled ? "enabled" : "disabled";
|
|
72
|
+
const pid = readPid();
|
|
73
|
+
if (pid && isRunning()) {
|
|
74
|
+
process.kill(pid, "SIGHUP");
|
|
75
|
+
console.log(`${what} ${state}`);
|
|
76
|
+
} else {
|
|
77
|
+
console.log(`${what} ${state} — start nia to apply`);
|
|
78
|
+
}
|
|
79
|
+
}
|