@tjamescouch/niki 0.5.5 → 0.5.7

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.
Files changed (2) hide show
  1. package/bin/niki +101 -13
  2. 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,11 +54,17 @@ 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)
58
+ --on-kill <command> Run shell command when agent is killed (receives context via env vars)
59
+ Env vars: NIKI_KILL_REASON, NIKI_TOKENS, NIKI_BUDGET, NIKI_DURATION,
60
+ NIKI_SENDS, NIKI_TOOL_CALLS, NIKI_PID, NIKI_CMD
56
61
 
57
62
  Examples:
63
+ niki --profile longrun -- gro --persistent --model sonnet
58
64
  niki --budget 500000 -- claude -p "your prompt" --verbose
59
65
  niki --timeout 1800 --max-sends 5 -- claude -p "..." --model sonnet --verbose
60
- niki --restart --max-restarts 10 -- gro --model gpt-5.2 "your prompt"`);
66
+ niki --restart --max-restarts 10 -- gro --model gpt-5.2 "your prompt"
67
+ niki --on-kill 'curl -d "$NIKI_KILL_REASON: $NIKI_TOKENS tokens" ntfy.sh/my-agents' -- claude -p "..."`);
61
68
  process.exit(1);
62
69
  }
63
70
 
@@ -65,28 +72,67 @@ const nikiArgs = process.argv.slice(2, SEPARATOR);
65
72
  const childCmd = process.argv[SEPARATOR + 1];
66
73
  const childArgs = process.argv.slice(SEPARATOR + 2);
67
74
 
75
+ function getProfileArg(args) {
76
+ const i = args.indexOf("--profile");
77
+ if (i === -1) return null;
78
+ const v = args[i + 1];
79
+ return v && !v.startsWith("-") ? v : null;
80
+ }
81
+
82
+ const DEFAULTS = {
83
+ budget: "1000000",
84
+ timeout: "3600",
85
+ "max-sends": "10",
86
+ "max-tool-calls": "30",
87
+ "stall-timeout": "60",
88
+ "startup-timeout": "180",
89
+ "dead-air-timeout": "5",
90
+ "max-nudges": "3",
91
+ cooldown: "5",
92
+ "poll-interval": "1000",
93
+ "max-restarts": "0",
94
+ "restart-delay": "5"
95
+ };
96
+
97
+ const PROFILES = {
98
+ longrun: {
99
+ timeout: "86400",
100
+ "stall-timeout": "0",
101
+ "startup-timeout": "0",
102
+ "dead-air-timeout": "0",
103
+ cooldown: "15"
104
+ }
105
+ };
106
+
107
+ const profileArg = getProfileArg(nikiArgs);
108
+ const D = profileArg && PROFILES[profileArg] ? { ...DEFAULTS, ...PROFILES[profileArg] } : DEFAULTS;
109
+
110
+
68
111
  const { values: opts } = parseArgs({
69
112
  args: nikiArgs,
70
113
  options: {
71
- budget: { type: 'string', default: '1000000' },
72
- timeout: { type: 'string', default: '3600' },
73
- 'max-sends': { type: 'string', default: '10' },
74
- 'max-tool-calls': { type: 'string', default: '30' },
75
- 'stall-timeout': { type: 'string', default: '60' },
76
- 'startup-timeout': { type: 'string', default: '180' },
77
- 'dead-air-timeout': { type: 'string', default: '1440' },
78
- 'max-nudges': { type: 'string', default: '3' },
114
+ profile: { type: 'string' },
115
+ budget: { type: 'string', default: D.budget },
116
+ timeout: { type: 'string', default: D.timeout },
117
+ 'max-sends': { type: 'string', default: D["max-sends"] },
118
+ 'max-tool-calls': { type: 'string', default: D["max-tool-calls"] },
119
+ 'stall-timeout': { type: 'string', default: D["stall-timeout"] },
120
+ 'startup-timeout': { type: 'string', default: D["startup-timeout"] },
121
+ 'dead-air-timeout': { type: 'string', default: D["dead-air-timeout"] },
122
+ 'max-nudges': { type: 'string', default: D["max-nudges"] },
79
123
  log: { type: 'string' },
80
124
  'log-level': { type: 'string', default: 'info' },
81
125
  'log-json': { type: 'boolean', default: false },
82
126
  state: { type: 'string' },
83
127
  metrics: { type: 'string' },
84
- cooldown: { type: 'string', default: '5' },
128
+ cooldown: { type: 'string', default: D.cooldown },
85
129
  'abort-file': { type: 'string' },
86
- 'poll-interval': { type: 'string', default: '1000' },
130
+ 'poll-interval': { type: 'string', default: D["poll-interval"] },
87
131
  restart: { type: 'boolean', default: false },
88
- 'max-restarts': { type: 'string', default: '0' },
89
- 'restart-delay': { type: 'string', default: '5' },
132
+ 'max-restarts': { type: 'string', default: D["max-restarts"] },
133
+ 'restart-delay': { type: 'string', default: D["restart-delay"] },
134
+ 'kill-orphaned-mcp': { type: 'boolean', default: false },
135
+ 'on-kill': { type: 'string' },
90
136
  },
91
137
  });
