byterover-cli 1.0.2 → 1.0.4

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 (128) hide show
  1. package/README.md +62 -10
  2. package/dist/commands/curate.js +2 -2
  3. package/dist/commands/main.js +2 -2
  4. package/dist/commands/query.js +2 -2
  5. package/dist/commands/status.js +2 -2
  6. package/dist/config/context-tree-domains.d.ts +14 -2
  7. package/dist/config/context-tree-domains.js +22 -27
  8. package/dist/constants.d.ts +1 -0
  9. package/dist/constants.js +3 -0
  10. package/dist/core/domain/cipher/file-system/types.d.ts +2 -0
  11. package/dist/core/domain/entities/auth-token.js +6 -3
  12. package/dist/core/domain/entities/event.d.ts +1 -1
  13. package/dist/core/domain/entities/event.js +2 -1
  14. package/dist/core/domain/knowledge/relation-parser.d.ts +16 -1
  15. package/dist/core/domain/knowledge/relation-parser.js +19 -2
  16. package/dist/core/domain/transport/schemas.d.ts +17 -1
  17. package/dist/core/domain/transport/schemas.js +9 -1
  18. package/dist/core/interfaces/cipher/i-blob-storage.d.ts +6 -0
  19. package/dist/core/interfaces/cipher/index.d.ts +0 -1
  20. package/dist/core/interfaces/executor/i-curate-executor.d.ts +2 -0
  21. package/dist/infra/cipher/agent/cipher-agent.js +4 -0
  22. package/dist/infra/cipher/file-system/file-system-service.d.ts +4 -0
  23. package/dist/infra/cipher/file-system/file-system-service.js +5 -0
  24. package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.js +4 -2
  25. package/dist/infra/cipher/tools/implementations/create-knowledge-topic-tool.js +24 -17
  26. package/dist/infra/cipher/tools/implementations/curate-tool.js +28 -33
  27. package/dist/infra/cipher/tools/implementations/read-file-tool.js +3 -12
  28. package/dist/infra/cipher/tools/implementations/spec-analyze-tool.js +18 -15
  29. package/dist/infra/cipher/tools/implementations/task-tool.js +53 -7
  30. package/dist/infra/context-tree/file-context-tree-service.js +4 -15
  31. package/dist/infra/core/executors/curate-executor.d.ts +2 -7
  32. package/dist/infra/core/executors/curate-executor.js +18 -53
  33. package/dist/infra/core/executors/query-executor.d.ts +1 -7
  34. package/dist/infra/core/executors/query-executor.js +10 -35
  35. package/dist/infra/core/task-processor.d.ts +2 -0
  36. package/dist/infra/core/task-processor.js +1 -0
  37. package/dist/infra/http/authenticated-http-client.js +5 -0
  38. package/dist/infra/process/agent-worker.js +113 -6
  39. package/dist/infra/process/constants.d.ts +1 -0
  40. package/dist/infra/process/constants.js +1 -0
  41. package/dist/infra/process/task-queue-manager.js +2 -1
  42. package/dist/infra/process/transport-handlers.js +4 -0
  43. package/dist/infra/process/transport-worker.js +89 -1
  44. package/dist/infra/repl/commands/curate-command.js +2 -2
  45. package/dist/infra/repl/commands/gen-rules-command.js +2 -2
  46. package/dist/infra/repl/commands/init-command.js +2 -2
  47. package/dist/infra/repl/commands/login-command.js +2 -2
  48. package/dist/infra/repl/commands/logout-command.js +2 -2
  49. package/dist/infra/repl/commands/pull-command.js +2 -2
  50. package/dist/infra/repl/commands/push-command.js +2 -2
  51. package/dist/infra/repl/commands/query-command.js +2 -2
  52. package/dist/infra/repl/commands/space/list-command.js +2 -2
  53. package/dist/infra/repl/commands/space/switch-command.js +2 -2
  54. package/dist/infra/repl/commands/status-command.js +2 -2
  55. package/dist/infra/repl/repl-startup.js +0 -2
  56. package/dist/infra/storage/file-token-store.d.ts +31 -0
  57. package/dist/infra/storage/file-token-store.js +98 -0
  58. package/dist/infra/storage/keychain-token-store.d.ts +4 -1
  59. package/dist/infra/storage/keychain-token-store.js +6 -4
  60. package/dist/infra/storage/token-store.d.ts +10 -0
  61. package/dist/infra/storage/token-store.js +14 -0
  62. package/dist/infra/usecase/curate-use-case.js +1 -1
  63. package/dist/infra/usecase/init-use-case.js +2 -4
  64. package/dist/infra/user/http-user-service.js +6 -11
  65. package/dist/resources/prompts/curate.yml +14 -5
  66. package/dist/resources/prompts/plan.yml +6 -0
  67. package/dist/tui/app.js +1 -1
  68. package/dist/tui/components/execution/log-item.js +2 -5
  69. package/dist/tui/components/header.d.ts +1 -1
  70. package/dist/tui/components/header.js +25 -4
  71. package/dist/tui/components/index.d.ts +5 -1
  72. package/dist/tui/components/index.js +3 -1
  73. package/dist/tui/components/init.d.ts +33 -0
  74. package/dist/tui/components/init.js +253 -0
  75. package/dist/tui/components/onboarding/index.d.ts +1 -0
  76. package/dist/tui/components/onboarding/index.js +1 -0
  77. package/dist/tui/components/onboarding/onboarding-flow.d.ts +2 -0
  78. package/dist/tui/components/onboarding/onboarding-flow.js +8 -229
  79. package/dist/tui/components/onboarding/onboarding-step.js +1 -1
  80. package/dist/tui/components/onboarding/welcome-box.d.ts +14 -0
  81. package/dist/tui/components/onboarding/welcome-box.js +23 -0
  82. package/dist/tui/components/status-badge.d.ts +22 -0
  83. package/dist/tui/components/status-badge.js +32 -0
  84. package/dist/tui/contexts/auth-context.js +2 -1
  85. package/dist/tui/contexts/index.d.ts +1 -0
  86. package/dist/tui/contexts/index.js +1 -0
  87. package/dist/tui/contexts/onboarding-context.d.ts +14 -0
  88. package/dist/tui/contexts/onboarding-context.js +17 -22
  89. package/dist/tui/contexts/status-context.d.ts +33 -0
  90. package/dist/tui/contexts/status-context.js +159 -0
  91. package/dist/tui/hooks/use-auth-polling.d.ts +4 -1
  92. package/dist/tui/hooks/use-auth-polling.js +21 -7
  93. package/dist/tui/hooks/use-tab-navigation.js +0 -2
  94. package/dist/tui/providers/app-providers.js +2 -2
  95. package/dist/tui/types/index.d.ts +2 -0
  96. package/dist/tui/types/index.js +2 -0
  97. package/dist/tui/types/status.d.ts +46 -0
  98. package/dist/tui/types/status.js +13 -0
  99. package/dist/tui/utils/index.d.ts +6 -0
  100. package/dist/tui/utils/index.js +6 -0
  101. package/dist/tui/utils/time.d.ts +10 -0
  102. package/dist/tui/utils/time.js +15 -0
  103. package/dist/tui/views/command-view.js +0 -2
  104. package/dist/tui/views/index.d.ts +1 -0
  105. package/dist/tui/views/index.js +1 -0
  106. package/dist/tui/views/init-view.d.ts +15 -0
  107. package/dist/tui/views/init-view.js +29 -0
  108. package/dist/tui/views/logs-view.js +22 -8
  109. package/dist/utils/environment-detector.d.ts +5 -0
  110. package/dist/utils/environment-detector.js +31 -0
  111. package/dist/utils/global-data-path.d.ts +11 -0
  112. package/dist/utils/global-data-path.js +32 -0
  113. package/oclif.manifest.json +1 -1
  114. package/package.json +1 -1
  115. package/dist/core/interfaces/cipher/i-agent-storage.d.ts +0 -152
  116. package/dist/core/interfaces/cipher/i-agent-storage.js +0 -1
  117. package/dist/infra/cipher/consumer/consumer-lock.d.ts +0 -20
  118. package/dist/infra/cipher/consumer/consumer-lock.js +0 -41
  119. package/dist/infra/cipher/consumer/consumer-service.d.ts +0 -99
  120. package/dist/infra/cipher/consumer/consumer-service.js +0 -166
  121. package/dist/infra/cipher/consumer/execution-consumer.d.ts +0 -126
  122. package/dist/infra/cipher/consumer/execution-consumer.js +0 -561
  123. package/dist/infra/cipher/consumer/index.d.ts +0 -33
  124. package/dist/infra/cipher/consumer/index.js +0 -34
  125. package/dist/infra/cipher/consumer/queue-polling-service.d.ts +0 -120
  126. package/dist/infra/cipher/consumer/queue-polling-service.js +0 -249
  127. package/dist/infra/cipher/storage/agent-storage.d.ts +0 -246
  128. package/dist/infra/cipher/storage/agent-storage.js +0 -956
