@mauribadnights/clooks 0.3.1 → 0.4.0

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/dist/index.d.ts CHANGED
@@ -11,7 +11,12 @@ export { DenyCache } from './shortcircuit.js';
11
11
  export { RateLimiter } from './ratelimit.js';
12
12
  export { startWatcher, stopWatcher } from './watcher.js';
13
13
  export { generateAuthToken, validateAuth, rotateToken } from './auth.js';
14
+ export { syncSettings } from './sync.js';
15
+ export { installService, uninstallService, isServiceInstalled, getServiceStatus } from './service.js';
16
+ export type { ServiceStatus } from './service.js';
17
+ export type { SyncOptions } from './sync.js';
14
18
  export type { RotateTokenOptions } from './auth.js';
19
+ export { installAgent, isAgentInstalled } from './agent.js';
15
20
  export { evaluateFilter } from './filter.js';
16
21
  export { executeLLMHandler, executeLLMHandlersBatched, calculateCost, resetClient } from './llm.js';
17
22
  export { prefetchContext, renderPromptTemplate } from './prefetch.js';
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  // clooks — public API exports
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.PLUGINS_DIR = exports.LLM_PRICING = exports.DEFAULT_LLM_MAX_TOKENS = exports.DEFAULT_LLM_TIMEOUT = exports.COSTS_FILE = exports.LOG_FILE = exports.METRICS_FILE = exports.PID_FILE = exports.MANIFEST_PATH = exports.CONFIG_DIR = exports.DEFAULT_PORT = exports.renderPromptTemplate = exports.prefetchContext = exports.resetClient = exports.calculateCost = exports.executeLLMHandlersBatched = exports.executeLLMHandler = exports.evaluateFilter = exports.rotateToken = exports.validateAuth = exports.generateAuthToken = exports.stopWatcher = exports.startWatcher = exports.RateLimiter = exports.DenyCache = exports.resolveExecutionOrder = exports.cleanupHandlerState = exports.resetSessionIsolatedHandlers = exports.executeHandlers = exports.runDoctor = exports.getSettingsPath = exports.restore = exports.migrate = exports.MetricsCollector = exports.listPlugins = exports.uninstallPlugin = exports.installPlugin = exports.saveRegistry = exports.loadRegistry = exports.validatePluginManifest = exports.mergeManifests = exports.loadPlugins = exports.createDefaultManifest = exports.validateManifest = exports.loadCompositeManifest = exports.loadManifest = exports.isDaemonRunning = exports.stopDaemon = exports.startDaemon = exports.createServer = void 0;
5
- exports.PLUGIN_MANIFEST_NAME = exports.PLUGIN_REGISTRY = void 0;
4
+ exports.PID_FILE = exports.MANIFEST_PATH = exports.CONFIG_DIR = exports.DEFAULT_PORT = exports.renderPromptTemplate = exports.prefetchContext = exports.resetClient = exports.calculateCost = exports.executeLLMHandlersBatched = exports.executeLLMHandler = exports.evaluateFilter = exports.isAgentInstalled = exports.installAgent = exports.getServiceStatus = exports.isServiceInstalled = exports.uninstallService = exports.installService = exports.syncSettings = exports.rotateToken = exports.validateAuth = exports.generateAuthToken = exports.stopWatcher = exports.startWatcher = exports.RateLimiter = exports.DenyCache = exports.resolveExecutionOrder = exports.cleanupHandlerState = exports.resetSessionIsolatedHandlers = exports.executeHandlers = exports.runDoctor = exports.getSettingsPath = exports.restore = exports.migrate = exports.MetricsCollector = exports.listPlugins = exports.uninstallPlugin = exports.installPlugin = exports.saveRegistry = exports.loadRegistry = exports.validatePluginManifest = exports.mergeManifests = exports.loadPlugins = exports.createDefaultManifest = exports.validateManifest = exports.loadCompositeManifest = exports.loadManifest = exports.isDaemonRunning = exports.stopDaemon = exports.startDaemon = exports.createServer = void 0;
5
+ exports.PLUGIN_MANIFEST_NAME = exports.PLUGIN_REGISTRY = exports.PLUGINS_DIR = exports.LLM_PRICING = exports.DEFAULT_LLM_MAX_TOKENS = exports.DEFAULT_LLM_TIMEOUT = exports.COSTS_FILE = exports.LOG_FILE = exports.METRICS_FILE = void 0;
6
6
  var server_js_1 = require("./server.js");