92
138
 
@@ -107,6 +153,8 @@ const METRICS_FILE = opts.metrics;
107
153
  const RESTART = opts.restart;
108
154
  const MAX_RESTARTS = parseInt(opts['max-restarts'], 10);
109
155
  const RESTART_DELAY_S = parseFloat(opts['restart-delay']);
156
+ const KILL_ORPHANED_MCP = opts['kill-orphaned-mcp'];
157
+ const ON_KILL = opts['on-kill'];
110
158
  const LOG_JSON = opts['log-json'];
111
159
 
112
160
  // --- Invocation rate limiting (prevent tight crash loops) ---
@@ -375,6 +423,35 @@ function checkRateLimits() {
375
423
  }
376
424
  return null;
377
425
  }
426
+ // --- Kill notification hook ---
427
+
428
+ function fireOnKill(reason) {
429
+ if (!ON_KILL) return;
430
+ const duration = Math.round((Date.now() - new Date(state.startedAt).getTime()) / 1000);
431
+ const env = {
432
+ ...process.env,
433
+ NIKI_KILL_REASON: reason,
434
+ NIKI_TOKENS: String(state.tokensTotal),
435
+ NIKI_BUDGET: String(BUDGET),
436
+ NIKI_DURATION: String(duration),
437
+ NIKI_SENDS: String(state.sendCalls),
438
+ NIKI_TOOL_CALLS: String(state.toolCalls),
439
+ NIKI_PID: String(state.pid),
440
+ NIKI_CMD: childCmd,
441
+ };
442
+ try {
443
+ const hook = spawn('sh', ['-c', ON_KILL], {
444
+ env,
445
+ stdio: 'ignore',
446
+ detached: true,
447
+ });
448
+ hook.unref();
449
+ log(`on-kill hook fired: ${ON_KILL}`, 'debug');
450
+ } catch (err) {
451
+ log(`on-kill hook failed: ${err.message}`, 'warn');
452
+ }
453
+ }
454
+
378
455
  // --- Kill logic ---
379
456
 
380
457
  let child = null;
@@ -401,6 +478,8 @@ function killChild(reason) {
401
478
  }
402
479
  log(`KILL — reason: ${reason} | tokens: ${state.tokensTotal}/${BUDGET} | sends: ${state.sendCallsThisMinute}/min | tools: ${state.toolCallsThisMinute}/min`, 'error');
403
480
 
481
+ fireOnKill(reason);
482
+
404
483
  child.kill('SIGTERM');
405
484
 
406
485
  // Grace period, then SIGKILL
@@ -701,6 +780,15 @@ function startChild() {
701
780
  log(`Restart: enabled | max: ${MAX_RESTARTS || 'unlimited'} | delay: ${RESTART_DELAY_S}s ±30% | restarts so far: ${state.restarts}`, 'debug');
702
781
  }
703
782
 
783
+ // If --kill-orphaned-mcp: kill stale agentchat-mcp procs before spawning.
784
+ // Prevents session_displaced churn from orphaned prior-session MCP processes.
785
+ if (KILL_ORPHANED_MCP) {
786
+ try {
787
+ execSync('pkill -f agentchat-mcp', { stdio: 'ignore' });
788
+ log('Killed orphaned agentchat-mcp processes', 'debug');
789
+ } catch { /* pkill exits non-zero when nothing matched */ }
790
+ }
791
+
704
792
  child = spawn(childCmd, childArgs, {
705
793
  stdio: ['pipe', 'pipe', 'pipe'],
706
794
  env: process.env,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tjamescouch/niki",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Deterministic process supervisor for AI agents — token budgets, rate limits, and abort control",
5
5
  "bin": {
6
6
  "niki": "./bin/niki"