@link-assistant/hive-mind 1.50.8 → 1.50.9

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.
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Handle lightweight early-exit paths before solve loads its full dependency graph.
5
+ *
6
+ * @param {string[]} earlyArgs - Raw CLI args without the node/script prefix
7
+ * @returns {Promise<void>}
8
+ */
9
+ export async function handleSolveEarlyExit(earlyArgs) {
10
+ if (earlyArgs.includes('--version')) {
11
+ const { getVersion } = await import('./version.lib.mjs');
12
+ try {
13
+ console.log(await getVersion());
14
+ } catch {
15
+ console.error('Error: Unable to determine version');
16
+ process.exit(1);
17
+ }
18
+ process.exit(0);
19
+ }
20
+
21
+ if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
22
+ // Load minimal modules needed for help output.
23
+ const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
24
+ globalThis.use = use;
25
+ const { initializeConfig, createYargsConfig } = await import('./solve.config.lib.mjs');
26
+ const { yargs, hideBin } = await initializeConfig(use);
27
+ const rawArgs = hideBin(process.argv);
28
+ const argsWithoutHelp = rawArgs.filter(arg => arg !== '--help' && arg !== '-h');
29
+ createYargsConfig(yargs(argsWithoutHelp)).showHelp();
30
+ process.exit(0);
31
+ }
32
+
33
+ if (earlyArgs.length === 0) {
34
+ console.error('Usage: solve.mjs <issue-url> [options]');
35
+ console.error('\nError: Missing required github issue or pull request URL');
36
+ console.error('\nRun "solve.mjs --help" for more information');
37
+ process.exit(1);
38
+ }
39
+ }
@@ -229,13 +229,13 @@ export const SOLVE_OPTION_DEFINITIONS = {
229
229
  },
230
230
  think: {
231
231
  type: 'string',
232
- description: 'Thinking level for Claude. Translated to --thinking-budget for Claude Code >= 2.1.12 (off=0, low=~8000, medium=~16000, high=~24000, max=31999). For older versions, uses thinking keywords.',
232
+ description: 'Thinking level hint. For Claude, translated to --thinking-budget for Claude Code >= 2.1.12 (off=0, low=~8000, medium=~16000, high=~24000, max=31999). For Codex, mapped to reasoning effort (off=none, low=low, medium=medium, high=high, max=xhigh).',
233
233
  choices: ['off', 'low', 'medium', 'high', 'max'],
234
234
  default: undefined,
235
235
  },
236
236
  'thinking-budget': {
237
237
  type: 'number',
238
- description: 'Thinking token budget for Claude Code (0-31999). Controls MAX_THINKING_TOKENS. Default: 0 (thinking disabled). For older Claude Code versions, translated back to --think level.',
238
+ description: 'Thinking token budget. For Claude Code, controls MAX_THINKING_TOKENS (0-31999 by default). For Codex, enables finer reasoning-effort mapping including minimal/low/medium/high/xhigh.',
239
239
  default: undefined,
240
240
  },
