niahere 0.2.13 → 0.2.14

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/src/cli/index.ts CHANGED
@@ -136,10 +136,58 @@ switch (command) {
136
136
  if (prompt) {
137
137
  const { createChatEngine } = await import("../chat/engine");
138
138
  const { getMcpServers } = await import("../mcp");
139
+ const DIM = "\x1b[2m";
140
+ const RST = "\x1b[0m";
141
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
142
+ let frame = 0;
143
+ let statusText = "thinking";
144
+ let spinTimer: ReturnType<typeof setInterval> | null = null;
145
+ let streamedLen = 0;
146
+ let streaming = false;
147
+
148
+ const renderSpinner = () => {
149
+ process.stderr.write(`\x1b[2K\r${DIM} ${FRAMES[frame]} ${statusText}${RST}`);
150
+ frame = (frame + 1) % FRAMES.length;
151
+ };
152
+
139
153
  await withDb(async () => {
140
154
  const engine = await createChatEngine({ room: "cli-run", channel: "terminal", resume: false, mcpServers: getMcpServers() });
141
- const { result } = await engine.send(prompt);
142
- console.log(result.trim());
155
+ spinTimer = setInterval(renderSpinner, 80);
156
+ renderSpinner();
157
+
158
+ const { result, costUsd, turns } = await engine.send(prompt, {
159
+ onStream(textSoFar) {
160
+ if (!streaming) {
161
+ if (spinTimer) { clearInterval(spinTimer); spinTimer = null; }
162
+ process.stderr.write("\x1b[2K\r");
163
+ streaming = true;
164
+ }
165
+ const chunk = textSoFar.slice(streamedLen);
166
+ if (chunk) { process.stdout.write(chunk); streamedLen = textSoFar.length; }
167
+ },
168
+ onActivity(text) {
169
+ if (!streaming) statusText = text;
170
+ },
171
+ });
172
+
173
+ if (spinTimer) { clearInterval(spinTimer); spinTimer = null; }
174
+
175
+ if (!streaming && result.trim()) {
176
+ process.stderr.write("\x1b[2K\r");
177
+ process.stdout.write(result.trim());
178
+ } else if (streaming) {
179
+ const rest = result.slice(streamedLen);
180
+ if (rest.trim()) process.stdout.write(rest);
181
+ } else {
182
+ process.stderr.write("\x1b[2K\r");
183
+ }
184
+
185
+ const costStr = costUsd > 0 ? `$${costUsd.toFixed(4)}` : "";
186
+ const turnsStr = turns > 0 ? `${turns} turn${turns !== 1 ? "s" : ""}` : "";
187
+ const meta = [costStr, turnsStr].filter(Boolean).join(" · ");
188
+ if (meta) process.stderr.write(`\n${DIM}${meta}${RST}`);
189
+ process.stdout.write("\n");
190
+
143
191
  engine.close();
144
192
  });
145
193
  } else {
@@ -192,8 +240,13 @@ switch (command) {
192
240
  }
193
241
 
194
242
  case "chat": {
195
- const resume = process.argv[3] === "--resume" || process.argv[3] === "-r";
196
- await startRepl(resume);
243
+ const arg = process.argv[3];
244
+ const mode = (arg === "--new" || arg === "-n")
245
+ ? "new" as const
246
+ : (arg === "--resume" || arg === "-r")
247
+ ? "pick" as const
248
+ : "continue" as const;
249
+ await startRepl(mode);
197
250
  break;
198
251
  }
199
252
 
@@ -223,6 +276,59 @@ switch (command) {
223
276
  break;
224
277
  }
225
278
 
279
+ case "config": {
280
+ const configSub = process.argv[3];
281
+ const configKey = process.argv[4];
282
+ const configVal = process.argv.slice(5).join(" ");
283
+ const { readRawConfig, updateRawConfig } = await import("../utils/config");
284
+
285
+ if (configSub === "set" && configKey) {
286
+ if (!configVal) fail("Usage: nia config set <key> <value>");
287
+ // Support dot notation for nested keys (e.g. channels.default)
288
+ const parts = configKey.split(".");
289
+ let obj: Record<string, unknown> = {};
290
+ let cursor = obj;
291
+ for (let i = 0; i < parts.length - 1; i++) {
292
+ cursor[parts[i]] = {};
293
+ cursor = cursor[parts[i]] as Record<string, unknown>;
294
+ }
295
+ // Auto-detect booleans and numbers
296
+ let parsed: unknown = configVal;
297
+ if (configVal === "true") parsed = true;
298
+ else if (configVal === "false") parsed = false;
299
+ else if (/^\d+$/.test(configVal)) parsed = Number(configVal);
300
+ cursor[parts[parts.length - 1]] = parsed;
301
+ updateRawConfig(obj);
302
+ console.log(`${configKey} = ${configVal}`);
303
+ } else if (configSub === "get" && configKey) {
304
+ const raw = readRawConfig();
305
+ const parts = configKey.split(".");
306
+ let val: unknown = raw;
307
+ for (const p of parts) {
308
+ if (val && typeof val === "object") val = (val as Record<string, unknown>)[p];
309
+ else { val = undefined; break; }
310
+ }
311
+ if (val === undefined) {
312
+ console.log(`${configKey}: (not set)`);
313
+ } else if (typeof val === "object") {
314
+ const yaml = (await import("js-yaml")).default;
315
+ console.log(yaml.dump(val, { lineWidth: -1 }).trim());
316
+ } else {
317
+ console.log(`${configKey} = ${val}`);
318
+ }
319
+ } else if (!configSub || configSub === "list") {
320
+ const raw = readRawConfig();
321
+ const yaml = (await import("js-yaml")).default;
322
+ console.log(yaml.dump(raw, { lineWidth: -1 }).trim());
323
+ } else {
324
+ console.log("Usage: nia config <set|get|list>");
325
+ console.log(" nia config set <key> <value> — set a config value");
326
+ console.log(" nia config get <key> — get a config value");
327
+ console.log(" nia config list — show all config");
328
+ }
329
+ break;
330
+ }
331
+
226
332
  case "channels": {
227
333
  const sub = process.argv[3];
228
334
  const { updateRawConfig } = await import("../utils/config");
@@ -286,13 +392,14 @@ switch (command) {
286
392
  console.log(" start / stop — daemon + service control");
287
393
  console.log(" restart — restart daemon");
288
394
  console.log(" status [--json --rooms N --all] — show daemon, jobs, channels");
289
- console.log(" chat [-r|--resume] — interactive chat");
395
+ console.log(" chat — interactive chat (auto-continues last session)");
290
396
  console.log(" run <prompt> — one-shot execution");
291
397
  console.log(" history [room] — recent messages");
292
398
  console.log(" logs [-f] — daemon logs");
293
399
  console.log(" job <sub> — manage jobs");
294
400
  console.log(" db <sub> — database setup/status/migrate");
295
401
  console.log(" skills — list available skills");
402
+ console.log(" config <sub> — get/set/list config values");
296
403
  console.log(" send [-c ch] <msg> — send a message via channel");
297
404
  console.log(" telegram <token> — configure telegram");
298
405
  console.log(" slack <bot> <app> — configure slack");
@@ -1,5 +1,14 @@
1
1
  import { getSql } from "../connection";
2
2
 
3
+ export interface SessionSummary {
4
+ id: string;
5
+ room: string;
6
+ createdAt: string;
7
+ updatedAt: string;
8
+ preview: string | null;
9
+ messageCount: number;
10
+ }
11
+
3
12
  export async function getLatest(room: string): Promise<string | null> {
4
13
  const sql = getSql();
5
14
  const rows = await sql`
@@ -11,6 +20,35 @@ export async function getLatest(room: string): Promise<string | null> {
11
20
  return rows.length > 0 ? rows[0].id : null;
12
21
  }
13
22
 
23
+ export async function getRecent(room: string, limit = 10): Promise<SessionSummary[]> {
24
+ const sql = getSql();
25
+ const rows = await sql`
26
+ SELECT
27
+ s.id,
28
+ s.room,
29
+ s.created_at,
30
+ s.updated_at,
31
+ (
32
+ SELECT content FROM messages m
33
+ WHERE m.session_id = s.id AND m.sender = 'user'
34
+ ORDER BY m.created_at ASC LIMIT 1
35
+ ) AS preview,
36
+ (SELECT COUNT(*)::int FROM messages m WHERE m.session_id = s.id) AS message_count
37
+ FROM sessions s
38
+ WHERE s.room = ${room}
39
+ ORDER BY s.updated_at DESC
40
+ LIMIT ${limit}
41
+ `;
42
+ return rows.map((r) => ({
43
+ id: r.id,
44
+ room: r.room,
45
+ createdAt: String(r.created_at),
46
+ updatedAt: String(r.updated_at),
47
+ preview: r.preview ? String(r.preview) : null,
48
+ messageCount: r.message_count,
49
+ }));
50
+ }
51
+
14
52
  export async function create(id: string, room: string): Promise<void> {
15
53
  const sql = getSql();
16
54
  await sql`INSERT INTO sessions (id, room) VALUES (${id}, ${room})`;
@@ -22,6 +22,7 @@ export interface ChatEngine {
22
22
  export interface EngineOptions {
23
23
  room: string;
24
24
  channel: string;
25
- resume: boolean;
25
+ /** true = resume latest session, or pass a specific session ID */
26
+ resume: boolean | string;
26
27
  mcpServers?: Record<string, unknown>;
27
28
  }