7
7
  Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return server_js_1.createServer; } });
8
8
  Object.defineProperty(exports, "startDaemon", { enumerable: true, get: function () { return server_js_1.startDaemon; } });
@@ -47,6 +47,16 @@ var auth_js_1 = require("./auth.js");
47
47
  Object.defineProperty(exports, "generateAuthToken", { enumerable: true, get: function () { return auth_js_1.generateAuthToken; } });
48
48
  Object.defineProperty(exports, "validateAuth", { enumerable: true, get: function () { return auth_js_1.validateAuth; } });
49
49
  Object.defineProperty(exports, "rotateToken", { enumerable: true, get: function () { return auth_js_1.rotateToken; } });
50
+ var sync_js_1 = require("./sync.js");
51
+ Object.defineProperty(exports, "syncSettings", { enumerable: true, get: function () { return sync_js_1.syncSettings; } });
52
+ var service_js_1 = require("./service.js");
53
+ Object.defineProperty(exports, "installService", { enumerable: true, get: function () { return service_js_1.installService; } });
54
+ Object.defineProperty(exports, "uninstallService", { enumerable: true, get: function () { return service_js_1.uninstallService; } });
55
+ Object.defineProperty(exports, "isServiceInstalled", { enumerable: true, get: function () { return service_js_1.isServiceInstalled; } });
56
+ Object.defineProperty(exports, "getServiceStatus", { enumerable: true, get: function () { return service_js_1.getServiceStatus; } });
57
+ var agent_js_1 = require("./agent.js");
58
+ Object.defineProperty(exports, "installAgent", { enumerable: true, get: function () { return agent_js_1.installAgent; } });
59
+ Object.defineProperty(exports, "isAgentInstalled", { enumerable: true, get: function () { return agent_js_1.isAgentInstalled; } });
50
60
  var filter_js_1 = require("./filter.js");
51
61
  Object.defineProperty(exports, "evaluateFilter", { enumerable: true, get: function () { return filter_js_1.evaluateFilter; } });
52
62
  var llm_js_1 = require("./llm.js");
package/dist/manifest.js CHANGED
@@ -74,6 +74,39 @@ function validateManifest(manifest) {
74
74
  throw new Error(`LLM handler "${handler.id}" model must be one of: ${validModels.join(', ')}`);
75
75
  }
76
76
  }
77
+ // Validate async field type
78
+ if ('async' in handler && typeof handler.async !== 'boolean') {
79
+ throw new Error(`Handler "${handler.id}" async field must be a boolean`);
80
+ }
81
+ // Validate agent field type
82
+ if ('agent' in handler && typeof handler.agent !== 'string') {
83
+ throw new Error(`Handler "${handler.id}" agent field must be a string`);
84
+ }
85
+ // Validate project field type
86
+ if ('project' in handler && typeof handler.project !== 'string') {
87
+ throw new Error(`Handler "${handler.id}" project field must be a string`);
88
+ }
89
+ }
90
+ // Warn about async handlers with dependency relationships
91
+ const eventHandlerIds = new Set(handlers.map(h => h.id));
92
+ const dependedUponIds = new Set();
93
+ for (const h of handlers) {
94
+ if (h.depends) {
95
+ for (const dep of h.depends) {
96
+ if (eventHandlerIds.has(dep))
97
+ dependedUponIds.add(dep);
98
+ }
99
+ }
100
+ }
101
+ for (const h of handlers) {
102
+ if (h.async) {
103
+ if (dependedUponIds.has(h.id)) {
104
+ console.warn(`[clooks] Warning: async handler "${h.id}" has dependents — will run synchronously at runtime`);
105
+ }
106
+ if (h.depends?.some(d => eventHandlerIds.has(d))) {
107
+ console.warn(`[clooks] Warning: async handler "${h.id}" has dependencies — will run synchronously at runtime`);
108
+ }
109
+ }
77
110
  }
78
111
  }
79
112
  // Validate prefetch if present
@@ -5,8 +5,13 @@ export declare class RateLimiter {
5
5
  constructor(maxAttempts?: number, windowMs?: number);
6
6
  /** Check if source is rate-limited. Returns true if allowed. */
7
7
  check(source: string): boolean;
8
- /** Record an attempt from source. */
9
- record(source: string): void;
8
+ /** Record an auth failure from source. */
9
+ recordFailure(source: string): void;
10
+ /**
11
+ * How many seconds until the rate limit resets for a given source.
12
+ * Returns 0 if the source is not rate-limited.
13
+ */
14
+ retryAfter(source: string): number;
10
15
  /** Clean up old entries. */
11
16
  cleanup(): void;
12
17
  }
