claude-code-workflow 6.3.2 → 6.3.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 (36) hide show
  1. package/.claude/CLAUDE.md +9 -1
  2. package/.claude/commands/workflow/lite-plan.md +1 -1
  3. package/.claude/workflows/cli-tools-usage.md +515 -516
  4. package/ccw/dist/cli.d.ts.map +1 -1
  5. package/ccw/dist/cli.js +6 -1
  6. package/ccw/dist/cli.js.map +1 -1
  7. package/ccw/dist/commands/cli.d.ts +1 -1
  8. package/ccw/dist/commands/cli.d.ts.map +1 -1
  9. package/ccw/dist/commands/cli.js +71 -7
  10. package/ccw/dist/commands/cli.js.map +1 -1
  11. package/ccw/dist/tools/cli-executor.d.ts.map +1 -1
  12. package/ccw/dist/tools/cli-executor.js +19 -7
  13. package/ccw/dist/tools/cli-executor.js.map +1 -1
  14. package/ccw/dist/tools/cli-history-store.d.ts +33 -0
  15. package/ccw/dist/tools/cli-history-store.d.ts.map +1 -1
  16. package/ccw/dist/tools/cli-history-store.js +89 -5
  17. package/ccw/dist/tools/cli-history-store.js.map +1 -1
  18. package/ccw/src/cli.ts +263 -258
  19. package/ccw/src/commands/cli.ts +967 -884
  20. package/ccw/src/tools/cli-executor.ts +20 -7
  21. package/ccw/src/tools/cli-history-store.ts +125 -5
  22. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  23. package/codex-lens/src/codexlens/config.py +3 -0
  24. package/codex-lens/src/codexlens/search/__pycache__/chain_search.cpython-313.pyc +0 -0
  25. package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
  26. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  27. package/codex-lens/src/codexlens/search/chain_search.py +71 -1
  28. package/codex-lens/src/codexlens/search/ranking.py +274 -274
  29. package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
  30. package/codex-lens/src/codexlens/storage/__pycache__/dir_index.cpython-313.pyc +0 -0
  31. package/codex-lens/src/codexlens/storage/__pycache__/global_index.cpython-313.pyc +0 -0
  32. package/codex-lens/src/codexlens/storage/__pycache__/index_tree.cpython-313.pyc +0 -0
  33. package/codex-lens/src/codexlens/storage/dir_index.py +1888 -1850
  34. package/codex-lens/src/codexlens/storage/global_index.py +365 -0
  35. package/codex-lens/src/codexlens/storage/index_tree.py +83 -10
  36. package/package.json +1 -1
@@ -73,6 +73,7 @@ const ParamsSchema = z.object({
73
73
  noNative: z.boolean().optional(), // Force prompt concatenation instead of native resume
74
74
  category: z.enum(['user', 'internal', 'insight']).default('user'), // Execution category for tracking
75
75
  parentExecutionId: z.string().optional(), // Parent execution ID for fork/retry scenarios
76
+ stream: z.boolean().default(false), // false = cache full output (default), true = stream output via callback
76
77
  });
77
78
 
78
79
  // Execution category types
