@tjamescouch/niki 0.5.4 → 0.5.6
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/bin/niki +119 -20
- package/package.json +1 -1
package/bin/niki
CHANGED
|
@@ -34,6 +34,7 @@ if (SEPARATOR === -1 || SEPARATOR === process.argv.length - 1) {
|
|
|
34
34
|
Usage: niki [options] -- <command> [args...]
|
|
35
35
|
|
|
36
36
|
Options:
|
|
37
|
+
--profile <name> Apply a preset profile (default: none; profiles: longrun)
|
|
37
38
|
--budget <tokens> Max total tokens (input+output) before SIGTERM (default: 1000000)
|
|
38
39
|
--timeout <seconds> Max wall-clock runtime before SIGTERM (default: 3600)
|
|
39
40
|
--max-sends <n> Max agentchat_send calls per minute (default: 10)
|
|
@@ -53,8 +54,10 @@ Options:
|
|
|
53
54
|
--restart Restart the child process when it exits (default: off)
|
|
54
55
|
--max-restarts <n> Max restart attempts, 0=unlimited (default: 0)
|
|
55
56
|
--restart-delay <secs> Delay between restarts with ±30% jitter (default: 5)
|
|
57
|
+
--kill-orphaned-mcp Kill stale agentchat-mcp processes on startup (default: off)
|
|
56
58
|
|
|
57
59
|
Examples:
|
|
60
|
+
niki --profile longrun -- gro --persistent --model sonnet
|
|
58
61
|
niki --budget 500000 -- claude -p "your prompt" --verbose
|
|
59
62
|
niki --timeout 1800 --max-sends 5 -- claude -p "..." --model sonnet --verbose
|
|
60
63
|
niki --restart --max-restarts 10 -- gro --model gpt-5.2 "your prompt"`);
|
|
@@ -65,28 +68,66 @@ const nikiArgs = process.argv.slice(2, SEPARATOR);
|
|
|
65
68
|
const childCmd = process.argv[SEPARATOR + 1];
|
|
66
69
|
const childArgs = process.argv.slice(SEPARATOR + 2);
|
|
67
70
|
|
|
71
|
+
function getProfileArg(args) {
|
|
72
|
+
const i = args.indexOf("--profile");
|
|
73
|
+
if (i === -1) return null;
|
|
74
|
+
const v = args[i + 1];
|
|
75
|
+
return v && !v.startsWith("-") ? v : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const DEFAULTS = {
|
|
79
|
+
budget: "1000000",
|
|
80
|
+
timeout: "3600",
|
|
81
|
+
"max-sends": "10",
|
|
82
|
+
"max-tool-calls": "30",
|
|
83
|
+
"stall-timeout": "60",
|
|
84
|
+
"startup-timeout": "180",
|
|
85
|
+
"dead-air-timeout": "5",
|
|
86
|
+
"max-nudges": "3",
|
|
87
|
+
cooldown: "5",
|
|
88
|
+
"poll-interval": "1000",
|
|
89
|
+
"max-restarts": "0",
|
|
90
|
+
"restart-delay": "5"
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const PROFILES = {
|
|
94
|
+
longrun: {
|
|
95
|
+
timeout: "86400",
|
|
96
|
+
"stall-timeout": "0",
|
|
97
|
+
"startup-timeout": "0",
|
|
98
|
+
"dead-air-timeout": "0",
|
|
99
|
+
cooldown: "15"
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const profileArg = getProfileArg(nikiArgs);
|
|
104
|
+
const D = profileArg && PROFILES[profileArg] ? { ...DEFAULTS, ...PROFILES[profileArg] } : DEFAULTS;
|
|
105
|
+
|
|
106
|
+
|
|
68
107
|
const { values: opts } = parseArgs({
|
|
69
108
|
args: nikiArgs,
|
|
70
109
|
options: {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
'max-
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'
|
|
110
|
+
profile: { type: 'string' },
|
|
111
|
+
budget: { type: 'string', default: D.budget },
|
|
112
|
+
timeout: { type: 'string', default: D.timeout },
|
|
113
|
+
'max-sends': { type: 'string', default: D["max-sends"] },
|
|
114
|
+
'max-tool-calls': { type: 'string', default: D["max-tool-calls"] },
|
|
115
|
+
'stall-timeout': { type: 'string', default: D["stall-timeout"] },
|
|
116
|
+
'startup-timeout': { type: 'string', default: D["startup-timeout"] },
|
|
117
|
+
'dead-air-timeout': { type: 'string', default: D["dead-air-timeout"] },
|
|
118
|
+
'max-nudges': { type: 'string', default: D["max-nudges"] },
|
|
79
119
|
log: { type: 'string' },
|
|
80
120
|
'log-level': { type: 'string', default: 'info' },
|
|
81
121
|
'log-json': { type: 'boolean', default: false },
|
|
82
122
|
state: { type: 'string' },
|
|
83
123
|
metrics: { type: 'string' },
|
|
84
|
-
cooldown: { type: 'string', default:
|
|
124
|
+
cooldown: { type: 'string', default: D.cooldown },
|
|
85
125
|
'abort-file': { type: 'string' },
|
|
86
|
-
'poll-interval': { type: 'string', default:
|
|
126
|
+
'poll-interval': { type: 'string', default: D["poll-interval"] },
|
|
87
127
|
restart: { type: 'boolean', default: false },
|
|
88
|
-
'max-restarts': { type: 'string', default:
|
|
89
|
-
'restart-delay': { type: 'string', default:
|
|
128
|
+
'max-restarts': { type: 'string', default: D["max-restarts"] },
|
|
129
|
+
'restart-delay': { type: 'string', default: D["restart-delay"] },
|
|
130
|
+
'kill-orphaned-mcp': { type: 'boolean', default: false },
|
|
90
131
|
},
|
|
91
132
|
});
|
|
92
133
|
|
|
@@ -107,6 +148,7 @@ const METRICS_FILE = opts.metrics;
|
|
|
107
148
|
const RESTART = opts.restart;
|
|
108
149
|
const MAX_RESTARTS = parseInt(opts['max-restarts'], 10);
|
|
109
150
|
const RESTART_DELAY_S = parseFloat(opts['restart-delay']);
|
|
151
|
+
const KILL_ORPHANED_MCP = opts['kill-orphaned-mcp'];
|
|
110
152
|
const LOG_JSON = opts['log-json'];
|
|
111
153
|
|
|
112
154
|
// --- Invocation rate limiting (prevent tight crash loops) ---
|
|
@@ -177,6 +219,13 @@ if (LOG_FILE) {
|
|
|
177
219
|
logStream = createWriteStream(resolve(LOG_FILE), { flags: 'a' });
|
|
178
220
|
}
|
|
179
221
|
|
|
222
|
+
// Format bytes/tokens in human-readable form
|
|
223
|
+
function fmtSize(n) {
|
|
224
|
+
if (n >= 1000000) return `${(n/1000000).toFixed(1)}M`;
|
|
225
|
+
if (n >= 1000) return `${(n/1000).toFixed(1)}K`;
|
|
226
|
+
return String(n);
|
|
227
|
+
}
|
|
228
|
+
|
|
180
229
|
function log(msg, level = 'info', fields = null) {
|
|
181
230
|
const numLevel = LOG_LEVELS[level] ?? LOG_LEVELS.info;
|
|
182
231
|
if (numLevel < LOG_LEVEL) return;
|
|
@@ -193,10 +242,27 @@ function log(msg, level = 'info', fields = null) {
|
|
|
193
242
|
const prefix = level === 'info' ? '' : `[${level.toUpperCase()}] `;
|
|
194
243
|
const line = `[${ts}] ${prefix}${msg}`;
|
|
195
244
|
if (logStream) logStream.write(line + '\n');
|
|
196
|
-
|
|
245
|
+
|
|
246
|
+
// For stderr: use concise format at info level, verbose at debug
|
|
247
|
+
if (level === 'debug' || LOG_LEVEL === LOG_LEVELS.debug) {
|
|
248
|
+
process.stderr.write(`[niki] ${line}\n`);
|
|
249
|
+
} else if (level === 'info') {
|
|
250
|
+
// Concise format: strip timestamp, just show the message
|
|
251
|
+
process.stderr.write(`[niki] ${msg}\n`);
|
|
252
|
+
} else {
|
|
253
|
+
// Warn/error: show full line
|
|
254
|
+
process.stderr.write(`[niki] ${line}\n`);
|
|
255
|
+
}
|
|
197
256
|
}
|
|
198
257
|
}
|
|
199
258
|
|
|
259
|
+
// Log API traffic in human-readable format
|
|
260
|
+
function logAPI(tokensIn, tokensOut) {
|
|
261
|
+
const budgetPct = ((state.tokensTotal / BUDGET) * 100).toFixed(1);
|
|
262
|
+
const msg = `[API: ${fmtSize(tokensIn)} → / ${fmtSize(tokensOut)} ←] Budget: ${fmtSize(state.tokensTotal)}/${fmtSize(BUDGET)} (${budgetPct}%)`;
|
|
263
|
+
log(msg, 'info');
|
|
264
|
+
}
|
|
265
|
+
|
|
200
266
|
function writeState() {
|
|
201
267
|
if (!STATE_FILE) return;
|
|
202
268
|
try {
|
|
@@ -263,6 +329,9 @@ const TOKEN_PATTERNS = [
|
|
|
263
329
|
|
|
264
330
|
function parseTokens(line) {
|
|
265
331
|
let changed = false;
|
|
332
|
+
const prevIn = state.tokensIn;
|
|
333
|
+
const prevOut = state.tokensOut;
|
|
334
|
+
|
|
266
335
|
for (const { regex, field } of TOKEN_PATTERNS) {
|
|
267
336
|
regex.lastIndex = 0;
|
|
268
337
|
let match;
|
|
@@ -278,6 +347,15 @@ function parseTokens(line) {
|
|
|
278
347
|
}
|
|
279
348
|
}
|
|
280
349
|
if (changed) {
|
|
350
|
+
const deltaIn = state.tokensIn - prevIn;
|
|
351
|
+
const deltaOut = state.tokensOut - prevOut;
|
|
352
|
+
|
|
353
|
+
// Human-readable API traffic log
|
|
354
|
+
if (deltaIn > 0 || deltaOut > 0) {
|
|
355
|
+
logAPI(deltaIn, deltaOut);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Verbose log for debug mode
|
|
281
359
|
log(`Tokens — in: ${state.tokensIn.toLocaleString()} out: ${state.tokensOut.toLocaleString()} total: ${state.tokensTotal.toLocaleString()}/${BUDGET.toLocaleString()} (${Math.round(state.tokensTotal / BUDGET * 100)}%)`, 'debug');
|
|
282
360
|
checkBudgetThresholds();
|
|
283
361
|
}
|
|
@@ -534,7 +612,7 @@ function onChildOutput() {
|
|
|
534
612
|
}
|
|
535
613
|
if (!gotFirstOutput) {
|
|
536
614
|
gotFirstOutput = true;
|
|
537
|
-
log(`First output received after ${Math.round((Date.now() - new Date(state.startedAt).getTime()) / 1000)}s — switching to stall-timeout=${STALL_TIMEOUT_S}s`, '
|
|
615
|
+
log(`First output received after ${Math.round((Date.now() - new Date(state.startedAt).getTime()) / 1000)}s — switching to stall-timeout=${STALL_TIMEOUT_S}s`, 'debug');
|
|
538
616
|
}
|
|
539
617
|
resetStallTimer();
|
|
540
618
|
}
|
|
@@ -655,10 +733,23 @@ function scheduleAbortPoll() {
|
|
|
655
733
|
let timeoutId = null;
|
|
656
734
|
|
|
657
735
|
function startChild() {
|
|
658
|
-
|
|
659
|
-
|
|
736
|
+
// Concise startup message
|
|
737
|
+
const cmd = `${childCmd} ${childArgs.join(' ').substring(0, 60)}...`;
|
|
738
|
+
log(`[START] ${cmd} | Budget: ${fmtSize(BUDGET)} tokens | Timeout: ${TIMEOUT_S}s`, 'info');
|
|
739
|
+
|
|
740
|
+
// Verbose details at debug level
|
|
741
|
+
log(`Budget: ${BUDGET.toLocaleString()} tokens | Timeout: ${TIMEOUT_S}s | Startup: ${STARTUP_TIMEOUT_S}s | Stall: ${STALL_TIMEOUT_S}s | Dead air: ${DEAD_AIR_TIMEOUT_M}min | Max sends: ${MAX_SENDS}/min | Max tools: ${MAX_TOOL_CALLS}/min`, 'debug');
|
|
660
742
|
if (RESTART) {
|
|
661
|
-
log(`Restart: enabled | max: ${MAX_RESTARTS || 'unlimited'} | delay: ${RESTART_DELAY_S}s ±30% | restarts so far: ${state.restarts}`, '
|
|
743
|
+
log(`Restart: enabled | max: ${MAX_RESTARTS || 'unlimited'} | delay: ${RESTART_DELAY_S}s ±30% | restarts so far: ${state.restarts}`, 'debug');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// If --kill-orphaned-mcp: kill stale agentchat-mcp procs before spawning.
|
|
747
|
+
// Prevents session_displaced churn from orphaned prior-session MCP processes.
|
|
748
|
+
if (KILL_ORPHANED_MCP) {
|
|
749
|
+
try {
|
|
750
|
+
execSync('pkill -f agentchat-mcp', { stdio: 'ignore' });
|
|
751
|
+
log('Killed orphaned agentchat-mcp processes', 'debug');
|
|
752
|
+
} catch { /* pkill exits non-zero when nothing matched */ }
|
|
662
753
|
}
|
|
663
754
|
|
|
664
755
|
child = spawn(childCmd, childArgs, {
|
|
@@ -712,13 +803,13 @@ function startChild() {
|
|
|
712
803
|
|
|
713
804
|
// Start stall detection
|
|
714
805
|
if (STALL_TIMEOUT_S > 0 || STARTUP_TIMEOUT_S > 0) {
|
|
715
|
-
log(`Stall detection: startup-timeout=${STARTUP_TIMEOUT_S}s, stall-timeout=${STALL_TIMEOUT_S}s, max-nudges=${MAX_NUDGES}`, '
|
|
806
|
+
log(`Stall detection: startup-timeout=${STARTUP_TIMEOUT_S}s, stall-timeout=${STALL_TIMEOUT_S}s, max-nudges=${MAX_NUDGES}`, 'debug');
|
|
716
807
|
resetStallTimer();
|
|
717
808
|
}
|
|
718
809
|
|
|
719
810
|
// Start dead air detection
|
|
720
811
|
if (DEAD_AIR_TIMEOUT_M > 0) {
|
|
721
|
-
log(`Dead air detection: ${DEAD_AIR_TIMEOUT_M}min threshold, ${Math.round(DEAD_AIR_POLL_MS / 1000)}s poll interval`, '
|
|
812
|
+
log(`Dead air detection: ${DEAD_AIR_TIMEOUT_M}min threshold, ${Math.round(DEAD_AIR_POLL_MS / 1000)}s poll interval`, 'debug');
|
|
722
813
|
scheduleDeadAirPoll();
|
|
723
814
|
}
|
|
724
815
|
|
|
@@ -747,7 +838,15 @@ function startChild() {
|
|
|
747
838
|
state.gotFirstOutput = gotFirstOutput;
|
|
748
839
|
|
|
749
840
|
const level = state.killedBy ? 'error' : (code === 0 ? 'info' : 'warn');
|
|
750
|
-
|
|
841
|
+
|
|
842
|
+
// Concise exit message
|
|
843
|
+
const exitMsg = state.killedBy
|
|
844
|
+
? `[EXIT] Killed: ${state.killedBy} | Tokens: ${fmtSize(state.tokensTotal)} | ${state.duration}s`
|
|
845
|
+
: `[EXIT] Code: ${code} | Tokens: ${fmtSize(state.tokensTotal)} | ${state.duration}s`;
|
|
846
|
+
log(exitMsg, level);
|
|
847
|
+
|
|
848
|
+
// Verbose details at debug level
|
|
849
|
+
log(`Exit — code: ${code} signal: ${signal} | tokens: ${state.tokensTotal.toLocaleString()} | tools: ${state.toolCalls} | sends: ${state.sendCalls} | duration: ${state.duration}s | output: ${gotFirstOutput}${state.killedBy ? ` | killed: ${state.killedBy}` : ''} | restarts: ${state.restarts}`, 'debug');
|
|
751
850
|
writeState();
|
|
752
851
|
writeMetrics();
|
|
753
852
|
|