241
241
  'thinking-budget-claude-minimum-version': {
@@ -250,7 +250,7 @@ export const SOLVE_OPTION_DEFINITIONS = {
250
250
  },
251
251
  'prompt-plan-sub-agent': {
252
252
  type: 'boolean',
253
- description: 'Encourage AI to use Plan sub-agent for initial planning (only works with --tool claude)',
253
+ description: 'Encourage AI to use a planning sub-agent or planning workflow for initial planning. Supported for --tool claude and --tool codex.',
254
254
  default: false,
255
255
  },
256
256
  'base-branch': {
@@ -326,27 +326,27 @@ export const SOLVE_OPTION_DEFINITIONS = {
326
326
  },
327
327
  'interactive-mode': {
328
328
  type: 'boolean',
329
- description: '[EXPERIMENTAL] Post Claude output as PR comments in real-time. Only supported for --tool claude.',
329
+ description: '[EXPERIMENTAL] Post tool output as PR comments in real-time. Supported for --tool claude and --tool codex.',
330
330
  default: false,
331
331
  },
332
332
  'prompt-explore-sub-agent': {
333
333
  type: 'boolean',
334
- description: 'Encourage Claude to use Explore sub-agent for codebase exploration. Only supported for --tool claude.',
334
+ description: 'Encourage AI to use Explore-style sub-agent workflow for codebase exploration. Supported for --tool claude and --tool codex.',
335
335
  default: false,
336
336
  },
337
337
  'prompt-general-purpose-sub-agent': {
338
338
  type: 'boolean',
339
- description: 'Prompt AI to use general-purpose sub agents for processing large tasks with multiple files/folders. Only supported for --tool claude.',
339
+ description: 'Prompt AI to use general-purpose sub agents for processing large tasks with multiple files/folders. Supported for --tool claude and --tool codex.',
340
340
  default: false,
341
341
  },
342
342
  'tokens-budget-stats': {
343
343
  type: 'boolean',
344
- description: '[EXPERIMENTAL] Show detailed token budget statistics including context window usage and ratios. Only supported for --tool claude.',
344
+ description: '[EXPERIMENTAL] Show detailed token budget statistics including context window usage and ratios. Supported for --tool claude, --tool codex, and any tool that returns detailed token usage.',
345
345
  default: false,
346
346
  },
347
347
  'prompt-issue-reporting': {
348
348
  type: 'boolean',
349
- description: 'Enable automatic issue creation for spotted bugs/errors not related to main task. Issues will include reproducible examples, workarounds, and fix suggestions. Works for both current and third-party repositories. Only supported for --tool claude.',
349
+ description: 'Enable automatic issue creation for spotted bugs/errors not related to main task. Issues will include reproducible examples, workarounds, and fix suggestions. Works for both current and third-party repositories. Supported for --tool claude and --tool codex.',
350
350
  default: false,
351
351
  },
352
352
  'prompt-architecture-care': {
@@ -356,12 +356,12 @@ export const SOLVE_OPTION_DEFINITIONS = {
356
356
  },
357
357
  'prompt-case-studies': {
358
358
  type: 'boolean',
359
- description: 'Create comprehensive case study documentation for the issue including logs, analysis, timeline, root cause investigation, and proposed solutions. Organizes findings into ./docs/case-studies/issue-{id}/ directory. Only supported for --tool claude.',
359
+ description: 'Create comprehensive case study documentation for the issue including logs, analysis, timeline, root cause investigation, and proposed solutions. Organizes findings into ./docs/case-studies/issue-{id}/ directory. Supported for --tool claude and --tool codex.',
360
360
  default: false,
361
361
  },
362
362
  'prompt-playwright-mcp': {
363
363
  type: 'boolean',
364
- description: 'Enable Playwright MCP browser automation hints in system prompt (enabled by default, only takes effect if Playwright MCP is installed). Use --no-prompt-playwright-mcp to disable. Only supported for --tool claude.',
364
+ description: 'Enable Playwright MCP browser automation hints in system prompt (enabled by default, only takes effect if Playwright MCP is installed). Use --no-prompt-playwright-mcp to disable. Supported for --tool claude and --tool codex.',
365
365
  default: true,
366
366
  },
367
367
  'prompt-check-sibling-pull-requests': {
@@ -391,7 +391,7 @@ export const SOLVE_OPTION_DEFINITIONS = {
391
391
  },
392
392
  'prompt-subagents-via-agent-commander': {
393
393
  type: 'boolean',
394
- description: 'Guide Claude to use agent-commander CLI (start-agent) instead of native Task tool for subagent delegation. Allows using any supported agent type (claude, opencode, codex, agent) with unified API. Only works with --tool claude and requires agent-commander to be installed.',
394
+ description: 'Guide AI to use agent-commander CLI (start-agent) instead of native tool-specific delegation for subagent work. Allows using any supported agent type (claude, opencode, codex, agent) with a unified API. Supported for --tool claude and --tool codex and requires agent-commander to be installed.',
395
395
  default: false,
396
396
  },
397
397
  'auto-init-repository': {
package/src/solve.mjs CHANGED
@@ -1,39 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // Import Sentry instrumentation first (must be before other imports)
3
3
  import './instrument.mjs';
4
- // Early exit paths - handle these before loading all modules to speed up testing
5
4
  const earlyArgs = process.argv.slice(2);
6
- if (earlyArgs.includes('--version')) {
7
- const { getVersion } = await import('./version.lib.mjs');
8
- try {
9
- const version = await getVersion();
10
- console.log(version);
11
- } catch {
12
- console.error('Error: Unable to determine version');
13
- process.exit(1);
14
- }
15
- process.exit(0);
16
- }
17
- if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
18
- // Load minimal modules needed for help
19
- const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
20
- globalThis.use = use;
21
- const config = await import('./solve.config.lib.mjs');
22
- const { initializeConfig, createYargsConfig } = config;
23
- const { yargs, hideBin } = await initializeConfig(use);
24
- const rawArgs = hideBin(process.argv);
25
- // Filter out help flags to avoid duplicate display
26
- const argsWithoutHelp = rawArgs.filter(arg => arg !== '--help' && arg !== '-h');
27
- createYargsConfig(yargs(argsWithoutHelp)).showHelp();
28
- process.exit(0);
29
- }
30
- if (earlyArgs.length === 0) {
31
- console.error('Usage: solve.mjs <issue-url> [options]');
32
- console.error('\nError: Missing required github issue or pull request URL');
33
- console.error('\nRun "solve.mjs --help" for more information');
34
- process.exit(1);
35
- }
36
- // Now load all modules for normal operation
5
+ const { handleSolveEarlyExit } = await import('./solve.bootstrap.lib.mjs');
6
+ await handleSolveEarlyExit(earlyArgs);
7
+
37
8
  const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
38
9
  globalThis.use = use;
39
10
  const { $ } = await use('command-stream');
@@ -58,9 +29,9 @@ const { processAutoContinueForIssue } = autoContinue;
58
29
  const repository = await import('./solve.repository.lib.mjs');
59
30
  const { setupTempDirectory, cleanupTempDirectory } = repository;
60
31
  const results = await import('./solve.results.lib.mjs');
61
- const { cleanupClaudeFile, showSessionSummary, verifyResults, buildClaudeResumeCommand, checkForAiCreatedComments, attachSolutionSummary } = results;
32
+ const { cleanupClaudeFile, showSessionSummary, verifyResults, buildClaudeResumeCommand, buildSolveResumeCommand, checkForAiCreatedComments, attachSolutionSummary } = results;
62
33
  const claudeLib = await import('./claude.lib.mjs');
63
- const { executeClaude } = claudeLib;
34
+ const { executeClaude, checkPlaywrightMcpAvailability } = claudeLib;
64
35
 
65
36
  const githubLinking = await import('./github-linking.lib.mjs');
66
37
  const { extractLinkedIssueNumber } = githubLinking;
@@ -769,9 +740,21 @@ try {
769
740
  });
770
741
  } else if (argv.tool === 'codex') {
771
742
  const codexLib = await import('./codex.lib.mjs');
772
- const { executeCodex } = codexLib;
743
+ const { executeCodex, checkPlaywrightMcpAvailability } = codexLib;
773
744
  const codexPath = process.env.CODEX_PATH || 'codex';
774
745
 
746
+ if (argv.promptPlaywrightMcp) {
747
+ const playwrightMcpAvailable = await checkPlaywrightMcpAvailability();
748
+ if (playwrightMcpAvailable) {
749
+ await log('🎭 Playwright MCP detected - enabling browser automation hints', { verbose: true });
750
+ } else {
751
+ await log('ℹ️ Playwright MCP not detected - browser automation hints will be disabled', { verbose: true });
752
+ argv.promptPlaywrightMcp = false;
753
+ }
754
+ } else {
755
+ await log('ℹ️ Playwright MCP explicitly disabled via --no-prompt-playwright-mcp', { verbose: true });
756
+ }
757
+
775
758
  toolResult = await executeCodex({
776
759
  issueUrl,
777
760
  issueNumber,
@@ -831,7 +814,6 @@ try {
831
814
  if (argv.tool === 'claude' || !argv.tool) {
832
815
  // If flag is true (default), check if Playwright MCP is actually available
833
816
  if (argv.promptPlaywrightMcp) {
834
- const { checkPlaywrightMcpAvailability } = claudeLib;
835
817
  const playwrightMcpAvailable = await checkPlaywrightMcpAvailability();
836
818
  if (playwrightMcpAvailable) {
837
819
  await log('🎭 Playwright MCP detected - enabling browser automation hints', { verbose: true });
@@ -927,6 +909,12 @@ try {
927
909
  await log('');
928
910
  await log(` ${claudeResumeCmd}`);
929
911
  await log('');
912
+ } else if (argv.url) {
913
+ const solveResumeCmd = buildSolveResumeCommand({ issueUrl: argv.url, sessionId, tool: toolForResume, model: argv.model, tempDir });
914
+ await log(`💡 To continue this ${toolForResume} session with solve:`);
915
+ await log('');
916
+ await log(` ${solveResumeCmd}`);
917
+ await log('');
930
918
  }
931
919
  }
932
920
 
@@ -936,7 +924,7 @@ try {
936
924
  try {
937
925
  // Build Claude CLI resume command
938
926
  const tool = argv.tool || 'claude';
939
- const resumeCommand = tool === 'claude' ? buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model }) : null;
927
+ const resumeCommand = tool === 'claude' ? buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model }) : sessionId ? buildSolveResumeCommand({ issueUrl: argv.url, sessionId, tool, model: argv.model, tempDir }) : null;
940
928
  const logUploadSuccess = await attachLogToGitHub({
941
929
  logFile: getLogFile(),
942
930
  targetType: 'pr',
@@ -974,7 +962,7 @@ try {
974
962
  const resetTime = global.limitResetTime;
975
963
  // Build Claude CLI resume command
976
964
  const tool = argv.tool || 'claude';
977
- const resumeCmd = tool === 'claude' ? buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model }) : null;
965
+ const resumeCmd = tool === 'claude' ? buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model }) : sessionId ? buildSolveResumeCommand({ issueUrl: argv.url, sessionId, tool, model: argv.model, tempDir }) : null;
978
966
  const resumeSection = resumeCmd ? `To resume after the limit resets, use:\n\`\`\`bash\n${resumeCmd}\n\`\`\`` : `Session ID: \`${sessionId}\``;
979
967
  // Format the reset time with relative time and UTC conversion if available
980
968
  const timezone = global.limitTimezone || null;
@@ -1002,7 +990,7 @@ try {
1002
990
  try {
1003
991
  // Build Claude CLI resume command (only for logging, not shown to users when auto-resume is enabled)
1004
992
  const tool = argv.tool || 'claude';
1005
- const resumeCommand = tool === 'claude' ? buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model }) : null;
993
+ const resumeCommand = tool === 'claude' ? buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model }) : sessionId ? buildSolveResumeCommand({ issueUrl: argv.url, sessionId, tool, model: argv.model, tempDir }) : null;
1006
994
  const logUploadSuccess = await attachLogToGitHub({
1007
995
  logFile: getLogFile(),
1008
996
  targetType: 'pr',
@@ -1090,6 +1078,13 @@ try {
1090
1078
  await log('');
1091
1079
  await log(` ${claudeResumeCmd}`);
1092
1080
  await log('');
1081
+ } else if (sessionId && argv.url) {
1082
+ const solveResumeCmd = buildSolveResumeCommand({ issueUrl: argv.url, sessionId, tool: toolForFailure, model: argv.model, tempDir });
1083
+ await log('');
1084
+ await log(`💡 To continue this ${toolForFailure} session with solve:`);
1085
+ await log('');
1086
+ await log(` ${solveResumeCmd}`);
1087
+ await log('');
1093
1088
  }
1094
1089
 
1095
1090
  // Attach failure logs before exiting (Issues #1212, #1462: fall back to issue if no PR)
@@ -1104,7 +1099,7 @@ try {
1104
1099
  try {
1105
1100
  // Build Claude CLI resume command
1106
1101
  const tool = argv.tool || 'claude';
1107
- const resumeCommand = sessionId && tool === 'claude' ? buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model }) : null;
1102
+ const resumeCommand = sessionId ? (tool === 'claude' ? buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model }) : buildSolveResumeCommand({ issueUrl: argv.url, sessionId, tool, model: argv.model, tempDir })) : null;
1108
1103
  const logUploadSuccess = await attachLogToGitHub({
1109
1104
  logFile: getLogFile(),
1110
1105
  targetType: logTargetType,
@@ -358,8 +358,8 @@ export const createProgressMonitor = ({ owner, repo, prNumber, $, log, verbose =
358
358
  };
359
359
 
360
360
  /**
361
- * Process a Claude CLI stream event, detecting TodoWrite tool calls
362
- * and updating progress automatically. Call this for each parsed NDJSON event.
361
+ * Process a tool stream event, detecting Claude TodoWrite or Codex todo_list
362
+ * updates and updating progress automatically.
363
363
  *
364
364
  * @param {Object} data - Parsed JSON event from Claude CLI stream
365
365
  * @param {boolean} force - Force update even if within rate limit interval
@@ -381,6 +381,14 @@ export const createProgressMonitor = ({ owner, repo, prNumber, $, log, verbose =
381
381
  if (data.type === 'user' && data.tool_use_result?.newTodos) {
382
382
  updated = await updateProgress(data.tool_use_result.newTodos, force);
383
383
  }
384
+ // Pattern 3: Codex item event with todo_list payload
385
+ if ((data.type === 'item.started' || data.type === 'item.updated' || data.type === 'item.completed') && data.item?.type === 'todo_list' && Array.isArray(data.item.items)) {
386
+ const todos = data.item.items.map(todo => ({
387
+ status: todo?.completed ? 'completed' : 'pending',
388
+ content: todo?.text || '',
389
+ }));
390
+ updated = await updateProgress(todos, force);
391
+ }
384
392
  return updated;
385
393
  };
386
394
 
@@ -208,9 +208,21 @@ export const executeToolIteration = async params => {
208
208
  } else if (argv.tool === 'codex') {
209
209
  // Use Codex
210
210
  const codexExecLib = await import('./codex.lib.mjs');
211
- const { executeCodex } = codexExecLib;
211
+ const { executeCodex, checkPlaywrightMcpAvailability } = codexExecLib;
212
212
  const codexPath = argv.codexPath || 'codex';
213
213
 
214
+ if (argv.promptPlaywrightMcp) {
215
+ const playwrightMcpAvailable = await checkPlaywrightMcpAvailability();
216
+ if (playwrightMcpAvailable) {
217
+ await log('🎭 Playwright MCP detected - enabling browser automation hints', { verbose: true });
218
+ } else {
219
+ await log('ℹ️ Playwright MCP not detected - browser automation hints will be disabled', { verbose: true });
220
+ argv.promptPlaywrightMcp = false;
221
+ }
222
+ } else {
223
+ await log('ℹ️ Playwright MCP explicitly disabled via --no-prompt-playwright-mcp', { verbose: true });
224
+ }
225
+
214
226
  toolResult = await executeCodex({
215
227
  issueUrl,
216
228
  issueNumber,
@@ -37,6 +37,41 @@ const { autoContinueWhenLimitResets } = autoContinue;
37
37
  const claudeCommandBuilder = await import('./claude.command-builder.lib.mjs');
38
38
  export const { buildClaudeResumeCommand, buildClaudeInitialCommand } = claudeCommandBuilder;
39
39
 
40
+ /**
41
+ * Build a solve.mjs resume command for tools that do not have a first-party interactive
42
+ * resume CLI flow like Claude Code. This keeps the invocation within hive-mind so the
43
+ * original tool selection and working directory can be preserved.
44
+ *
45
+ * @param {Object} options
46
+ * @param {string} options.issueUrl - The issue URL passed to solve.mjs
47
+ * @param {string} options.sessionId - The session ID to resume
48
+ * @param {string|null} [options.tool] - Tool name (codex, opencode, agent)
49
+ * @param {string|null} [options.model] - Model name to preserve
50
+ * @param {string|null} [options.tempDir] - Working directory to preserve
51
+ * @param {string} [options.nodePath] - Node binary path
52
+ * @param {string} [options.scriptPath] - solve.mjs path
53
+ * @returns {string}
54
+ */
55
+ export const buildSolveResumeCommand = ({ issueUrl, sessionId, tool = null, model = null, tempDir = null, nodePath = process.argv[0], scriptPath = process.argv[1] }) => {
56
+ const shellQuote = value => `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
57
+
58
+ const args = [shellQuote(scriptPath), shellQuote(issueUrl), '--resume', shellQuote(sessionId)];
59
+
60
+ if (tool && tool !== 'claude') {
61
+ args.push('--tool', shellQuote(tool));
62
+ }
63
+
64
+ if (model) {
65
+ args.push('--model', shellQuote(model));
66
+ }
67
+
68
+ if (tempDir) {
69
+ args.push('--working-directory', shellQuote(tempDir));
70
+ }
71
+
72
+ return `${shellQuote(nodePath)} ${args.join(' ')}`;
73
+ };
74
+
40
75
  // Import error handling functions
41
76
  // const errorHandlers = await import('./solve.error-handlers.lib.mjs'); // Not currently used
42
77
  // Import Sentry integration
@@ -444,12 +479,8 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
444
479
  const absoluteLogPath = path.resolve(getLogFile());
445
480
  await log(`✅ Complete log file: ${absoluteLogPath}`);
446
481
 
447
- // Show claude resume command only for --tool claude (or default)
448
- // This allows users to investigate, resume, see context, and more
449
- // Uses the (cd ... && claude --resume ...) pattern for a fully copyable, executable command
450
482
  const tool = argv.tool || 'claude';
451
483
  if (tool === 'claude') {
452
- // Build the Claude CLI resume command using the command builder
453
484
  const claudeResumeCmd = buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model });
454
485
 
455
486
  await log('');
@@ -457,6 +488,13 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
457
488
  await log('');
458
489
  await log(` ${claudeResumeCmd}`);
459
490
  await log('');
491
+ } else if (issueUrl) {
492
+ const solveResumeCmd = buildSolveResumeCommand({ issueUrl, sessionId, tool, model: argv.model, tempDir });
493
+ await log('');
494
+ await log(`💡 To continue this ${tool} session with solve:`);
495
+ await log('');
496
+ await log(` ${solveResumeCmd}`);
497
+ await log('');
460
498
  }
461
499
 
462
500
  if (limitReached) {
@@ -472,7 +510,7 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
472
510
  await log(`\n⏰ Limit resets at: ${global.limitResetTime}`);
473
511
  }
474
512
 
475
- await log('\n💡 After the limit resets, resume using the Claude command above.');
513
+ await log('\n💡 After the limit resets, resume using the command above.');
476
514
 
477
515
  if (argv.autoCleanup !== false) {
478
516
  await log('');
@@ -306,7 +306,7 @@ export const performSystemChecks = async (minDiskSpace = 2048, skipToolConnectio
306
306
  } else if (argv.tool === 'codex') {
307
307
  // Validate Codex connection
308
308
  const codexLib = await import('./codex.lib.mjs');
309
- isToolConnected = await codexLib.validateCodexConnection(model);
309
+ isToolConnected = await codexLib.validateCodexConnection(model, argv.verbose);
310
310
  if (!isToolConnected) {
311
311
  await log('❌ Cannot proceed without Codex connection', { level: 'error' });
312
312
  return false;
@@ -41,7 +41,7 @@ const { parseGitHubUrl, validateGitHubEntityExistence } = await import('./github
41
41
  const { validateModelName, buildModelOptionDescription } = await import('./models/index.mjs');
42
42
  const { validateBranchInArgs } = await import('./solve.branch.lib.mjs');
43
43
  const { extractIsolationFromArgs, isValidPerCommandIsolation, resolveIsolation, createIsolationAwareQueueCallback } = await import('./telegram-isolation.lib.mjs');
44
- const { formatUsageMessage, getAllCachedLimits } = await import('./limits.lib.mjs');
44
+ const { formatUsageMessage, formatCodexLimitsSection, getAllCachedLimits } = await import('./limits.lib.mjs');
45
45
  const { getVersionInfo, formatVersionMessage } = await import('./version-info.lib.mjs');
46
46
  const { escapeMarkdown, escapeMarkdownV2, cleanNonPrintableChars, makeSpecialCharsVisible } = await import('./telegram-markdown.lib.mjs');
47
47
  const { getSolveQueue, createQueueExecuteCallback } = await import('./telegram-solve-queue.lib.mjs');
@@ -758,9 +758,11 @@ bot.command('limits', async ctx => {
758
758
 
759
759
  // Format message with usage limits and queue status (issues #1343, #1267)
760
760
  const claudeError = limits.claude.success ? null : limits.claude.error;
761
+ const codexError = limits.codex.success ? null : limits.codex.error;
761
762
  const solveQueue = getSolveQueue({ verbose: VERBOSE });
762
763
  const queueStatus = await solveQueue.formatStatus();
763
- const message = '📊 *Usage Limits*\n\n' + formatUsageMessage(limits.claude.success ? limits.claude.usage : null, limits.disk.success ? limits.disk.diskSpace : null, limits.github.success ? limits.github.githubRateLimit : null, limits.cpu.success ? limits.cpu.cpuLoad : null, limits.memory.success ? limits.memory.memory : null, claudeError, [queueStatus]);
764
+ const codexSection = formatCodexLimitsSection(limits.codex.success ? limits.codex : null, codexError);
765
+ const message = '📊 *Usage Limits*\n\n' + formatUsageMessage(limits.claude.success ? limits.claude.usage : null, limits.disk.success ? limits.disk.diskSpace : null, limits.github.success ? limits.github.githubRateLimit : null, limits.cpu.success ? limits.cpu.cpuLoad : null, limits.memory.success ? limits.memory.memory : null, claudeError, [codexSection, queueStatus]);
764
766
  await ctx.telegram.editMessageText(fetchingMessage.chat.id, fetchingMessage.message_id, undefined, message, { parse_mode: 'Markdown' });
765
767
  });
766
768
  bot.command('version', async ctx => {
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { exec } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+
6
+ const execAsync = promisify(exec);
7
+
8
+ /**
9
+ * Count running processes by name.
10
+ * @param {string} processName - Process name to search for (e.g., 'claude', 'agent')
11
+ * @param {boolean} verbose - Whether to log verbose output
12
+ * @returns {Promise<{count: number, processes: string[]}>}
13
+ */
14
+ export async function getRunningProcesses(processName, verbose = false) {
15
+ try {
16
+ const { stdout } = await execAsync(`pgrep -l -x ${processName} 2>/dev/null || true`);
17
+ const lines = stdout
18
+ .trim()
19
+ .split('\n')
20
+ .filter(line => line.trim());
21
+
22
+ const processes = lines
23
+ .map(line => {
24
+ const parts = line.trim().split(/\s+/);
25
+ return {
26
+ pid: parts[0],
27
+ name: parts.slice(1).join(' ') || processName,
28
+ };
29
+ })
30
+ .filter(p => p.pid);
31
+
32
+ if (verbose) {
33
+ console.log(`[VERBOSE] /solve_queue found ${processes.length} running ${processName} processes`);
34
+ if (processes.length > 0) {
35
+ console.log(`[VERBOSE] /solve_queue processes: ${JSON.stringify(processes)}`);
36
+ }
37
+ }
38
+
39
+ return {
40
+ count: processes.length,
41
+ processes: processes.map(p => `${p.pid}:${p.name}`),
42
+ };
43
+ } catch (error) {
44
+ if (verbose) {
45
+ console.error(`[VERBOSE] /solve_queue error counting ${processName} processes:`, error.message);
46
+ }
47
+ return { count: 0, processes: [] };
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Count running claude processes.
53
+ * @param {boolean} verbose - Whether to log verbose output
54
+ * @returns {Promise<{count: number, processes: string[]}>}
55
+ */
56
+ export async function getRunningClaudeProcesses(verbose = false) {
57
+ return getRunningProcesses('claude', verbose);
58
+ }
59
+
60
+ /**
61
+ * Count running agent processes.
62
+ * @param {boolean} verbose - Whether to log verbose output
63
+ * @returns {Promise<{count: number, processes: string[]}>}
64
+ */
65
+ export async function getRunningAgentProcesses(verbose = false) {
66
+ return getRunningProcesses('agent', verbose);
67
+ }
68
+
69
+ /**
70
+ * Count running codex processes.
71
+ * @param {boolean} verbose - Whether to log verbose output
72
+ * @returns {Promise<{count: number, processes: string[]}>}
73
+ */
74
+ export async function getRunningCodexProcesses(verbose = false) {
75
+ return getRunningProcesses('codex', verbose);
76
+ }
77
+
78
+ /**
79
+ * Format a threshold as percentage for display.
80
+ * @param {number} ratio - Ratio (0.0 - 1.0)
81
+ * @returns {string} Formatted percentage
82
+ */
83
+ export function formatThresholdPercent(ratio) {
84
+ return `${Math.round(ratio * 100)}%`;
85
+ }
86
+
87
+ /**
88
+ * Format milliseconds into human-readable duration.
89
+ * Shows days, hours, minutes, and seconds as appropriate.
90
+ * Examples: "5h 43m 23s", "2m 15s", "45s", "1d 3h 12m 5s"
91
+ *
92
+ * @param {number} ms - Duration in milliseconds
93
+ * @returns {string} Human-readable duration
94
+ * @see https://github.com/link-assistant/hive-mind/issues/1267
95
+ */
96
+ export function formatDuration(ms) {
97
+ if (ms < 0) ms = 0;
98
+
99
+ const totalSeconds = Math.floor(ms / 1000);
100
+ const days = Math.floor(totalSeconds / 86400);
101
+ const hours = Math.floor((totalSeconds % 86400) / 3600);
102
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
103
+ const seconds = totalSeconds % 60;
104
+
105
+ const parts = [];
106
+ if (days > 0) parts.push(`${days}d`);
107
+ if (hours > 0) parts.push(`${hours}h`);
108
+ if (minutes > 0) parts.push(`${minutes}m`);
109
+ if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
110
+
111
+ return parts.join(' ');
112
+ }
113
+
114
+ /**
115
+ * Generate human-readable waiting reason based on threshold violation.
116
+ * @param {string} metric - The metric name (ram, cpu, disk, etc.)
117
+ * @param {number} currentValue - Current value (as percentage 0-100)
118
+ * @param {number} threshold - Threshold ratio (0.0 - 1.0)
119
+ * @returns {string} Human-readable reason
120
+ */
121
+ export function formatWaitingReason(metric, currentValue, threshold) {
122
+ const thresholdPercent = formatThresholdPercent(threshold);
123
+ const currentPercent = Math.round(currentValue);
124
+
125
+ switch (metric) {
126
+ case 'ram':
127
+ return `RAM usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
128
+ case 'cpu':
129
+ return `CPU usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
130
+ case 'disk':
131
+ return `Disk usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
132
+ case 'claude_5_hour_session':
133
+ return `Claude 5 hour session limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
134
+ case 'claude_weekly':
135
+ return `Claude weekly limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
136
+ case 'codex_5_hour_session':
137
+ return `Codex 5 hour session limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
138
+ case 'codex_weekly':
139
+ return `Codex weekly limit is ${currentPercent}% (threshold: ${thresholdPercent})`;
140
+ case 'github':
141
+ return `GitHub API usage is ${currentPercent}% (threshold: ${thresholdPercent})`;
142
+ case 'min_interval':
143
+ return 'Minimum interval between commands not reached';
144
+ case 'claude_running':
145
+ return 'Claude process is already running';
146
+ case 'codex_running':
147
+ return 'Codex process is already running';
148
+ default:
149
+ return `${metric} threshold exceeded`;
150
+ }
151
+ }