@@ -863,24 +864,36 @@ async function executeCliTool(
863
864
  const endTime = Date.now();
864
865
  const duration = endTime - startTime;
865
866
 
866
- // Determine status
867
+ // Determine status - prioritize output content over exit code
867
868
  let status: 'success' | 'error' | 'timeout' = 'success';
868
869
  if (timedOut) {
869
870
  status = 'timeout';
870
871
  } else if (code !== 0) {
871
- // Check if HTTP 429 but results exist (Gemini quirk)
872
- if (stderr.includes('429') && stdout.trim()) {
872
+ // Non-zero exit code doesn't always mean failure
873
+ // Check if there's valid output (AI response) - treat as success
874
+ const hasValidOutput = stdout.trim().length > 0;
875
+ const hasFatalError = stderr.includes('FATAL') ||
876
+ stderr.includes('Authentication failed') ||
877
+ stderr.includes('API key') ||
878
+ stderr.includes('rate limit exceeded');
879
+
880
+ if (hasValidOutput && !hasFatalError) {
881
+ // Has output and no fatal errors - treat as success despite exit code
873
882
  status = 'success';
874
883
  } else {
875
884
  status = 'error';
876
885
  }
877
886
  }
878
887
 
879
- // Create new turn
888
+ // Create new turn - cache full output when not streaming (default)
889
+ const shouldCache = !parsed.data.stream;
880
890
  const newTurnOutput = {
881
- stdout: stdout.substring(0, 10240), // Truncate to 10KB
882
- stderr: stderr.substring(0, 2048), // Truncate to 2KB
883
- truncated: stdout.length > 10240 || stderr.length > 2048
891
+ stdout: stdout.substring(0, 10240), // Truncate preview to 10KB
892
+ stderr: stderr.substring(0, 2048), // Truncate preview to 2KB
893
+ truncated: stdout.length > 10240 || stderr.length > 2048,
894
+ cached: shouldCache,
895
+ stdout_full: shouldCache ? stdout : undefined,
896
+ stderr_full: shouldCache ? stderr : undefined
884
897
  };
885
898
 
886
899
  // Determine base turn number for merge scenarios
@@ -23,6 +23,9 @@ export interface ConversationTurn {
23
23
  stdout: string;
24
24
  stderr: string;
25
25
  truncated: boolean;
26
+ cached?: boolean;
27
+ stdout_full?: string;
28
+ stderr_full?: string;
26
29
  };
27
30
  }
28
31
 
@@ -315,6 +318,28 @@ export class CliHistoryStore {
315
318
  } catch (indexErr) {
316
319
  console.warn('[CLI History] Turns timestamp index creation warning:', (indexErr as Error).message);
317
320
  }
321
+
322
+ // Add cached output columns to turns table for non-streaming mode
323
+ const turnsInfo = this.db.prepare('PRAGMA table_info(turns)').all() as Array<{ name: string }>;
324
+ const hasCached = turnsInfo.some(col => col.name === 'cached');
325
+ const hasStdoutFull = turnsInfo.some(col => col.name === 'stdout_full');
326
+ const hasStderrFull = turnsInfo.some(col => col.name === 'stderr_full');
327
+
328
+ if (!hasCached) {
329
+ console.log('[CLI History] Migrating database: adding cached column to turns table...');
330
+ this.db.exec('ALTER TABLE turns ADD COLUMN cached INTEGER DEFAULT 0;');
331
+ console.log('[CLI History] Migration complete: cached column added');
332
+ }
333
+ if (!hasStdoutFull) {
334
+ console.log('[CLI History] Migrating database: adding stdout_full column to turns table...');
335
+ this.db.exec('ALTER TABLE turns ADD COLUMN stdout_full TEXT;');
336
+ console.log('[CLI History] Migration complete: stdout_full column added');
337
+ }
338
+ if (!hasStderrFull) {
339
+ console.log('[CLI History] Migrating database: adding stderr_full column to turns table...');
340
+ this.db.exec('ALTER TABLE turns ADD COLUMN stderr_full TEXT;');
341
+ console.log('[CLI History] Migration complete: stderr_full column added');
342
+ }
318
343
  } catch (err) {
319
344
  console.error('[CLI History] Migration error:', (err as Error).message);
320
345
  // Don't throw - allow the store to continue working with existing schema
@@ -421,8 +446,8 @@ export class CliHistoryStore {
421
446
  `);
422
447
 
423
448
  const upsertTurn = this.db.prepare(`
424
- INSERT INTO turns (conversation_id, turn_number, timestamp, prompt, duration_ms, status, exit_code, stdout, stderr, truncated)
425
- VALUES (@conversation_id, @turn_number, @timestamp, @prompt, @duration_ms, @status, @exit_code, @stdout, @stderr, @truncated)
449
+ INSERT INTO turns (conversation_id, turn_number, timestamp, prompt, duration_ms, status, exit_code, stdout, stderr, truncated, cached, stdout_full, stderr_full)
450
+ VALUES (@conversation_id, @turn_number, @timestamp, @prompt, @duration_ms, @status, @exit_code, @stdout, @stderr, @truncated, @cached, @stdout_full, @stderr_full)
426
451
  ON CONFLICT(conversation_id, turn_number) DO UPDATE SET
427
452
  timestamp = @timestamp,
428
453
  prompt = @prompt,
@@ -431,7 +456,10 @@ export class CliHistoryStore {
431
456
  exit_code = @exit_code,
432
457
  stdout = @stdout,
433
458
  stderr = @stderr,
434
- truncated = @truncated
459
+ truncated = @truncated,
460
+ cached = @cached,
461
+ stdout_full = @stdout_full,
462
+ stderr_full = @stderr_full
435
463
  `);
436
464
 
437
465
  const transaction = this.db.transaction(() => {
@@ -463,7 +491,10 @@ export class CliHistoryStore {
463
491
  exit_code: turn.exit_code,
464
492
  stdout: turn.output.stdout,
465
493
  stderr: turn.output.stderr,
466
- truncated: turn.output.truncated ? 1 : 0
494
+ truncated: turn.output.truncated ? 1 : 0,
495
+ cached: turn.output.cached ? 1 : 0,
496
+ stdout_full: turn.output.stdout_full || null,
497
+ stderr_full: turn.output.stderr_full || null
467
498
  });
468
499
  }
469
500
  });
@@ -507,7 +538,10 @@ export class CliHistoryStore {
507
538
  output: {
508
539
  stdout: t.stdout || '',
509
540
  stderr: t.stderr || '',
510
- truncated: !!t.truncated
541
+ truncated: !!t.truncated,
542
+ cached: !!t.cached,
543
+ stdout_full: t.stdout_full || undefined,
544
+ stderr_full: t.stderr_full || undefined
511
545
  }
512
546
  }))
513
547
  };
@@ -533,6 +567,92 @@ export class CliHistoryStore {
533
567
  };
534
568
  }
535
569
 
570
+ /**
571
+ * Get paginated cached output for a conversation turn
572
+ * @param conversationId - Conversation ID
573
+ * @param turnNumber - Turn number (default: latest turn)
574
+ * @param options - Pagination options
575
+ */
576
+ getCachedOutput(
577
+ conversationId: string,
578
+ turnNumber?: number,
579
+ options: {
580
+ offset?: number; // Character offset (default: 0)
581
+ limit?: number; // Max characters to return (default: 10000)
582
+ outputType?: 'stdout' | 'stderr' | 'both'; // Which output to fetch
583
+ } = {}
584
+ ): {
585
+ conversationId: string;
586
+ turnNumber: number;
587
+ stdout?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
588
+ stderr?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
589
+ cached: boolean;
590
+ prompt: string;
591
+ status: string;
592
+ timestamp: string;
593
+ } | null {
594
+ const { offset = 0, limit = 10000, outputType = 'both' } = options;
595
+
596
+ // Get turn (latest if not specified)
597
+ let turn;
598
+ if (turnNumber !== undefined) {
599
+ turn = this.db.prepare(`
600
+ SELECT * FROM turns WHERE conversation_id = ? AND turn_number = ?
601
+ `).get(conversationId, turnNumber) as any;
602
+ } else {
603
+ turn = this.db.prepare(`
604
+ SELECT * FROM turns WHERE conversation_id = ? ORDER BY turn_number DESC LIMIT 1
605
+ `).get(conversationId) as any;
606
+ }
607
+
608
+ if (!turn) return null;
609
+
610
+ const result: {
611
+ conversationId: string;
612
+ turnNumber: number;
613
+ stdout?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
614
+ stderr?: { content: string; totalBytes: number; offset: number; hasMore: boolean };
615
+ cached: boolean;
616
+ prompt: string;
617
+ status: string;
618
+ timestamp: string;
619
+ } = {
620
+ conversationId,
621
+ turnNumber: turn.turn_number,
622
+ cached: !!turn.cached,
623
+ prompt: turn.prompt,
624
+ status: turn.status,
625
+ timestamp: turn.timestamp
626
+ };
627
+
628
+ // Use full output if cached, otherwise use truncated
629
+ if (outputType === 'stdout' || outputType === 'both') {
630
+ const fullStdout = turn.cached ? (turn.stdout_full || '') : (turn.stdout || '');
631
+ const totalBytes = fullStdout.length;
632
+ const content = fullStdout.substring(offset, offset + limit);
633
+ result.stdout = {
634
+ content,
635
+ totalBytes,
636
+ offset,
637
+ hasMore: offset + limit < totalBytes
638
+ };
639
+ }
640
+
641
+ if (outputType === 'stderr' || outputType === 'both') {
642
+ const fullStderr = turn.cached ? (turn.stderr_full || '') : (turn.stderr || '');
643
+ const totalBytes = fullStderr.length;
644
+ const content = fullStderr.substring(offset, offset + limit);
645
+ result.stderr = {
646
+ content,
647
+ totalBytes,
648
+ offset,
649
+ hasMore: offset + limit < totalBytes
650
+ };
651
+ }
652
+
653
+ return result;
654
+ }
655
+
536
656
  /**
537
657
  * Query execution history
538
658
  */
@@ -100,6 +100,9 @@ class Config:
100
100
  # For litellm: model name from config (e.g., "qwen3-embedding")
101
101
  embedding_use_gpu: bool = True # For fastembed: whether to use GPU acceleration
102
102
 
103
+ # Indexing/search optimizations
104
+ global_symbol_index_enabled: bool = True # Enable project-wide symbol index fast path
105
+
103
106
  # Multi-endpoint configuration for litellm backend
104
107
  embedding_endpoints: List[Dict[str, Any]] = field(default_factory=list)
105
108
  # List of endpoint configs: [{"model": "...", "api_key": "...", "api_base": "...", "weight": 1.0}]
@@ -11,11 +11,14 @@ from dataclasses import dataclass, field
11
11
  from pathlib import Path
12
12
  from typing import List, Optional, Dict, Any
13
13
  import logging
14
+ import os
14
15
  import time
15
16
 
16
17
  from codexlens.entities import SearchResult, Symbol
18
+ from codexlens.config import Config
17
19
  from codexlens.storage.registry import RegistryStore, DirMapping
18
20
  from codexlens.storage.dir_index import DirIndexStore, SubdirLink
21
+ from codexlens.storage.global_index import GlobalSymbolIndex
19
22
  from codexlens.storage.path_mapper import PathMapper
20
23
  from codexlens.storage.sqlite_store import SQLiteStore
21
24
  from codexlens.search.hybrid_search import HybridSearchEngine
@@ -107,7 +110,8 @@ class ChainSearchEngine:
107
110
  def __init__(self,
108
111
  registry: RegistryStore,
109
112
  mapper: PathMapper,
110
- max_workers: int = 8):
113
+ max_workers: int = 8,
114
+ config: Config | None = None):
111
115
  """Initialize chain search engine.
112
116
 
113
117
  Args:
@@ -120,6 +124,7 @@ class ChainSearchEngine:
120
124
  self.logger = logging.getLogger(__name__)
121
125
  self._max_workers = max_workers
122
126
  self._executor: Optional[ThreadPoolExecutor] = None
127
+ self._config = config
123
128
 
124
129
  def _get_executor(self, max_workers: Optional[int] = None) -> ThreadPoolExecutor:
125
130
  """Get or create the shared thread pool executor.
@@ -294,6 +299,71 @@ class ChainSearchEngine:
294
299
  self.logger.warning(f"No index found for {source_path}")
295
300
  return []
296
301
 
302
+ # Fast path: project-wide global symbol index (avoids chain traversal).
303
+ if self._config is None or getattr(self._config, "global_symbol_index_enabled", True):
304
+ try:
305
+ # Avoid relying on index_to_source() here; use the same logic as _find_start_index
306
+ # to determine the effective search root directory.
307
+ search_root = source_path.resolve()
308
+ exact_index = self.mapper.source_to_index_db(search_root)
309
+ if not exact_index.exists():
310
+ nearest = self.registry.find_nearest_index(search_root)
311
+ if nearest:
312
+ search_root = nearest.source_path
313
+
314
+ project = self.registry.find_by_source_path(str(search_root))
315
+ if project:
316
+ global_db_path = Path(project["index_root"]) / GlobalSymbolIndex.DEFAULT_DB_NAME
317
+ if global_db_path.exists():
318
+ query_limit = max(int(options.total_limit) * 10, int(options.total_limit))
319
+ with GlobalSymbolIndex(global_db_path, project_id=int(project["id"])) as global_index:
320
+ candidates = global_index.search(name=name, kind=kind, limit=query_limit)
321
+
322
+ # Apply depth constraint relative to the start index directory.
323
+ filtered: List[Symbol] = []
324
+ for sym in candidates:
325
+ if not sym.file:
326
+ continue
327
+ try:
328
+ root_str = str(search_root)
329
+ file_dir_str = str(Path(sym.file).parent)
330
+
331
+ # Normalize Windows long-path prefix (\\?\) if present.
332
+ if root_str.startswith("\\\\?\\"):
333
+ root_str = root_str[4:]
334
+ if file_dir_str.startswith("\\\\?\\"):
335
+ file_dir_str = file_dir_str[4:]
336
+
337
+ root_cmp = root_str.lower().rstrip("\\/")
338
+ dir_cmp = file_dir_str.lower().rstrip("\\/")
339
+
340
+ if os.path.commonpath([root_cmp, dir_cmp]) != root_cmp:
341
+ continue
342
+
343
+ rel = os.path.relpath(dir_cmp, root_cmp)
344
+ rel_depth = 0 if rel == "." else len(rel.split(os.sep))
345
+ except Exception:
346
+ continue
347
+
348
+ if options.depth >= 0 and rel_depth > options.depth:
349
+ continue
350
+ filtered.append(sym)
351
+
352
+ if filtered:
353
+ # Match existing semantics: dedupe by (name, kind, range), sort by name.
354
+ seen = set()
355
+ unique_symbols: List[Symbol] = []
356
+ for sym in filtered:
357
+ key = (sym.name, sym.kind, sym.range)
358
+ if key in seen:
359
+ continue
360
+ seen.add(key)
361
+ unique_symbols.append(sym)
362
+ unique_symbols.sort(key=lambda s: s.name)
363
+ return unique_symbols[: options.total_limit]
364
+ except Exception as exc:
365
+ self.logger.debug("Global symbol index fast path failed: %s", exc)
366
+
297
367
  index_paths = self._collect_index_paths(start_index, options.depth)
298
368
  if not index_paths:
299
369
  return []