package/dist/ratelimit.js CHANGED
@@ -1,9 +1,13 @@
1
1
  "use strict";
2
2
  // clooks rate limiting — protect against auth brute-force
3
+ //
4
+ // IMPORTANT: This rate limiter should ONLY be used when auth is configured.
5
+ // It tracks auth failures per source IP. When the limit is exceeded, requests
6
+ // from that source are blocked with 429 until the window expires.
3
7
  Object.defineProperty(exports, "__esModule", { value: true });
4
8
  exports.RateLimiter = void 0;
5
9
  class RateLimiter {
6
- attempts = new Map(); // source → timestamps
10
+ attempts = new Map(); // source → auth failure timestamps
7
11
  maxAttempts;
8
12
  windowMs;
9
13
  constructor(maxAttempts = 10, windowMs = 60_000) {
@@ -16,17 +20,34 @@ class RateLimiter {
16
20
  const timestamps = this.attempts.get(source);
17
21
  if (!timestamps)
18
22
  return true;
19
- // Count recent attempts within window
23
+ // Count recent auth failures within window
20
24
  const recent = timestamps.filter(t => now - t <= this.windowMs);
21
25
  return recent.length < this.maxAttempts;
22
26
  }
23
- /** Record an attempt from source. */
24
- record(source) {
27
+ /** Record an auth failure from source. */
28
+ recordFailure(source) {
25
29
  const now = Date.now();
26
30
  const timestamps = this.attempts.get(source) ?? [];
27
31
  timestamps.push(now);
28
32
  this.attempts.set(source, timestamps);
29
33
  }
34
+ /**
35
+ * How many seconds until the rate limit resets for a given source.
36
+ * Returns 0 if the source is not rate-limited.
37
+ */
38
+ retryAfter(source) {
39
+ const now = Date.now();
40
+ const timestamps = this.attempts.get(source);
41
+ if (!timestamps)
42
+ return 0;
43
+ const recent = timestamps.filter(t => now - t <= this.windowMs);
44
+ if (recent.length < this.maxAttempts)
45
+ return 0;
46
+ // The oldest recent attempt determines when the window expires
47
+ const oldest = Math.min(...recent);
48
+ const expiresAt = oldest + this.windowMs;
49
+ return Math.ceil((expiresAt - now) / 1000);
50
+ }
30
51
  /** Clean up old entries. */
31
52
  cleanup() {
32
53
  const now = Date.now();
package/dist/server.d.ts CHANGED
@@ -4,6 +4,13 @@ import { MetricsCollector } from './metrics.js';
4
4
  import { DenyCache } from './shortcircuit.js';
5
5
  import { RateLimiter } from './ratelimit.js';
6
6
  import type { Manifest } from './types.js';
7
+ /** Session agent cache: session_id → { agent_type, timestamp } */
8
+ declare const sessionAgents: Map<string, {
9
+ agent: string;
10
+ ts: number;
11
+ }>;
12
+ /** Exported for testing */
13
+ export { sessionAgents };
7
14
  export interface ServerContext {
8
15
  server: Server;
9
16
  metrics: MetricsCollector;
@@ -29,11 +36,24 @@ export declare function startDaemon(manifest: Manifest, metrics: MetricsCollecto
29
36
  */
30
37
  export declare function stopDaemon(): boolean;
31
38
  /**
32
- * Check if daemon is currently running.
39
+ * Check if daemon is currently running (PID check only).
40
+ * Use for stop/status where a quick check is fine.
33
41
  */
34
42
  export declare function isDaemonRunning(): boolean;
43
+ /**
44
+ * Check if daemon is running AND healthy (PID + health endpoint).
45
+ * Defends against stale PIDs reused by macOS after sleep/lid-close.
46
+ * Use for ensure-running and start where correctness matters.
47
+ */
48
+ export declare function isDaemonHealthy(): Promise<boolean>;
49
+ /**
50
+ * Clean up a stale daemon: remove PID file and attempt to kill the process.
51
+ * Returns the stale PID for logging purposes.
52
+ */
53
+ export declare function cleanupStaleDaemon(): number | null;
35
54
  /**
36
55
  * Start daemon as a detached background process.
56
+ * Always removes any existing PID file first — the new daemon writes its own.
37
57
  */
38
58
  export declare function startDaemonBackground(options?: {
39
59
  noWatch?: boolean;
package/dist/server.js CHANGED
@@ -1,10 +1,13 @@
1
1
  "use strict";
2
2
  // clooks HTTP server — persistent hook daemon
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.sessionAgents = void 0;
4
5
  exports.createServer = createServer;
5
6
  exports.startDaemon = startDaemon;
6
7
  exports.stopDaemon = stopDaemon;
7
8
  exports.isDaemonRunning = isDaemonRunning;
9
+ exports.isDaemonHealthy = isDaemonHealthy;
10
+ exports.cleanupStaleDaemon = cleanupStaleDaemon;
8
11
  exports.startDaemonBackground = startDaemonBackground;
9
12
  const http_1 = require("http");
10
13
  const fs_1 = require("fs");
@@ -17,6 +20,18 @@ const shortcircuit_js_1 = require("./shortcircuit.js");
17
20
  const ratelimit_js_1 = require("./ratelimit.js");
18
21
  const constants_js_1 = require("./constants.js");
19
22
  const manifest_js_1 = require("./manifest.js");
23
+ /** Session agent cache: session_id → { agent_type, timestamp } */
24
+ const sessionAgents = new Map();
25
+ exports.sessionAgents = sessionAgents;
26
+ const SESSION_AGENT_TTL = 24 * 60 * 60 * 1000; // 24 hours
27
+ function cleanupSessionAgents() {
28
+ const now = Date.now();
29
+ for (const [id, entry] of sessionAgents) {
30
+ if (now - entry.ts > SESSION_AGENT_TTL) {
31
+ sessionAgents.delete(id);
32
+ }
33
+ }
34
+ }
20
35
  function log(msg) {
21
36
  const line = `[${new Date().toISOString()}] ${msg}\n`;
22
37
  try {
@@ -99,6 +114,7 @@ function createServer(manifest, metrics) {
99
114
  ctx.cleanupInterval = setInterval(() => {
100
115
  denyCache.cleanup();
101
116
  rateLimiter.cleanup();
117
+ cleanupSessionAgents();
102
118
  }, 60_000);
103
119
  // Unref so it doesn't keep the process alive
104
120
  if (ctx.cleanupInterval && typeof ctx.cleanupInterval === 'object' && 'unref' in ctx.cleanupInterval) {
@@ -131,17 +147,24 @@ function createServer(manifest, metrics) {
131
147
  });
132
148
  return;
133
149
  }
134
- // Auth check for all POST requests
150
+ // Auth check for all POST requests — only when auth token is configured
135
151
  if (method === 'POST' && authToken) {
136
152
  const source = req.socket.remoteAddress ?? 'unknown';
137
- // Rate limiting check
153
+ // Rate limiting: check if this source has too many auth failures
138
154
  if (!rateLimiter.check(source)) {
139
- sendJson(res, 429, { error: 'Too many requests' });
155
+ const retryAfter = rateLimiter.retryAfter(source);
156
+ const body = JSON.stringify({ error: 'Too many auth failures' });
157
+ res.writeHead(429, {
158
+ 'Content-Type': 'application/json',
159
+ 'Content-Length': Buffer.byteLength(body),
160
+ 'Retry-After': String(retryAfter),
161
+ });
162
+ res.end(body);
140
163
  return;
141
164
  }
142
165
  const authHeader = req.headers['authorization'];
143
166
  if (!(0, auth_js_1.validateAuth)(authHeader, authToken)) {
144
- rateLimiter.record(source);
167
+ rateLimiter.recordFailure(source);
145
168
  log(`Auth failure from ${source}`);
146
169
  sendJson(res, 401, { error: 'Unauthorized' });
147
170
  return;
@@ -156,7 +179,7 @@ function createServer(manifest, metrics) {
156
179
  return;
157
180
  }
158
181
  const event = eventName;
159
- // On SessionStart, reset session-isolated handlers across ALL events
182
+ // On SessionStart, cache agent and reset session-isolated handlers
160
183
  if (event === 'SessionStart') {
161
184
  const allHandlers = Object.values(ctx.manifest.handlers)
162
185
  .flat()
@@ -178,6 +201,12 @@ function createServer(manifest, metrics) {
178
201
  sendJson(res, 400, { error: 'Invalid JSON body' });
179
202
  return;
180
203
  }
204
+ // Cache agent_type on SessionStart
205
+ if (event === 'SessionStart' && input.agent_type && input.session_id) {
206
+ sessionAgents.set(input.session_id, { agent: input.agent_type, ts: Date.now() });
207
+ }
208
+ // Resolve current agent for this session
209
+ const currentAgent = input.session_id ? sessionAgents.get(input.session_id)?.agent : undefined;
181
210
  // Short-circuit: skip PostToolUse if PreToolUse denied this tool
182
211
  if (event === 'PostToolUse' && input.tool_name && input.session_id) {
183
212
  if (denyCache.isDenied(input.session_id, input.tool_name)) {
@@ -186,16 +215,23 @@ function createServer(manifest, metrics) {
186
215
  return;
187
216
  }
188
217
  }
189
- log(`Hook: ${eventName} (${handlers.length} handler${handlers.length > 1 ? 's' : ''})`);
218
+ const allHandlerConfigs = handlers;
219
+ const syncCount = allHandlerConfigs.filter(h => !h.async).length;
220
+ const asyncCount = allHandlerConfigs.filter(h => h.async).length;
221
+ if (asyncCount > 0) {
222
+ log(`Hook: ${eventName} (${syncCount} sync, ${asyncCount} async handler${syncCount + asyncCount > 1 ? 's' : ''})`);
223
+ }
224
+ else {
225
+ log(`Hook: ${eventName} (${handlers.length} handler${handlers.length > 1 ? 's' : ''})`);
226
+ }
190
227
  try {
191
228
  // Pre-fetch shared context if configured
192
229
  let context;
193
230
  if (ctx.manifest.prefetch && ctx.manifest.prefetch.length > 0) {
194
231
  context = await (0, prefetch_js_1.prefetchContext)(ctx.manifest.prefetch, input);
195
232
  }
196
- const results = await (0, handlers_js_1.executeHandlers)(event, input, handlers, context);
197
- // Record metrics and costs
198
- for (const result of results) {
233
+ // Callback for recording async handler metrics when they complete
234
+ const recordResult = (result) => {
199
235
  metrics.record({
200
236
  ts: new Date().toISOString(),
201
237
  event,
@@ -208,9 +244,8 @@ function createServer(manifest, metrics) {
208
244
  cost_usd: result.cost_usd,
209
245
  session_id: input.session_id,
210
246
  });
211
- // Track cost for LLM handlers
212
247
  if (result.usage && result.cost_usd !== undefined && result.cost_usd > 0) {
213
- const handlerConfig = handlers.find(h => h.id === result.id);
248
+ const handlerConfig = allHandlerConfigs.find(h => h.id === result.id);
214
249
  if (handlerConfig && handlerConfig.type === 'llm') {
215
250
  const llmConfig = handlerConfig;
216
251
  metrics.trackCost({
@@ -224,6 +259,11 @@ function createServer(manifest, metrics) {
224
259
  });
225
260
  }
226
261
  }
262
+ };
263
+ const results = await (0, handlers_js_1.executeHandlers)(event, input, allHandlerConfigs, context, recordResult, currentAgent);
264
+ // Record metrics and costs for sync results
265
+ for (const result of results) {
266
+ recordResult(result);
227
267
  }
228
268
  // Short-circuit: if PreToolUse had a deny, record it in the cache
229
269
  if (event === 'PreToolUse' && input.tool_name && input.session_id) {
@@ -373,6 +413,13 @@ function startDaemon(manifest, metrics, options) {
373
413
  };
374
414
  process.on('SIGTERM', shutdown);
375
415
  process.on('SIGINT', shutdown);
416
+ // Visibility into macOS sleep/wake cycles
417
+ process.on('SIGTSTP', () => {
418
+ log('Daemon suspended (system sleep)');
419
+ });
420
+ process.on('SIGCONT', () => {
421
+ log('Daemon resumed (system wake)');
422
+ });
376
423
  });
377
424
  }
378
425
  /**
@@ -415,7 +462,8 @@ function stopDaemon() {
415
462
  return true;
416
463
  }
417
464
  /**
418
- * Check if daemon is currently running.
465
+ * Check if daemon is currently running (PID check only).
466
+ * Use for stop/status where a quick check is fine.
419
467
  */
420
468
  function isDaemonRunning() {
421
469
  if (!(0, fs_1.existsSync)(constants_js_1.PID_FILE))
@@ -433,10 +481,86 @@ function isDaemonRunning() {
433
481
  return false;
434
482
  }
435
483
  }
484
+ /**
485
+ * Check if daemon is running AND healthy (PID + health endpoint).
486
+ * Defends against stale PIDs reused by macOS after sleep/lid-close.
487
+ * Use for ensure-running and start where correctness matters.
488
+ */
489
+ async function isDaemonHealthy() {
490
+ if (!(0, fs_1.existsSync)(constants_js_1.PID_FILE))
491
+ return false;
492
+ const pidStr = (0, fs_1.readFileSync)(constants_js_1.PID_FILE, 'utf-8').trim();
493
+ const pid = parseInt(pidStr, 10);
494
+ if (isNaN(pid))
495
+ return false;
496
+ // Step 1: PID alive?
497
+ try {
498
+ process.kill(pid, 0);
499
+ }
500
+ catch {
501
+ return false;
502
+ }
503
+ // Step 2: Health endpoint responds?
504
+ const port = constants_js_1.DEFAULT_PORT; // health check always on default port
505
+ try {
506
+ const { get } = await import('http');
507
+ const data = await new Promise((resolve, reject) => {
508
+ const req = get(`http://127.0.0.1:${port}/health`, (res) => {
509
+ let body = '';
510
+ res.on('data', (chunk) => { body += chunk.toString(); });
511
+ res.on('end', () => resolve(body));
512
+ });
513
+ req.on('error', reject);
514
+ req.setTimeout(2000, () => { req.destroy(); reject(new Error('timeout')); });
515
+ });
516
+ const health = JSON.parse(data);
517
+ return health.status === 'ok';
518
+ }
519
+ catch {
520
+ return false;
521
+ }
522
+ }
523
+ /**
524
+ * Clean up a stale daemon: remove PID file and attempt to kill the process.
525
+ * Returns the stale PID for logging purposes.
526
+ */
527
+ function cleanupStaleDaemon() {
528
+ if (!(0, fs_1.existsSync)(constants_js_1.PID_FILE))
529
+ return null;
530
+ const pidStr = (0, fs_1.readFileSync)(constants_js_1.PID_FILE, 'utf-8').trim();
531
+ const pid = parseInt(pidStr, 10);
532
+ // Remove stale PID file
533
+ try {
534
+ (0, fs_1.unlinkSync)(constants_js_1.PID_FILE);
535
+ }
536
+ catch {
537
+ // ignore
538
+ }
539
+ // Try to kill the stale process (might be our daemon but unhealthy)
540
+ if (!isNaN(pid)) {
541
+ try {
542
+ process.kill(pid, 'SIGTERM');
543
+ }
544
+ catch {
545
+ // Process doesn't exist — that's fine
546
+ }
547
+ return pid;
548
+ }
549
+ return null;
550
+ }
436
551
  /**
437
552
  * Start daemon as a detached background process.
553
+ * Always removes any existing PID file first — the new daemon writes its own.
438
554
  */
439
555
  function startDaemonBackground(options) {
556
+ // Clean any stale PID file before spawning
557
+ try {
558
+ if ((0, fs_1.existsSync)(constants_js_1.PID_FILE))
559
+ (0, fs_1.unlinkSync)(constants_js_1.PID_FILE);
560
+ }
561
+ catch {
562
+ // ignore
563
+ }
440
564
  const args = [process.argv[1], 'start', '--foreground'];
441
565
  if (options?.noWatch) {
442
566
  args.push('--no-watch');
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Find the clooks binary path by checking PATH or falling back to process.argv[1].
3
+ */
4
+ declare function findClooksPath(): string;
5
+ declare const PLIST_LABEL = "com.clooks.daemon";
6
+ declare function getPlistPath(): string;
7
+ declare const SYSTEMD_SERVICE_NAME = "clooks";
8
+ declare function getSystemdServicePath(): string;
9
+ declare const TASK_NAME = "clooks";
10
+ export type ServiceStatus = 'running' | 'stopped' | 'not-installed';
11
+ /**
12
+ * Install clooks as an OS service (launchd on macOS, systemd on Linux, schtasks on Windows).
13
+ */
14
+ export declare function installService(): void;
15
+ /**
16
+ * Uninstall the clooks OS service.
17
+ */
18
+ export declare function uninstallService(): void;
19
+ /**
20
+ * Check if the clooks OS service is installed.
21
+ */
22
+ export declare function isServiceInstalled(): boolean;
23
+ /**
24
+ * Get the current service status.
25
+ */
26
+ export declare function getServiceStatus(): ServiceStatus;
27
+ export { findClooksPath, getPlistPath, getSystemdServicePath, PLIST_LABEL, SYSTEMD_SERVICE_NAME, TASK_NAME };