@@ -76,6 +76,11 @@ export class AuthenticatedHttpClient {
76
76
  * Preserves error information while abstracting axios-specific details.
77
77
  */
78
78
  handleError(error) {
79
+ if (isAxiosError(error) && error.response?.status === 401) {
80
+ // IMPORTANT: Do not handle 401 errors here - let callers handle errors (e.g., distinguish 401 from network errors)
81
+ return error;
82
+ }
83
+ // WARNING: isLLMServerError() matches any response with the standard ApiErrorResponse structure (IAM, Cogit, LLM services all use it)
79
84
  if (this.isLLMServerError(error)) {
80
85
  // Extract standardized API error message
81
86
  return new Error(this.parseHttpError(error));
@@ -27,8 +27,9 @@ import { ProjectConfigStore } from '../config/file-config-store.js';
27
27
  import { CurateExecutor } from '../core/executors/curate-executor.js';
28
28
  import { QueryExecutor } from '../core/executors/query-executor.js';
29
29
  import { createTaskProcessor } from '../core/task-processor.js';
30
- import { KeychainTokenStore } from '../storage/keychain-token-store.js';
30
+ import { createTokenStore } from '../storage/token-store.js';
31
31
  import { createTransportClient } from '../transport/transport-factory.js';
32
+ import { CURATE_MAX_CONCURRENT } from './constants.js';
32
33
  import { TaskQueueManager } from './task-queue-manager.js';
33
34
  // IPC types imported from ./ipc-types.ts
34
35
  function sendToParent(message) {
@@ -62,6 +63,12 @@ let initializationError;
62
63
  let isInitializing = false;
63
64
  /** Guard: prevent double cleanup */
64
65
  let isCleaningUp = false;
66
+ /** Parent process PID for heartbeat monitoring */
67
+ let parentPid;
68
+ /** Parent heartbeat running flag (for recursive setTimeout pattern) */
69
+ let parentHeartbeatRunning = false;
70
+ /** Parent heartbeat check interval in milliseconds */
71
+ const PARENT_HEARTBEAT_INTERVAL_MS = 2000;
65
72
  let eventForwarders = [];
66
73
  // ============================================================================
67
74
  // Task Queue Manager (replaces inline queue logic)
@@ -69,13 +76,13 @@ let eventForwarders = [];
69
76
  /**
70
77
  * Task queue manager handles:
71
78
  * - Separate queues for curate and query tasks
72
- * - Concurrency limits (max 2 concurrent per type)
79
+ * - Concurrency limits (max 1 concurrent per type)
73
80
  * - Task deduplication (same taskId can't be queued twice)
74
81
  * - Cancel tasks from queue before processing
75
82
  * - FIFO processing order
76
83
  */
77
84
  const taskQueueManager = new TaskQueueManager({
78
- curate: { maxConcurrent: 2 },
85
+ curate: { maxConcurrent: CURATE_MAX_CONCURRENT },
79
86
  onExecutorError(taskId, error) {
80
87
  agentLog(`Executor error for task ${taskId}: ${error}`);
81
88
  },
@@ -104,7 +111,6 @@ function cleanupAgentEventForwarding() {
104
111
  return;
105
112
  }
106
113
  // Get the old agent's event bus (if still available)
107
- // Cast to CipherAgent to access agentEventBus property
108
114
  const eventBus = cipherAgent?.agentEventBus;
109
115
  if (eventBus) {
110
116
  for (const { event, handler } of eventForwarders) {
@@ -310,7 +316,7 @@ async function tryInitializeAgent(forceReinit = false) {
310
316
  taskProcessor = undefined;
311
317
  isAgentInitialized = false;
312
318
  }
313
- const tokenStore = new KeychainTokenStore();
319
+ const tokenStore = createTokenStore();
314
320
  const configStore = new ProjectConfigStore();
315
321
  const authToken = await tokenStore.load();
316
322
  const brvConfig = await configStore.read();
@@ -320,6 +326,12 @@ async function tryInitializeAgent(forceReinit = false) {
320
326
  agentLog('Cannot initialize - no auth token');
321
327
  return false;
322
328
  }
329
+ // Check if token is expired - fail early with clear message instead of 401 later
330
+ if (authToken.isExpired()) {
331
+ initializationError = new NotAuthenticatedError();
332
+ agentLog('Cannot initialize - token expired (please run /login to re-authenticate)');
333
+ return false;
334
+ }
323
335
  // Create Executors
324
336
  const curateExecutor = new CurateExecutor();
325
337
  const queryExecutor = new QueryExecutor();
@@ -370,6 +382,13 @@ async function tryInitializeAgent(forceReinit = false) {
370
382
  }
371
383
  return true;
372
384
  }
385
+ catch (error) {
386
+ // Catch errors and return false instead of throwing
387
+ // This allows lazy init to retry when tasks arrive
388
+ initializationError = error instanceof Error ? error : new Error(String(error));
389
+ agentLog(`Agent initialization failed: ${error}`);
390
+ return false;
391
+ }
373
392
  finally {
374
393
  isInitializing = false;
375
394
  }
@@ -378,7 +397,7 @@ async function tryInitializeAgent(forceReinit = false) {
378
397
  * Handle task:execute from Transport.
379
398
  */
380
399
  async function handleTaskExecute(data) {
381
- const { content, files, taskId, type } = data;
400
+ const { clientCwd, content, files, taskId, type } = data;
382
401
  agentLog(`Processing task: ${taskId} (type=${type})`);
383
402
  // If not initialized, try to initialize now (lazy init for post-onboarding)
384
403
  if (!isAgentInitialized) {
@@ -409,6 +428,7 @@ async function handleTaskExecute(data) {
409
428
  // File validation is handled by UseCase (business logic belongs there)
410
429
  // Note: taskId is passed to UseCase → CipherAgent → ChatSession, which adds it to all events
411
430
  const result = await taskProcessor.process({
431
+ clientCwd,
412
432
  content,
413
433
  files,
414
434
  taskId,
@@ -519,6 +539,71 @@ async function startAgent() {
519
539
  });
520
540
  agentLog('Ready to process tasks');
521
541
  }
542
+ // ============================================================================
543
+ // Parent Heartbeat Monitoring
544
+ // ============================================================================
545
+ /**
546
+ * Setup parent process heartbeat monitoring.
547
+ *
548
+ * Why this is needed:
549
+ * - When main process receives SIGKILL, it dies immediately
550
+ * - SIGKILL cannot be caught, so no cleanup happens
551
+ * - IPC 'disconnect' event may not fire
552
+ * - Child processes become orphans (PPID = 1)
553
+ *
554
+ * This function periodically checks if parent is still alive.
555
+ * If parent dies, child self-terminates to prevent zombie processes.
556
+ */
557
+ function setupParentHeartbeat() {
558
+ // Already running - don't start another
559
+ if (parentHeartbeatRunning)
560
+ return;
561
+ parentHeartbeatRunning = true;
562
+ parentPid = process.ppid;
563
+ /**
564
+ * Recursive setTimeout pattern - safer than setInterval:
565
+ * - No callback overlap possible
566
+ * - Clean cancellation (just set flag = false)
567
+ * - No orphan timers
568
+ */
569
+ const checkParent = () => {
570
+ // Stopped - don't schedule next check
571
+ if (!parentHeartbeatRunning || !parentPid)
572
+ return;
573
+ // Check if parent is still alive using signal 0
574
+ // Signal 0 doesn't send any signal, just checks if process exists
575
+ try {
576
+ process.kill(parentPid, 0);
577
+ }
578
+ catch {
579
+ // Parent is dead - self-terminate
580
+ agentLog(`Parent process (${parentPid}) died - shutting down to prevent zombie`);
581
+ parentHeartbeatRunning = false;
582
+ // Stop agent and exit
583
+ stopAgent()
584
+ .catch(() => { })
585
+ .finally(() => {
586
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
587
+ process.exit(0);
588
+ });
589
+ return;
590
+ }
591
+ // Schedule next check (only if still running)
592
+ if (parentHeartbeatRunning) {
593
+ setTimeout(checkParent, PARENT_HEARTBEAT_INTERVAL_MS);
594
+ }
595
+ };
596
+ // Start first check after delay
597
+ setTimeout(checkParent, PARENT_HEARTBEAT_INTERVAL_MS);
598
+ agentLog(`Parent heartbeat monitoring started (PPID: ${parentPid})`);
599
+ }
600
+ /**
601
+ * Stop the parent heartbeat monitoring.
602
+ * With recursive setTimeout, just set flag to false - next check won't schedule.
603
+ */
604
+ function stopParentHeartbeat() {
605
+ parentHeartbeatRunning = false;
606
+ }
522
607
  /**
523
608
  * Stop Agent Process.
524
609
  */
@@ -530,6 +615,8 @@ async function stopAgent() {
530
615
  }
531
616
  isCleaningUp = true;
532
617
  try {
618
+ // Stop parent heartbeat first
619
+ stopParentHeartbeat();
533
620
  // Clear task queue
534
621
  taskQueueManager.clear();
535
622
  // Cleanup event forwarders before stopping agent
@@ -559,11 +646,16 @@ async function runWorker() {
559
646
  try {
560
647
  await startAgent();
561
648
  sendToParent({ type: 'ready' });
649
+ // Start parent heartbeat monitoring after ready
650
+ // This ensures we self-terminate if parent dies (SIGKILL scenario)
651
+ setupParentHeartbeat();
562
652
  }
563
653
  catch (error) {
564
654
  const message = error instanceof Error ? error.message : String(error);
565
655
  agentLog(`Failed to start: ${message}`);
566
656
  sendToParent({ error: message, type: 'error' });
657
+ // Cleanup before exit to release any acquired resources
658
+ await stopAgent().catch(() => { });
567
659
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
568
660
  process.exit(1);
569
661
  }
@@ -588,6 +680,19 @@ async function runWorker() {
588
680
  process.once('SIGTERM', cleanup);
589
681
  process.once('SIGINT', cleanup);
590
682
  process.once('disconnect', cleanup);
683
+ // Global exception handlers - ensure cleanup on unexpected errors
684
+ process.on('uncaughtException', async (error) => {
685
+ agentLog(`Uncaught exception: ${error}`);
686
+ await stopAgent().catch(() => { });
687
+ // eslint-disable-next-line n/no-process-exit
688
+ process.exit(1);
689
+ });
690
+ process.on('unhandledRejection', async (reason) => {
691
+ agentLog(`Unhandled rejection: ${reason}`);
692
+ await stopAgent().catch(() => { });
693
+ // eslint-disable-next-line n/no-process-exit
694
+ process.exit(1);
695
+ });
591
696
  }
592
697
  // ============================================================================
593
698
  // Run
@@ -597,6 +702,8 @@ try {
597
702
  }
598
703
  catch (error) {
599
704
  agentLog(`Fatal error: ${error}`);
705
+ // Cleanup before exit to release any acquired resources
706
+ await stopAgent().catch(() => { });
600
707
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
601
708
  process.exit(1);
602
709
  }
@@ -0,0 +1 @@
1
+ export declare const CURATE_MAX_CONCURRENT = 1;
@@ -0,0 +1 @@
1
+ export const CURATE_MAX_CONCURRENT = 1;
@@ -10,6 +10,7 @@
10
10
  *
11
11
  * This class is extracted from agent-worker.ts to enable unit testing.
12
12
  */
13
+ import { CURATE_MAX_CONCURRENT } from './constants.js';
13
14
  export class TaskQueueManager {
14
15
  activeCurateTasks = 0;
15
16
  activeQueryTasks = 0;
@@ -22,7 +23,7 @@ export class TaskQueueManager {
22
23
  taskExecutor;
23
24
  constructor(config) {
24
25
  this.config = {
25
- curate: { maxConcurrent: config?.curate?.maxConcurrent ?? 2 },
26
+ curate: { maxConcurrent: config?.curate?.maxConcurrent ?? CURATE_MAX_CONCURRENT },
26
27
  // Query tasks are unlimited (Infinity) - lightweight and fast
27
28
  query: { maxConcurrent: config?.query?.maxConcurrent ?? Infinity },
28
29
  };
@@ -152,6 +152,7 @@ export class TransportHandlers {
152
152
  clientId,
153
153
  content: data.content,
154
154
  createdAt: Date.now(),
155
+ ...(data.clientCwd ? { clientCwd: data.clientCwd } : {}),
155
156
  ...(data.files?.length ? { files: data.files } : {}),
156
157
  taskId,
157
158
  type: data.type,
@@ -161,6 +162,7 @@ export class TransportHandlers {
161
162
  // Broadcast task:created to broadcast-room for TUI monitoring
162
163
  this.transport.broadcastTo('broadcast-room', TransportTaskEventNames.CREATED, {
163
164
  content: data.content,
165
+ ...(data.clientCwd ? { clientCwd: data.clientCwd } : {}),
164
166
  ...(data.files?.length ? { files: data.files } : {}),
165
167
  taskId,
166
168
  type: data.type,
@@ -170,6 +172,7 @@ export class TransportHandlers {
170
172
  const executeMsg = {
171
173
  clientId,
172
174
  content: data.content,
175
+ ...(data.clientCwd ? { clientCwd: data.clientCwd } : {}),
173
176
  ...(data.files?.length ? { files: data.files } : {}),
174
177
  taskId,
175
178
  type: data.type,
@@ -214,6 +217,7 @@ export class TransportHandlers {
214
217
  // so it's not available at task:started time. It's saved to DB instead.
215
218
  this.transport.broadcastTo('broadcast-room', TransportTaskEventNames.STARTED, {
216
219
  content: task.content,
220
+ ...(task.clientCwd ? { clientCwd: task.clientCwd } : {}),
217
221
  ...(task.files?.length ? { files: task.files } : {}),
218
222
  taskId,
219
223
  type: task.type,
@@ -35,9 +35,13 @@ function sendToParent(message) {
35
35
  let transportServer;
36
36
  let transportHandlers;
37
37
  let instancePollingInterval;
38
+ let parentHeartbeatRunning = false;
39
+ let parentPid;
38
40
  const instanceManager = new FileInstanceManager();
39
41
  /** Polling interval in milliseconds */
40
42
  const INSTANCE_POLLING_INTERVAL_MS = 2000;
43
+ /** Parent heartbeat check interval in milliseconds */
44
+ const PARENT_HEARTBEAT_INTERVAL_MS = 2000;
41
45
  /**
42
46
  * Setup polling to detect instance.json deletion and recreate it.
43
47
  *
@@ -79,6 +83,69 @@ function stopInstancePolling() {
79
83
  instancePollingInterval = undefined;
80
84
  }
81
85
  }
86
+ /**
87
+ * Setup parent process heartbeat monitoring.
88
+ *
89
+ * Why this is needed:
90
+ * - When main process receives SIGKILL, it dies immediately
91
+ * - SIGKILL cannot be caught, so no cleanup happens
92
+ * - IPC 'disconnect' event may not fire
93
+ * - Child processes become orphans (PPID = 1)
94
+ *
95
+ * This function periodically checks if parent is still alive.
96
+ * If parent dies, child self-terminates to prevent zombie processes.
97
+ */
98
+ function setupParentHeartbeat() {
99
+ // Already running - don't start another
100
+ if (parentHeartbeatRunning)
101
+ return;
102
+ parentHeartbeatRunning = true;
103
+ parentPid = process.ppid;
104
+ /**
105
+ * Recursive setTimeout pattern - safer than setInterval:
106
+ * - No callback overlap possible
107
+ * - Clean cancellation (just set flag = false)
108
+ * - No orphan timers
109
+ */
110
+ const checkParent = () => {
111
+ // Stopped - don't schedule next check
112
+ if (!parentHeartbeatRunning || !parentPid)
113
+ return;
114
+ // Check if parent is still alive using signal 0
115
+ // Signal 0 doesn't send any signal, just checks if process exists
116
+ try {
117
+ process.kill(parentPid, 0);
118
+ }
119
+ catch {
120
+ // Parent is dead - self-terminate
121
+ transportLog(`Parent process (${parentPid}) died - shutting down to prevent zombie`);
122
+ parentHeartbeatRunning = false;
123
+ stopInstancePolling();
124
+ // Release instance lock and exit
125
+ stopTransport()
126
+ .catch(() => { })
127
+ .finally(() => {
128
+ // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
129
+ process.exit(0);
130
+ });
131
+ return;
132
+ }
133
+ // Schedule next check (only if still running)
134
+ if (parentHeartbeatRunning) {
135
+ setTimeout(checkParent, PARENT_HEARTBEAT_INTERVAL_MS);
136
+ }
137
+ };
138
+ // Start first check after delay
139
+ setTimeout(checkParent, PARENT_HEARTBEAT_INTERVAL_MS);
140
+ transportLog(`Parent heartbeat monitoring started (PPID: ${parentPid})`);
141
+ }
142
+ /**
143
+ * Stop the parent heartbeat monitoring.
144
+ * With recursive setTimeout, just set flag to false - next check won't schedule.
145
+ */
146
+ function stopParentHeartbeat() {
147
+ parentHeartbeatRunning = false;
148
+ }
82
149
  async function startTransport() {
83
150
  // Create Socket.IO server
84
151
  transportServer = createTransportServer();
@@ -103,7 +170,8 @@ async function startTransport() {
103
170
  return port;
104
171
  }
105
172
  async function stopTransport() {
106
- // Stop instance polling first
173
+ // Stop heartbeat and polling first
174
+ stopParentHeartbeat();
107
175
  stopInstancePolling();
108
176
  // Release instance.json
109
177
  const projectRoot = process.cwd();
@@ -125,11 +193,16 @@ async function runWorker() {
125
193
  try {
126
194
  const port = await startTransport();
127
195
  sendToParent({ port, type: 'ready' });
196
+ // Start parent heartbeat monitoring after ready
197
+ // This ensures we self-terminate if parent dies (SIGKILL scenario)
198
+ setupParentHeartbeat();
128
199
  }
129
200
  catch (error) {
130
201
  const message = error instanceof Error ? error.message : String(error);
131
202
  transportLog(`Failed to start: ${message}`);
132
203
  sendToParent({ error: message, type: 'error' });
204
+ // Cleanup before exit to release any acquired resources
205
+ await stopTransport().catch(() => { });
133
206
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
134
207
  process.exit(1);
135
208
  }
@@ -154,6 +227,19 @@ async function runWorker() {
154
227
  process.once('SIGTERM', cleanup);
155
228
  process.once('SIGINT', cleanup);
156
229
  process.once('disconnect', cleanup);
230
+ // Global exception handlers - ensure cleanup on unexpected errors
231
+ process.on('uncaughtException', async (error) => {
232
+ transportLog(`Uncaught exception: ${error}`);
233
+ await stopTransport().catch(() => { });
234
+ // eslint-disable-next-line n/no-process-exit
235
+ process.exit(1);
236
+ });
237
+ process.on('unhandledRejection', async (reason) => {
238
+ transportLog(`Unhandled rejection: ${reason}`);
239
+ await stopTransport().catch(() => { });
240
+ // eslint-disable-next-line n/no-process-exit
241
+ process.exit(1);
242
+ });
157
243
  }
158
244
  // ============================================================================
159
245
  // Run
@@ -163,6 +249,8 @@ try {
163
249
  }
164
250
  catch (error) {
165
251
  transportLog(`Fatal error: ${error}`);
252
+ // Cleanup before exit to release any acquired resources
253
+ await stopTransport().catch(() => { });
166
254
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
167
255
  process.exit(1);
168
256
  }
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { isDevelopment } from '../../../config/environment.js';
3
3
  import { CommandKind } from '../../../tui/types.js';
4
4
  import { FileGlobalConfigStore } from "../../storage/file-global-config-store.js";
5
- import { KeychainTokenStore } from '../../storage/keychain-token-store.js';
5
+ import { createTokenStore } from '../../storage/token-store.js';
6
6
  import { ReplTerminal } from '../../terminal/repl-terminal.js';
7
7
  import { MixpanelTrackingService } from '../../tracking/mixpanel-tracking-service.js';
8
8
  import { CurateUseCase } from '../../usecase/curate-use-case.js';
@@ -38,7 +38,7 @@ export const curateCommand = {
38
38
  contextText = args || undefined;
39
39
  }
40
40
  const terminal = new ReplTerminal({ onMessage, onPrompt });
41
- const tokenStore = new KeychainTokenStore();
41
+ const tokenStore = createTokenStore();
42
42
  const globalConfigStore = new FileGlobalConfigStore();
43
43
  const useCase = new CurateUseCase({
44
44
  terminal,
@@ -3,7 +3,7 @@ import { FsFileService } from '../../file/fs-file-service.js';
3
3
  import { LegacyRuleDetector } from '../../rule/legacy-rule-detector.js';
4
4
  import { RuleTemplateService } from '../../rule/rule-template-service.js';
5
5
  import { FileGlobalConfigStore } from "../../storage/file-global-config-store.js";
6
- import { KeychainTokenStore } from '../../storage/keychain-token-store.js';
6
+ import { createTokenStore } from '../../storage/token-store.js';
7
7
  import { FsTemplateLoader } from '../../template/fs-template-loader.js';
8
8
  import { ReplTerminal } from '../../terminal/repl-terminal.js';
9
9
  import { MixpanelTrackingService } from '../../tracking/mixpanel-tracking-service.js';
@@ -23,7 +23,7 @@ export const genRulesCommand = {
23
23
  const templateLoader = new FsTemplateLoader(fileService);
24
24
  const templateService = new RuleTemplateService(templateLoader);
25
25
  const globalConfigStore = new FileGlobalConfigStore();
26
- const trackingService = new MixpanelTrackingService({ globalConfigStore, tokenStore: new KeychainTokenStore() });
26
+ const trackingService = new MixpanelTrackingService({ globalConfigStore, tokenStore: createTokenStore() });
27
27
  // Create and run UseCase
28
28
  const useCase = new GenerateRulesUseCase(fileService, new LegacyRuleDetector(), templateService, terminal, trackingService);
29
29
  await useCase.run();
@@ -10,7 +10,7 @@ import { LegacyRuleDetector } from '../../rule/legacy-rule-detector.js';
10
10
  import { RuleTemplateService } from '../../rule/rule-template-service.js';
11
11
  import { HttpSpaceService } from '../../space/http-space-service.js';
12
12
  import { FileGlobalConfigStore } from "../../storage/file-global-config-store.js";
13
- import { KeychainTokenStore } from '../../storage/keychain-token-store.js';
13
+ import { createTokenStore } from '../../storage/token-store.js';
14
14
  import { HttpTeamService } from '../../team/http-team-service.js';
15
15
  import { FsTemplateLoader } from '../../template/fs-template-loader.js';
16
16
  import { ReplTerminal } from '../../terminal/repl-terminal.js';
@@ -37,7 +37,7 @@ export const initCommand = {
37
37
  const terminal = new ReplTerminal({ onMessage, onPrompt });
38
38
  // Create services
39
39
  const envConfig = getCurrentConfig();
40
- const tokenStore = new KeychainTokenStore();
40
+ const tokenStore = createTokenStore();
41
41
  const globalConfigStore = new FileGlobalConfigStore();
42
42
  const trackingService = new MixpanelTrackingService({ globalConfigStore, tokenStore });
43
43
  const fileService = new FsFileService();
@@ -6,7 +6,7 @@ import { OidcDiscoveryService } from '../../auth/oidc-discovery-service.js';
6
6
  import { SystemBrowserLauncher } from '../../browser/system-browser-launcher.js';
7
7
  import { CallbackHandler } from '../../http/callback-handler.js';
8
8
  import { FileGlobalConfigStore } from "../../storage/file-global-config-store.js";
9
- import { KeychainTokenStore } from '../../storage/keychain-token-store.js';
9
+ import { createTokenStore } from '../../storage/token-store.js';
10
10
  import { ReplTerminal } from '../../terminal/repl-terminal.js';
11
11
  import { MixpanelTrackingService } from '../../tracking/mixpanel-tracking-service.js';
12
12
  import { LoginUseCase } from '../../usecase/login-use-case.js';
@@ -23,7 +23,7 @@ export const loginCommand = {
23
23
  const terminal = new ReplTerminal({ onMessage, onPrompt });
24
24
  // Create services
25
25
  const config = getCurrentConfig();
26
- const tokenStore = new KeychainTokenStore();
26
+ const tokenStore = createTokenStore();
27
27
  const globalConfigStore = new FileGlobalConfigStore();
28
28
  const trackingService = new MixpanelTrackingService({ globalConfigStore, tokenStore });
29
29
  const discoveryService = new OidcDiscoveryService();
@@ -1,7 +1,7 @@
1
1
  import { CommandKind } from '../../../tui/types.js';
2
2
  import { FileGlobalConfigStore } from "../../storage/file-global-config-store.js";
3
3
  import { FileOnboardingPreferenceStore } from '../../storage/file-onboarding-preference-store.js';
4
- import { KeychainTokenStore } from '../../storage/keychain-token-store.js';
4
+ import { createTokenStore } from '../../storage/token-store.js';
5
5
  import { ReplTerminal } from '../../terminal/repl-terminal.js';
6
6
  import { MixpanelTrackingService } from '../../tracking/mixpanel-tracking-service.js';
7
7
  import { LogoutUseCase } from '../../usecase/logout-use-case.js';
@@ -26,7 +26,7 @@ export const logoutCommand = {
26
26
  flags: logoutFlags,
27
27
  strict: false,
28
28
  });
29
- const tokenStore = new KeychainTokenStore();
29
+ const tokenStore = createTokenStore();
30
30
  const globalConfigStore = new FileGlobalConfigStore();
31
31
  const useCase = new LogoutUseCase({
32
32
  onboardingPreferenceStore: new FileOnboardingPreferenceStore(),
@@ -6,7 +6,7 @@ import { ProjectConfigStore } from '../../config/file-config-store.js';
6
6
  import { FileContextTreeSnapshotService } from '../../context-tree/file-context-tree-snapshot-service.js';
7
7
  import { FileContextTreeWriterService } from '../../context-tree/file-context-tree-writer-service.js';
8
8
  import { FileGlobalConfigStore } from "../../storage/file-global-config-store.js";
9
- import { KeychainTokenStore } from '../../storage/keychain-token-store.js';
9
+ import { createTokenStore } from '../../storage/token-store.js';
10
10
  import { ReplTerminal } from '../../terminal/repl-terminal.js';
11
11
  import { MixpanelTrackingService } from '../../tracking/mixpanel-tracking-service.js';
12
12
  import { PullUseCase } from '../../usecase/pull-use-case.js';
@@ -28,7 +28,7 @@ export const pullCommand = {
28
28
  const terminal = new ReplTerminal({ onMessage, onPrompt });
29
29
  const parsed = await parseReplArgs(args, { flags: pullFlags, strict: false });
30
30
  const envConfig = getCurrentConfig();
31
- const tokenStore = new KeychainTokenStore();
31
+ const tokenStore = createTokenStore();
32
32
  const globalConfigStore = new FileGlobalConfigStore();
33
33
  const trackingService = new MixpanelTrackingService({ globalConfigStore, tokenStore });
34
34
  const contextTreeSnapshotService = new FileContextTreeSnapshotService();
@@ -6,7 +6,7 @@ import { ProjectConfigStore } from '../../config/file-config-store.js';
6
6
  import { FileContextFileReader } from '../../context-tree/file-context-file-reader.js';
7
7
  import { FileContextTreeSnapshotService } from '../../context-tree/file-context-tree-snapshot-service.js';
8
8
  import { FileGlobalConfigStore } from "../../storage/file-global-config-store.js";
9
- import { KeychainTokenStore } from '../../storage/keychain-token-store.js';
9
+ import { createTokenStore } from '../../storage/token-store.js';
10
10
  import { ReplTerminal } from '../../terminal/repl-terminal.js';
11
11
  import { MixpanelTrackingService } from '../../tracking/mixpanel-tracking-service.js';
12
12
  import { PushUseCase } from '../../usecase/push-use-case.js';
@@ -34,7 +34,7 @@ export const pushCommand = {
34
34
  const terminal = new ReplTerminal({ onMessage, onPrompt });
35
35
  const parsed = await parseReplArgs(args, { flags: pushFlags, strict: false });
36
36
  const envConfig = getCurrentConfig();
37
- const tokenStore = new KeychainTokenStore();
37
+ const tokenStore = createTokenStore();
38
38
  const globalConfigStore = new FileGlobalConfigStore();
39
39
  const trackingService = new MixpanelTrackingService({ globalConfigStore, tokenStore });
40
40
  const useCase = new PushUseCase({
@@ -1,7 +1,7 @@
1
1
  import { isDevelopment } from '../../../config/environment.js';
2
2
  import { CommandKind } from '../../../tui/types.js';
3
3
  import { FileGlobalConfigStore } from "../../storage/file-global-config-store.js";
4
- import { KeychainTokenStore } from '../../storage/keychain-token-store.js';
4
+ import { createTokenStore } from '../../storage/token-store.js';
5
5
  import { ReplTerminal } from '../../terminal/repl-terminal.js';
6
6
  import { MixpanelTrackingService } from '../../tracking/mixpanel-tracking-service.js';
7
7
  import { QueryUseCase } from '../../usecase/query-use-case.js';
@@ -31,7 +31,7 @@ export const queryCommand = {
31
31
  query = args;
32
32
  }
33
33
  const terminal = new ReplTerminal({ onMessage, onPrompt });
34
- const tokenStore = new KeychainTokenStore();
34
+ const tokenStore = createTokenStore();
35
35
  const globalConfigStore = new FileGlobalConfigStore();
36
36
  const useCase = new QueryUseCase({
37
37
  terminal,
@@ -2,7 +2,7 @@ import { getCurrentConfig } from '../../../../config/environment.js';
2
2
  import { CommandKind } from '../../../../tui/types.js';
3
3
  import { ProjectConfigStore } from '../../../config/file-config-store.js';
4
4
  import { HttpSpaceService } from '../../../space/http-space-service.js';
5
- import { KeychainTokenStore } from '../../../storage/keychain-token-store.js';
5
+ import { createTokenStore } from '../../../storage/token-store.js';
6
6
  import { ReplTerminal } from '../../../terminal/repl-terminal.js';
7
7
  import { SpaceListUseCase } from '../../../usecase/space-list-use-case.js';
8
8
  import { Flags, parseReplArgs, toCommandFlags } from '../arg-parser.js';
@@ -54,7 +54,7 @@ export const listCommand = {
54
54
  projectConfigStore: new ProjectConfigStore(),
55
55
  spaceService: new HttpSpaceService({ apiBaseUrl: envConfig.apiBaseUrl }),
56
56
  terminal,
57
- tokenStore: new KeychainTokenStore(),
57
+ tokenStore: createTokenStore(),
58
58
  });
59
59
  await useCase.run();
60
60
  },
@@ -2,7 +2,7 @@ import { getCurrentConfig } from '../../../../config/environment.js';
2
2
  import { CommandKind } from '../../../../tui/types.js';
3
3
  import { ProjectConfigStore } from '../../../config/file-config-store.js';
4
4
  import { HttpSpaceService } from '../../../space/http-space-service.js';
5
- import { KeychainTokenStore } from '../../../storage/keychain-token-store.js';
5
+ import { createTokenStore } from '../../../storage/token-store.js';
6
6
  import { HttpTeamService } from '../../../team/http-team-service.js';
7
7
  import { ReplTerminal } from '../../../terminal/repl-terminal.js';
8
8
  import { SpaceSwitchUseCase } from '../../../usecase/space-switch-use-case.js';
@@ -21,7 +21,7 @@ export const switchCommand = {
21
21
  spaceService: new HttpSpaceService({ apiBaseUrl: envConfig.apiBaseUrl }),
22
22
  teamService: new HttpTeamService({ apiBaseUrl: envConfig.apiBaseUrl }),
23
23
  terminal,
24
- tokenStore: new KeychainTokenStore(),
24
+ tokenStore: createTokenStore(),
25
25
  workspaceDetector: new WorkspaceDetectorService(),
26
26
  });
27
27
  await useCase.run();
@@ -3,7 +3,7 @@ import { ProjectConfigStore } from '../../config/file-config-store.js';
3
3
  import { FileContextTreeService } from '../../context-tree/file-context-tree-service.js';
4
4
  import { FileContextTreeSnapshotService } from '../../context-tree/file-context-tree-snapshot-service.js';
5
5
  import { FileGlobalConfigStore } from "../../storage/file-global-config-store.js";
6
- import { KeychainTokenStore } from '../../storage/keychain-token-store.js';
6
+ import { createTokenStore } from '../../storage/token-store.js';
7
7
  import { ReplTerminal } from '../../terminal/repl-terminal.js';
8
8
  import { MixpanelTrackingService } from '../../tracking/mixpanel-tracking-service.js';
9
9
  import { StatusUseCase } from '../../usecase/status-use-case.js';
@@ -15,7 +15,7 @@ export const statusCommand = {
15
15
  return {
16
16
  async execute(onMessage, onPrompt) {
17
17
  const terminal = new ReplTerminal({ onMessage, onPrompt });
18
- const tokenStore = new KeychainTokenStore();
18
+ const tokenStore = createTokenStore();
19
19
  const globalConfigStore = new FileGlobalConfigStore();
20
20
  const trackingService = new MixpanelTrackingService({ globalConfigStore, tokenStore });
21
21
  const useCase = new StatusUseCase({
@@ -2,7 +2,6 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { render } from 'ink';
3
3
  import { App } from '../../tui/app.js';
4
4
  import { AppProviders } from '../../tui/providers/app-providers.js';
5
- import { stopQueuePollingService } from '../cipher/consumer/queue-polling-service.js';
6
5
  import { connectTransportClient, disconnectTransportClient } from './transport-client-helper.js';
7
6
  /** Broadcast client - joins broadcast-room to monitor all events */
8
7
  let transportBroadcastClient = null;
@@ -30,6 +29,5 @@ export async function startRepl(options) {
30
29
  // Cleanup
31
30
  await disconnectTransportClient(transportBroadcastClient);
32
31
  transportBroadcastClient = null;
33
- stopQueuePollingService();
34
32
  await trackingService.track('repl', { status: 'finished' });
35
33
  }