niahere 0.2.76 → 0.2.77
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 +1 -1
- package/src/chat/engine.ts +15 -2
- package/src/cli/index.ts +6 -6
- package/src/commands/service.ts +10 -3
- package/src/core/active-handles.ts +31 -0
- package/src/core/daemon.ts +16 -5
- package/src/core/engine-guard.ts +1 -1
- package/src/core/force-shutdown.ts +52 -0
- package/src/core/runner.ts +36 -3
- package/src/prompts/environment.md +7 -0
- package/src/prompts/index.ts +8 -0
- package/src/utils/os.ts +33 -0
package/package.json
CHANGED
package/src/chat/engine.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { truncate, formatToolUse } from "../utils/format-activity";
|
|
|
23
23
|
import { finalizeSession, cancelPending } from "../core/finalizer";
|
|
24
24
|
import { log } from "../utils/log";
|
|
25
25
|
import { isRetryableApiError, sleep } from "../utils/retry";
|
|
26
|
+
import { registerActiveHandle, unregisterActiveHandle } from "../core/active-handles";
|
|
26
27
|
|
|
27
28
|
const IDLE_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
28
29
|
const LONG_RUNNING_WARN = 30 * 60 * 1000; // 30 minutes
|
|
@@ -272,9 +273,20 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
|
|
|
272
273
|
queryHandle.close();
|
|
273
274
|
queryHandle = null;
|
|
274
275
|
}
|
|
276
|
+
unregisterActiveHandle(room);
|
|
275
277
|
alive = false;
|
|
276
278
|
}
|
|
277
279
|
|
|
280
|
+
async function abortActiveQuery(reason: string) {
|
|
281
|
+
const activePending = pending;
|
|
282
|
+
pending = null;
|
|
283
|
+
if (activePending) {
|
|
284
|
+
activePending.reject(new Error(reason));
|
|
285
|
+
}
|
|
286
|
+
teardown();
|
|
287
|
+
await ActiveEngine.unregister(room).catch(() => {});
|
|
288
|
+
}
|
|
289
|
+
|
|
278
290
|
function startQuery() {
|
|
279
291
|
stream = new MessageStream();
|
|
280
292
|
alive = true;
|
|
@@ -309,6 +321,7 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
|
|
|
309
321
|
prompt: stream as any,
|
|
310
322
|
options: options as any,
|
|
311
323
|
});
|
|
324
|
+
registerActiveHandle(room, abortActiveQuery);
|
|
312
325
|
|
|
313
326
|
// Background consumer — runs for the lifetime of the query
|
|
314
327
|
(async () => {
|
|
@@ -523,6 +536,7 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
|
|
|
523
536
|
}
|
|
524
537
|
} finally {
|
|
525
538
|
clearLongRunningTimer();
|
|
539
|
+
unregisterActiveHandle(room);
|
|
526
540
|
alive = false;
|
|
527
541
|
stream = null;
|
|
528
542
|
queryHandle = null;
|
|
@@ -596,8 +610,7 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
|
|
|
596
610
|
log.error({ err, room }, "finalization enqueue failed during close");
|
|
597
611
|
}
|
|
598
612
|
}
|
|
599
|
-
|
|
600
|
-
await ActiveEngine.unregister(room).catch(() => {});
|
|
613
|
+
await abortActiveQuery("chat engine closed");
|
|
601
614
|
},
|
|
602
615
|
};
|
|
603
616
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -116,8 +116,8 @@ switch (command) {
|
|
|
116
116
|
const stopGuard = parseGuardFlags(process.argv.slice(3));
|
|
117
117
|
if (!(await guardActiveEngines("stop", stopGuard))) process.exit(1);
|
|
118
118
|
const { unregisterService } = await import("../commands/service");
|
|
119
|
-
await unregisterService();
|
|
120
|
-
stopDaemon();
|
|
119
|
+
await unregisterService({ force: stopGuard.force });
|
|
120
|
+
stopDaemon({ force: stopGuard.force });
|
|
121
121
|
console.log("nia stopped");
|
|
122
122
|
break;
|
|
123
123
|
}
|
|
@@ -138,9 +138,9 @@ switch (command) {
|
|
|
138
138
|
if (!(await guardActiveEngines("restart", restartGuard))) process.exit(1);
|
|
139
139
|
const { isServiceInstalled, restartService } = await import("../commands/service");
|
|
140
140
|
if (isServiceInstalled()) {
|
|
141
|
-
await restartService();
|
|
141
|
+
await restartService({ force: restartGuard.force });
|
|
142
142
|
} else {
|
|
143
|
-
stopDaemon();
|
|
143
|
+
stopDaemon({ force: restartGuard.force });
|
|
144
144
|
startDaemon();
|
|
145
145
|
}
|
|
146
146
|
const restartPid = readPid();
|
|
@@ -536,9 +536,9 @@ switch (command) {
|
|
|
536
536
|
console.log("Restarting daemon...");
|
|
537
537
|
const { isServiceInstalled, restartService } = await import("../commands/service");
|
|
538
538
|
if (isServiceInstalled()) {
|
|
539
|
-
await restartService();
|
|
539
|
+
await restartService({ force: updateGuard.force });
|
|
540
540
|
} else {
|
|
541
|
-
stopDaemon();
|
|
541
|
+
stopDaemon({ force: updateGuard.force });
|
|
542
542
|
startDaemon();
|
|
543
543
|
}
|
|
544
544
|
console.log("Restarted.");
|
package/src/commands/service.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "path";
|
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { getPaths } from "../utils/paths";
|
|
5
5
|
import { findDaemonPids } from "../core/daemon";
|
|
6
|
+
import { clearForceShutdownRequest, requestForceShutdown } from "../core/force-shutdown";
|
|
6
7
|
|
|
7
8
|
const PLIST_NAME = "com.niahere.agent";
|
|
8
9
|
const SYSTEMD_UNIT = "niahere.service";
|
|
@@ -173,7 +174,8 @@ export async function registerService(): Promise<void> {
|
|
|
173
174
|
// Windows/other: no-op, daemon still works via startDaemon()
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
export async function unregisterService(): Promise<void> {
|
|
177
|
+
export async function unregisterService(opts: { force?: boolean } = {}): Promise<void> {
|
|
178
|
+
if (opts.force) requestForceShutdown(findDaemonPids());
|
|
177
179
|
if (process.platform === "darwin") {
|
|
178
180
|
await uninstallLaunchd();
|
|
179
181
|
} else if (process.platform === "linux") {
|
|
@@ -188,7 +190,9 @@ export function isServiceInstalled(): boolean {
|
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
/** Restart via service manager. Waits for old process to fully exit before starting new one. */
|
|
191
|
-
export async function restartService(): Promise<void> {
|
|
193
|
+
export async function restartService(opts: { force?: boolean } = {}): Promise<void> {
|
|
194
|
+
if (opts.force) requestForceShutdown(findDaemonPids());
|
|
195
|
+
const waitMs = opts.force ? 30_000 : 310_000;
|
|
192
196
|
if (process.platform === "darwin") {
|
|
193
197
|
const path = plistPath();
|
|
194
198
|
// Unload — sends SIGTERM and disables KeepAlive respawn
|
|
@@ -198,7 +202,8 @@ export async function restartService(): Promise<void> {
|
|
|
198
202
|
console.error(` warning: launchctl unload failed: ${stderr.trim()}`);
|
|
199
203
|
}
|
|
200
204
|
// Wait for old daemon to fully exit (graceful shutdown may take time for active engines)
|
|
201
|
-
await waitForDaemonExit(
|
|
205
|
+
await waitForDaemonExit(waitMs);
|
|
206
|
+
if (opts.force) clearForceShutdownRequest();
|
|
202
207
|
// Load — starts a fresh single instance
|
|
203
208
|
const load = Bun.spawn(["launchctl", "load", path], { stdout: "pipe", stderr: "pipe" });
|
|
204
209
|
if (await load.exited !== 0) {
|
|
@@ -209,6 +214,8 @@ export async function restartService(): Promise<void> {
|
|
|
209
214
|
// systemd restart handles stop-wait-start internally
|
|
210
215
|
const restart = Bun.spawn(["systemctl", "--user", "restart", SYSTEMD_UNIT], { stdout: "pipe", stderr: "pipe" });
|
|
211
216
|
await restart.exited;
|
|
217
|
+
if (opts.force) await waitForDaemonExit(waitMs);
|
|
218
|
+
if (opts.force) clearForceShutdownRequest();
|
|
212
219
|
}
|
|
213
220
|
}
|
|
214
221
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { log } from "../utils/log";
|
|
2
|
+
|
|
3
|
+
type CloseHandle = (reason: string) => void | Promise<void>;
|
|
4
|
+
|
|
5
|
+
const handles = new Map<string, CloseHandle>();
|
|
6
|
+
|
|
7
|
+
export function registerActiveHandle(room: string, close: CloseHandle): void {
|
|
8
|
+
handles.set(room, close);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function unregisterActiveHandle(room: string): void {
|
|
12
|
+
handles.delete(room);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function activeHandleCount(): number {
|
|
16
|
+
return handles.size;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function closeAllActiveHandles(reason: string): Promise<number> {
|
|
20
|
+
const entries = [...handles.entries()];
|
|
21
|
+
for (const [room, close] of entries) {
|
|
22
|
+
try {
|
|
23
|
+
await close(reason);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
log.warn({ err, room }, "failed to close active handle");
|
|
26
|
+
} finally {
|
|
27
|
+
handles.delete(room);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return entries.length;
|
|
31
|
+
}
|
package/src/core/daemon.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { startAlive, stopAlive } from "./alive";
|
|
|
15
15
|
import { createNiaMcpServer } from "../mcp/server";
|
|
16
16
|
import { setMcpFactory } from "../mcp";
|
|
17
17
|
import { processPending, cleanupOldRequests } from "./finalizer";
|
|
18
|
+
import { closeAllActiveHandles } from "./active-handles";
|
|
19
|
+
import { clearForceShutdownRequest, consumeForceShutdownRequest, requestForceShutdown } from "./force-shutdown";
|
|
18
20
|
|
|
19
21
|
export { isRunning, readPid, removePid, writePid };
|
|
20
22
|
|
|
@@ -46,8 +48,11 @@ export function startDaemon(): number {
|
|
|
46
48
|
return pid;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
export function stopDaemon(): boolean {
|
|
51
|
+
export function stopDaemon(opts: { force?: boolean } = {}): boolean {
|
|
50
52
|
const pidfilePid = readPid();
|
|
53
|
+
if (opts.force) {
|
|
54
|
+
requestForceShutdown([...(pidfilePid ? [pidfilePid] : []), ...findDaemonPids()]);
|
|
55
|
+
}
|
|
51
56
|
removePid();
|
|
52
57
|
|
|
53
58
|
// Kill all daemon processes — pidfile PID plus any orphans.
|
|
@@ -55,7 +60,8 @@ export function stopDaemon(): boolean {
|
|
|
55
60
|
if (killed === 0 && pidfilePid === null) return false;
|
|
56
61
|
|
|
57
62
|
// Wait for processes to finish (up to 5 min for active engines, then SIGKILL)
|
|
58
|
-
waitForExit(310_000);
|
|
63
|
+
waitForExit(opts.force ? 30_000 : 310_000);
|
|
64
|
+
if (opts.force) clearForceShutdownRequest();
|
|
59
65
|
return true;
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -361,16 +367,21 @@ export async function runDaemon(): Promise<void> {
|
|
|
361
367
|
const shutdown = async () => {
|
|
362
368
|
if (shuttingDown) return;
|
|
363
369
|
shuttingDown = true;
|
|
370
|
+
const force = consumeForceShutdownRequest(process.pid);
|
|
364
371
|
|
|
365
|
-
log.info("shutting down...");
|
|
372
|
+
log.info({ force }, "shutting down...");
|
|
366
373
|
|
|
367
374
|
stopAlive();
|
|
368
375
|
stopScheduler();
|
|
369
376
|
await stopChannels(channels);
|
|
370
377
|
|
|
371
378
|
try {
|
|
379
|
+
if (force) {
|
|
380
|
+
const closed = await closeAllActiveHandles("force shutdown");
|
|
381
|
+
log.warn({ closed }, "force closed active engines");
|
|
382
|
+
}
|
|
372
383
|
const engines = await ActiveEngine.list();
|
|
373
|
-
if (engines.length > 0) {
|
|
384
|
+
if (engines.length > 0 && !force) {
|
|
374
385
|
log.info({ count: engines.length }, "waiting for active engines to finish");
|
|
375
386
|
const deadline = Date.now() + 300_000;
|
|
376
387
|
while (Date.now() < deadline) {
|
|
@@ -378,8 +389,8 @@ export async function runDaemon(): Promise<void> {
|
|
|
378
389
|
if (remaining.length === 0) break;
|
|
379
390
|
await new Promise((r) => setTimeout(r, 1000));
|
|
380
391
|
}
|
|
381
|
-
await ActiveEngine.clearAll();
|
|
382
392
|
}
|
|
393
|
+
if (engines.length > 0 || force) await ActiveEngine.clearAll();
|
|
383
394
|
} catch {
|
|
384
395
|
// postgres may be gone
|
|
385
396
|
}
|
package/src/core/engine-guard.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Default: warn and refuse.
|
|
5
5
|
* --wait <minutes>: poll until engines clear, then proceed. Times out with error.
|
|
6
|
-
* --force: skip the
|
|
6
|
+
* --force: skip the guard and ask the daemon to close active handles.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { ActiveEngine } from "../db/models";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { getNiaHome } from "../utils/paths";
|
|
4
|
+
|
|
5
|
+
const FORCE_MARKER_MAX_AGE_MS = 10 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
interface ForceShutdownMarker {
|
|
8
|
+
createdAt: number;
|
|
9
|
+
pids: number[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function markerPath(): string {
|
|
13
|
+
return join(getNiaHome(), "tmp", "force-shutdown.json");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function requestForceShutdown(pids: number[] = []): void {
|
|
17
|
+
const path = markerPath();
|
|
18
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
19
|
+
const uniquePids = [...new Set(pids.filter((pid) => Number.isInteger(pid) && pid > 0))];
|
|
20
|
+
const marker: ForceShutdownMarker = { createdAt: Date.now(), pids: uniquePids };
|
|
21
|
+
writeFileSync(path, JSON.stringify(marker));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function clearForceShutdownRequest(): void {
|
|
25
|
+
try {
|
|
26
|
+
unlinkSync(markerPath());
|
|
27
|
+
} catch {
|
|
28
|
+
// already gone
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function consumeForceShutdownRequest(pid: number = process.pid): boolean {
|
|
33
|
+
const path = markerPath();
|
|
34
|
+
if (!existsSync(path)) return false;
|
|
35
|
+
|
|
36
|
+
let marker: ForceShutdownMarker;
|
|
37
|
+
try {
|
|
38
|
+
marker = JSON.parse(readFileSync(path, "utf8"));
|
|
39
|
+
} catch {
|
|
40
|
+
clearForceShutdownRequest();
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (Date.now() - marker.createdAt > FORCE_MARKER_MAX_AGE_MS) {
|
|
45
|
+
clearForceShutdownRequest();
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const applies = marker.pids.length === 0 || marker.pids.includes(pid);
|
|
50
|
+
if (applies) clearForceShutdownRequest();
|
|
51
|
+
return applies;
|
|
52
|
+
}
|
package/src/core/runner.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { ActiveEngine } from "../db/models";
|
|
|
17
17
|
import { getPaths } from "../utils/paths";
|
|
18
18
|
import { log } from "../utils/log";
|
|
19
19
|
import { isRetryableApiError, sleep } from "../utils/retry";
|
|
20
|
+
import { registerActiveHandle, unregisterActiveHandle } from "./active-handles";
|
|
20
21
|
|
|
21
22
|
export type ActivityCallback = (line: string) => void;
|
|
22
23
|
|
|
@@ -98,6 +99,7 @@ export async function runJobWithClaude(
|
|
|
98
99
|
onActivity?: ActivityCallback,
|
|
99
100
|
model?: string,
|
|
100
101
|
sourceCtx?: McpSourceContext,
|
|
102
|
+
activeRoom?: string,
|
|
101
103
|
): Promise<RunnerOutput> {
|
|
102
104
|
const sessionId = randomUUID();
|
|
103
105
|
|
|
@@ -131,6 +133,13 @@ export async function runJobWithClaude(
|
|
|
131
133
|
prompt: singleMessage() as any,
|
|
132
134
|
options: options as any,
|
|
133
135
|
});
|
|
136
|
+
let abortReason: string | null = null;
|
|
137
|
+
if (activeRoom) {
|
|
138
|
+
registerActiveHandle(activeRoom, (reason) => {
|
|
139
|
+
abortReason = reason;
|
|
140
|
+
handle.close();
|
|
141
|
+
});
|
|
142
|
+
}
|
|
134
143
|
|
|
135
144
|
let agentText = "";
|
|
136
145
|
let actualSessionId = sessionId;
|
|
@@ -214,8 +223,28 @@ export async function runJobWithClaude(
|
|
|
214
223
|
}
|
|
215
224
|
}
|
|
216
225
|
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
if (abortReason) {
|
|
228
|
+
return {
|
|
229
|
+
agentText: "",
|
|
230
|
+
sessionId: actualSessionId,
|
|
231
|
+
terminalReason: "aborted",
|
|
232
|
+
error: abortReason,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
throw err;
|
|
217
236
|
} finally {
|
|
218
237
|
handle.close();
|
|
238
|
+
if (activeRoom) unregisterActiveHandle(activeRoom);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (abortReason) {
|
|
242
|
+
return {
|
|
243
|
+
agentText: "",
|
|
244
|
+
sessionId: actualSessionId,
|
|
245
|
+
terminalReason: "aborted",
|
|
246
|
+
error: abortReason,
|
|
247
|
+
};
|
|
219
248
|
}
|
|
220
249
|
|
|
221
250
|
return { agentText, sessionId: actualSessionId, terminalReason };
|
|
@@ -243,7 +272,7 @@ export async function runTask(opts: TaskOptions): Promise<RunnerOutput> {
|
|
|
243
272
|
await ActiveEngine.register(room, "system").catch(() => {});
|
|
244
273
|
try {
|
|
245
274
|
const systemPrompt = opts.systemPrompt || buildSystemPrompt("job");
|
|
246
|
-
const output = await runJobWithClaude(systemPrompt, opts.prompt, homedir());
|
|
275
|
+
const output = await runJobWithClaude(systemPrompt, opts.prompt, homedir(), undefined, undefined, undefined, room);
|
|
247
276
|
if (output.error) {
|
|
248
277
|
log.error({ task: opts.name, error: output.error }, "task failed");
|
|
249
278
|
} else {
|
|
@@ -297,11 +326,13 @@ export async function runJob(job: JobInput, onActivity?: ActivityCallback): Prom
|
|
|
297
326
|
const config = getConfig();
|
|
298
327
|
const timestamp = new Date().toISOString();
|
|
299
328
|
const startMs = performance.now();
|
|
329
|
+
const room = `job/${job.name}`;
|
|
300
330
|
|
|
301
331
|
// Update state: running
|
|
302
332
|
const state: Record<string, JobState> = { ...readState() };
|
|
303
333
|
state[job.name] = { lastRun: timestamp, status: "running", duration_ms: 0 };
|
|
304
334
|
writeState(state);
|
|
335
|
+
await ActiveEngine.register(room, "job").catch(() => {});
|
|
305
336
|
|
|
306
337
|
try {
|
|
307
338
|
let cwd = homedir();
|
|
@@ -352,7 +383,7 @@ export async function runJob(job: JobInput, onActivity?: ActivityCallback): Prom
|
|
|
352
383
|
const fullPrompt = `${systemPrompt}\n\n---\n\n${jobPrompt}`;
|
|
353
384
|
output = await runJobWithCodex(fullPrompt, cwd, resolvedModel);
|
|
354
385
|
} else {
|
|
355
|
-
output = await runJobWithClaude(systemPrompt, jobPrompt, cwd, onActivity, resolvedModel, jobSourceCtx);
|
|
386
|
+
output = await runJobWithClaude(systemPrompt, jobPrompt, cwd, onActivity, resolvedModel, jobSourceCtx, room);
|
|
356
387
|
|
|
357
388
|
for (let attempt = 0; attempt < MAX_API_RETRIES && output.error && isRetryableApiError(output.error); attempt++) {
|
|
358
389
|
const delay = RETRY_DELAYS[attempt] ?? 8_000;
|
|
@@ -361,7 +392,7 @@ export async function runJob(job: JobInput, onActivity?: ActivityCallback): Prom
|
|
|
361
392
|
"retrying after transient API error",
|
|
362
393
|
);
|
|
363
394
|
await sleep(delay);
|
|
364
|
-
output = await runJobWithClaude(systemPrompt, jobPrompt, cwd, onActivity, resolvedModel, jobSourceCtx);
|
|
395
|
+
output = await runJobWithClaude(systemPrompt, jobPrompt, cwd, onActivity, resolvedModel, jobSourceCtx, room);
|
|
365
396
|
}
|
|
366
397
|
}
|
|
367
398
|
|
|
@@ -435,5 +466,7 @@ export async function runJob(job: JobInput, onActivity?: ActivityCallback): Prom
|
|
|
435
466
|
writeState(freshState);
|
|
436
467
|
|
|
437
468
|
return result;
|
|
469
|
+
} finally {
|
|
470
|
+
await ActiveEngine.unregister(room).catch(() => {});
|
|
438
471
|
}
|
|
439
472
|
}
|
|
@@ -9,6 +9,13 @@ You are running as part of the assistant daemon.
|
|
|
9
9
|
- Current date (authoritative): {{currentDate}}
|
|
10
10
|
- Current time: {{currentTime}}
|
|
11
11
|
|
|
12
|
+
## Runtime OS
|
|
13
|
+
|
|
14
|
+
- OS: {{osName}} ({{osType}} {{osRelease}})
|
|
15
|
+
- Platform: {{osPlatform}}
|
|
16
|
+
- Architecture: {{osArch}}
|
|
17
|
+
- Shell: {{shell}}
|
|
18
|
+
|
|
12
19
|
When writing calendar digests, standups, reminders, or any dated message, preserve the weekday/date pairing from the authoritative current date above. If a weekday/date mismatch would matter, verify with the system date or source calendar before sending.
|
|
13
20
|
|
|
14
21
|
## Nia CLI
|
package/src/prompts/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join, resolve } from "path";
|
|
|
3
3
|
import { getPaths } from "../utils/paths";
|
|
4
4
|
import { getConfig } from "../utils/config";
|
|
5
5
|
import { formatPromptDate, formatPromptDateTime } from "../utils/time";
|
|
6
|
+
import { getRuntimeOsInfo } from "../utils/os";
|
|
6
7
|
import type { Mode } from "../types";
|
|
7
8
|
|
|
8
9
|
const PROMPTS_DIR = resolve(import.meta.dir);
|
|
@@ -21,6 +22,7 @@ export function getEnvironmentPrompt(): string {
|
|
|
21
22
|
const paths = getPaths();
|
|
22
23
|
const config = getConfig();
|
|
23
24
|
const now = new Date();
|
|
25
|
+
const osInfo = getRuntimeOsInfo();
|
|
24
26
|
|
|
25
27
|
// Build watch channel summary if Slack is configured with watch channels
|
|
26
28
|
let slackWatch = "";
|
|
@@ -37,6 +39,12 @@ export function getEnvironmentPrompt(): string {
|
|
|
37
39
|
timezone: config.timezone,
|
|
38
40
|
currentDate: formatPromptDate(now, config.timezone),
|
|
39
41
|
currentTime: formatPromptDateTime(now, config.timezone),
|
|
42
|
+
osName: osInfo.osName,
|
|
43
|
+
osType: osInfo.osType,
|
|
44
|
+
osRelease: osInfo.osRelease,
|
|
45
|
+
osPlatform: osInfo.osPlatform,
|
|
46
|
+
osArch: osInfo.osArch,
|
|
47
|
+
shell: osInfo.shell,
|
|
40
48
|
activeStart: config.activeHours.start,
|
|
41
49
|
activeEnd: config.activeHours.end,
|
|
42
50
|
model: config.model,
|
package/src/utils/os.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { arch, platform, release, type } from "os";
|
|
2
|
+
|
|
3
|
+
function shellName(): string {
|
|
4
|
+
const raw = process.env.SHELL || process.env.ComSpec || "";
|
|
5
|
+
if (!raw) return "unknown";
|
|
6
|
+
const normalized = raw.replace(/\\/g, "/");
|
|
7
|
+
return normalized.split("/").filter(Boolean).pop() || "unknown";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function osName(osPlatform: NodeJS.Platform): string {
|
|
11
|
+
switch (osPlatform) {
|
|
12
|
+
case "darwin":
|
|
13
|
+
return "macOS";
|
|
14
|
+
case "linux":
|
|
15
|
+
return "Linux";
|
|
16
|
+
case "win32":
|
|
17
|
+
return "Windows";
|
|
18
|
+
default:
|
|
19
|
+
return type();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getRuntimeOsInfo(): Record<string, string> {
|
|
24
|
+
const osPlatform = platform();
|
|
25
|
+
return {
|
|
26
|
+
osName: osName(osPlatform),
|
|
27
|
+
osType: type(),
|
|
28
|
+
osRelease: release(),
|
|
29
|
+
osPlatform,
|
|
30
|
+
osArch: arch(),
|
|
31
|
+
shell: shellName(),
|
|
32
|
+
};
|
|
33
|
+
}
|