claude-code-tools 1.4.1__py3-none-any.whl → 1.4.6__py3-none-any.whl

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.
@@ -1,3 +1,3 @@
1
1
  """Claude Code Tools - Collection of utilities for Claude Code."""
2
2
 
3
- __version__ = "1.4.1"
3
+ __version__ = "1.4.6"
@@ -2,11 +2,39 @@
2
2
 
3
3
  import json
4
4
  import os
5
+ import re
5
6
  from datetime import datetime
6
7
  from io import StringIO
7
8
  from pathlib import Path
8
9
  from typing import Any, Optional
9
10
 
11
+ # Known system-injected XML tags that appear at the start of messages.
12
+ # Using a whitelist of specific tags avoids filtering legitimate user
13
+ # messages that start with HTML/XML like <div> or <svg>.
14
+ NON_GENUINE_XML_TAGS = {
15
+ # Claude system tags (local command execution)
16
+ "command-name",
17
+ "command-message",
18
+ "command-args",
19
+ "local-command-stdout",
20
+ "bash-input",
21
+ "bash-stdout",
22
+ "bash-stderr",
23
+ "bash-notification",
24
+ # Codex system tags (environment/context injection)
25
+ "environment_context",
26
+ "user_instructions",
27
+ "user_shell_command",
28
+ }
29
+
30
+ # Regex patterns for non-genuine user messages (system-injected content).
31
+ # Messages matching any of these patterns are filtered out when finding
32
+ # the first real user message. Used for both Claude and Codex sessions.
33
+ NON_GENUINE_MSG_PATTERNS = [
34
+ re.compile(r"^Caveat:", re.IGNORECASE), # Caveat warnings about local commands
35
+ re.compile(r"^\s*\[SESSION LINEAGE\]", re.IGNORECASE), # Session continuation context
36
+ ]
37
+
10
38
  # Lazy import yaml to allow module to load even if not installed
11
39
  try:
12
40
  import yaml
@@ -139,22 +167,60 @@ def _extract_codex_message_text(data: dict) -> Optional[str]:
139
167
  return None
140
168
 
141
169
 
170
+ def _is_meta_user_message(data: dict, text: str) -> bool:
171
+ """
172
+ Check if a user message is a meta/system-injected message.
173
+
174
+ These include local command injections that Claude Code records
175
+ in the session file but aren't actual user queries.
176
+
177
+ Args:
178
+ data: The parsed JSON data for the message
179
+ text: The extracted text content
180
+
181
+ Returns:
182
+ True if this is a meta message that should be skipped
183
+ """
184
+ # Check isMeta flag
185
+ if data.get("isMeta") is True:
186
+ return True
187
+
188
+ # Check against regex patterns (Caveat, SESSION LINEAGE, etc.)
189
+ for pattern in NON_GENUINE_MSG_PATTERNS:
190
+ if pattern.search(text):
191
+ return True
192
+
193
+ # Check if message starts with a known system-injected XML tag
194
+ text_stripped = text.strip()
195
+ match = re.match(r"^<([a-z][a-z0-9_-]*)>", text_stripped)
196
+ if match and match.group(1) in NON_GENUINE_XML_TAGS:
197
+ return True
198
+
199
+ return False
200
+
201
+
142
202
  def extract_first_last_messages(
143
203
  session_file: Path, agent: str
144
- ) -> tuple[Optional[dict[str, str]], Optional[dict[str, str]]]:
204
+ ) -> tuple[
205
+ Optional[dict[str, str]],
206
+ Optional[dict[str, str]],
207
+ Optional[dict[str, str]],
208
+ ]:
145
209
  """
146
- Extract first and last user/assistant messages from a session.
210
+ Extract first/last messages and the first real user message from a session.
147
211
 
148
212
  Args:
149
213
  session_file: Path to session JSONL file
150
214
  agent: Agent type ('claude' or 'codex')
151
215
 
152
216
  Returns:
153
- Tuple of (first_msg, last_msg) where each is a dict with 'role' and
154
- 'content' keys, or None if not found
217
+ Tuple of (first_msg, last_msg, first_user_msg) where each is a dict
218
+ with 'role' and 'content' keys, or None if not found.
219
+ first_user_msg skips meta messages (local command injections).
155
220
  """
156
221
  first_msg: Optional[dict[str, str]] = None
157
222
  last_msg: Optional[dict[str, str]] = None
223
+ first_user_msg: Optional[dict[str, str]] = None
158
224
 
159
225
  try:
160
226
  with open(session_file, "r", encoding="utf-8") as f:
@@ -190,13 +256,20 @@ def extract_first_last_messages(
190
256
  }
191
257
  if first_msg is None:
192
258
  first_msg = msg_dict
259
+ # Track first real user message (skip meta messages)
260
+ if (
261
+ role == "user"
262
+ and first_user_msg is None
263
+ and not _is_meta_user_message(data, text)
264
+ ):
265
+ first_user_msg = msg_dict
193
266
  # Always update last_msg to get the last one
194
267
  last_msg = msg_dict
195
268
 
196
269
  except (OSError, IOError):
197
270
  pass
198
271
 
199
- return first_msg, last_msg
272
+ return first_msg, last_msg, first_user_msg
200
273
 
201
274
 
202
275
  def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
@@ -236,6 +309,7 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
236
309
  "trim_stats": None,
237
310
  "first_msg": None,
238
311
  "last_msg": None,
312
+ "first_user_msg": None,
239
313
  }
240
314
 
241
315
  # Track session start timestamp from JSON metadata
@@ -371,9 +445,12 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
371
445
  metadata["project"] = Path(metadata["cwd"]).name
372
446
 
373
447
  # Extract first and last messages
374
- first_msg, last_msg = extract_first_last_messages(session_file, agent)
448
+ first_msg, last_msg, first_user_msg = extract_first_last_messages(
449
+ session_file, agent
450
+ )
375
451
  metadata["first_msg"] = first_msg
376
452
  metadata["last_msg"] = last_msg
453
+ metadata["first_user_msg"] = first_user_msg
377
454
 
378
455
  return metadata
379
456
 
@@ -458,6 +535,8 @@ def generate_yaml_frontmatter(metadata: dict[str, Any]) -> str:
458
535
  yaml_data["first_msg"] = metadata["first_msg"]
459
536
  if metadata.get("last_msg"):
460
537
  yaml_data["last_msg"] = metadata["last_msg"]
538
+ if metadata.get("first_user_msg"):
539
+ yaml_data["first_user_msg"] = metadata["first_user_msg"]
461
540
 
462
541
  # Trim stats (only for trimmed sessions)
463
542
  if metadata.get("trim_stats"):
@@ -10,6 +10,7 @@ from importlib.metadata import version as get_pkg_version
10
10
  from pathlib import Path
11
11
  from typing import Any, Optional
12
12
 
13
+ from claude_code_tools.export_session import _is_meta_user_message
13
14
  from claude_code_tools.session_utils import is_valid_session
14
15
 
15
16
 
@@ -177,6 +178,7 @@ class SessionIndex:
177
178
  self.schema_builder.add_text_field("first_msg_content", stored=True)
178
179
  self.schema_builder.add_text_field("last_msg_role", stored=True)
179
180
  self.schema_builder.add_text_field("last_msg_content", stored=True)
181
+ self.schema_builder.add_text_field("first_user_msg_content", stored=True)
180
182
 
181
183
  # Session type fields (for filtering in TUI)
182
184
  self.schema_builder.add_text_field("derivation_type", stored=True)
@@ -321,10 +323,12 @@ class SessionIndex:
321
323
  # First and last message fields
322
324
  first_msg = metadata.get("first_msg", {}) or {}
323
325
  last_msg = metadata.get("last_msg", {}) or {}
326
+ first_user_msg = metadata.get("first_user_msg", {}) or {}
324
327
  doc.add_text("first_msg_role", first_msg.get("role", ""))
325
328
  doc.add_text("first_msg_content", first_msg.get("content", ""))
326
329
  doc.add_text("last_msg_role", last_msg.get("role", ""))
327
330
  doc.add_text("last_msg_content", last_msg.get("content", ""))
331
+ doc.add_text("first_user_msg_content", first_user_msg.get("content", ""))
328
332
 
329
333
  # Session type fields
330
334
  doc.add_text("derivation_type", metadata.get("derivation_type", "") or "")
@@ -405,10 +409,12 @@ class SessionIndex:
405
409
  # First and last message fields
406
410
  first_msg = metadata.get("first_msg", {}) or {}
407
411
  last_msg = metadata.get("last_msg", {}) or {}
412
+ first_user_msg = metadata.get("first_user_msg", {}) or {}
408
413
  doc.add_text("first_msg_role", first_msg.get("role", ""))
409
414
  doc.add_text("first_msg_content", first_msg.get("content", ""))
410
415
  doc.add_text("last_msg_role", last_msg.get("role", ""))
411
416
  doc.add_text("last_msg_content", last_msg.get("content", ""))
417
+ doc.add_text("first_user_msg_content", first_user_msg.get("content", ""))
412
418
 
413
419
  # Session type fields
414
420
  doc.add_text(
@@ -525,9 +531,10 @@ class SessionIndex:
525
531
  message = data.get("message", {})
526
532
  content = message.get("content")
527
533
 
528
- # Count user messages, but exclude tool results
529
- # Tool results have content as list with {"type": "tool_result"}
530
- # Real user messages have content as string or list with text
534
+ # Count user messages, but exclude:
535
+ # - Tool results (content is list with {"type": "tool_result"})
536
+ # - Meta messages (isMeta: true, e.g., "Caveat:" warnings)
537
+ # - Local command messages (<command-name>, <local-command-stdout>)
531
538
  if role == "user":
532
539
  is_tool_result = (
533
540
  isinstance(content, list)
@@ -535,7 +542,12 @@ class SessionIndex:
535
542
  and isinstance(content[0], dict)
536
543
  and content[0].get("type") == "tool_result"
537
544
  )
538
- if not is_tool_result:
545
+ # Check for meta/non-genuine messages
546
+ text_content = content if isinstance(content, str) else ""
547
+ if (
548
+ not is_tool_result
549
+ and not _is_meta_user_message(data, text_content)
550
+ ):
539
551
  user_count += 1
540
552
 
541
553
  if not content:
@@ -575,13 +587,23 @@ class SessionIndex:
575
587
  continue
576
588
 
577
589
  role = payload.get("role")
578
- if role == "user":
579
- user_count += 1
580
590
  content = payload.get("content", [])
581
591
 
582
592
  if not isinstance(content, list):
583
593
  continue
584
594
 
595
+ # Extract text first to check for meta patterns
596
+ codex_text = ""
597
+ for block in content:
598
+ if isinstance(block, dict):
599
+ if block.get("type") in ("input_text", "output_text"):
600
+ codex_text += block.get("text", "")
601
+
602
+ # Count user messages, but exclude non-genuine messages
603
+ if role == "user":
604
+ if not _is_meta_user_message({}, codex_text):
605
+ user_count += 1
606
+
585
607
  for block in content:
586
608
  if not isinstance(block, dict):
587
609
  continue
@@ -635,6 +657,7 @@ class SessionIndex:
635
657
  # Map metadata fields to expected format
636
658
  first_msg = metadata.get("first_msg") or {"role": "", "content": ""}
637
659
  last_msg = metadata.get("last_msg") or {"role": "", "content": ""}
660
+ first_user_msg = metadata.get("first_user_msg") or {"role": "", "content": ""}
638
661
 
639
662
  # Always use filename-derived session_id (the canonical identifier)
640
663
  # Internal sessionId field can be stale in forked sessions
@@ -660,6 +683,7 @@ class SessionIndex:
660
683
  "content": content,
661
684
  "first_msg": first_msg,
662
685
  "last_msg": last_msg,
686
+ "first_user_msg": first_user_msg,
663
687
  "lines": msg_count,
664
688
  "file_path": str(jsonl_path),
665
689
  }
@@ -742,6 +766,7 @@ class SessionIndex:
742
766
  metadata = parsed["metadata"]
743
767
  first_msg = parsed["first_msg"]
744
768
  last_msg = parsed["last_msg"]
769
+ first_user_msg = parsed.get("first_user_msg", {}) or {}
745
770
 
746
771
  # Skip helper sessions (SDK/headless sessions used for analysis)
747
772
  if metadata.get("session_type") == "helper":
@@ -785,6 +810,7 @@ class SessionIndex:
785
810
  doc.add_text("first_msg_content", first_msg.get("content", ""))
786
811
  doc.add_text("last_msg_role", last_msg.get("role", ""))
787
812
  doc.add_text("last_msg_content", last_msg.get("content", ""))
813
+ doc.add_text("first_user_msg_content", first_user_msg.get("content", ""))
788
814
 
789
815
  # Session type fields
790
816
  doc.add_text(
@@ -1007,6 +1033,7 @@ class SessionIndex:
1007
1033
  first_msg_content = doc.get_first("first_msg_content") or ""
1008
1034
  last_msg_role = doc.get_first("last_msg_role") or ""
1009
1035
  last_msg_content = doc.get_first("last_msg_content") or ""
1036
+ first_user_msg_content = doc.get_first("first_user_msg_content") or ""
1010
1037
 
1011
1038
  results.append({
1012
1039
  "session_id": session_id,
@@ -1024,6 +1051,7 @@ class SessionIndex:
1024
1051
  "first_msg_content": first_msg_content,
1025
1052
  "last_msg_role": last_msg_role,
1026
1053
  "last_msg_content": last_msg_content,
1054
+ "first_user_msg_content": first_user_msg_content,
1027
1055
  })
1028
1056
 
1029
1057
  if len(results) >= limit:
@@ -1087,6 +1115,7 @@ class SessionIndex:
1087
1115
  "first_msg_content": doc.get_first("first_msg_content") or "",
1088
1116
  "last_msg_role": doc.get_first("last_msg_role") or "",
1089
1117
  "last_msg_content": doc.get_first("last_msg_content") or "",
1118
+ "first_user_msg_content": doc.get_first("first_user_msg_content") or "",
1090
1119
  "claude_home": doc.get_first("claude_home") or "",
1091
1120
  })
1092
1121
 
@@ -1186,6 +1215,7 @@ class SessionIndex:
1186
1215
  "first_msg_content": doc.get_first("first_msg_content") or "",
1187
1216
  "last_msg_role": doc.get_first("last_msg_role") or "",
1188
1217
  "last_msg_content": doc.get_first("last_msg_content") or "",
1218
+ "first_user_msg_content": doc.get_first("first_user_msg_content") or "",
1189
1219
  "derivation_type": doc.get_first("derivation_type") or "",
1190
1220
  "is_sidechain": doc.get_first("is_sidechain") or "false",
1191
1221
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-tools
3
- Version: 1.4.1
3
+ Version: 1.4.6
4
4
  Summary: Collection of tools for working with Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -26,9 +26,8 @@ Description-Content-Type: text/markdown
26
26
  [![claude-code-tools](https://img.shields.io/github/v/release/pchalasani/claude-code-tools?filter=v*&label=claude-code-tools&color=blue)](https://pypi.org/project/claude-code-tools/)
27
27
  [![aichat-search](https://img.shields.io/github/v/release/pchalasani/claude-code-tools?filter=rust-v*&label=aichat-search&color=orange)](https://github.com/pchalasani/claude-code-tools/releases?q=rust)
28
28
 
29
- Collection of tools I built for productivity with
30
- Claude Code, Codex-CLI, and similar CLI coding agents:
31
- CLI commands, skills, agents, hooks, plugins.
29
+ Productivity tools for Claude Code, Codex-CLI, and similar CLI coding agents:
30
+ CLI commands, skills, agents, hooks, plugins. Click on a card below to navigate.
32
31
 
33
32
 
34
33
  <div align="center">
@@ -1,4 +1,4 @@
1
- claude_code_tools/__init__.py,sha256=eZWXonter7xAmVmfsqiT653lgnwVI1MifxxGlirbR0A,89
1
+ claude_code_tools/__init__.py,sha256=lJ_ty7aXHz-OaAsz_ikznt_x0M6lgCfbFZqflfmM_-U,89
2
2
  claude_code_tools/action_rpc.py,sha256=6NfWUjt22qqkDKq7ftNH2V9B8VSQycbFx_jDA0UrlJQ,17016
3
3
  claude_code_tools/aichat.py,sha256=s8pfRTmXR55J4yNElJdztXnfjFifaJbjrBtVo6NUe-s,95346
4
4
  claude_code_tools/claude_continue.py,sha256=GwgKGbTpj4ES615yCadjz0Q6wOb69T76rf_-wPnHix8,11727
@@ -10,7 +10,7 @@ claude_code_tools/env_safe.py,sha256=TSSkOjEpzBwNgbeSR-0tR1-pAW_qmbZNmn3fiAsHJ4w
10
10
  claude_code_tools/export_all.py,sha256=GOWj_5IZrrngeRUsDxbE48cOOZIxo7drZJWZh9QiuHg,9848
11
11
  claude_code_tools/export_claude_session.py,sha256=rEJLMcaCMuWbWxs1rfd0LuT6gSmjEsej6nueGrH1ujo,16256
12
12
  claude_code_tools/export_codex_session.py,sha256=V2deRcI6FMCEWYAEvvL74XXuW798B1esgTs6PH3_-7E,15650
13
- claude_code_tools/export_session.py,sha256=6GdZ6aVM3gPbQwWr6wg8w2TfbGAWDpCElY2_1EBSBlA,19141
13
+ claude_code_tools/export_session.py,sha256=I2ncN3lbbrfc8M-3URQVimyM2fAbcu4BXITtCdNfL6E,21860
14
14
  claude_code_tools/find_claude_session.py,sha256=QRv6u4T5X9c9QLj-1X8-uYj3wul5YsbFI5LgUUTFMW0,70559
15
15
  claude_code_tools/find_codex_session.py,sha256=dpZVek3cJ-se4JMwzGEDkZ50_XvtK6dfP36mo8KDHnI,48177
16
16
  claude_code_tools/find_original_session.py,sha256=JlHeati0X1KkPkmz4knvdfCqRHjuJRLfRRcn3ZsuG8o,4120
@@ -19,7 +19,7 @@ claude_code_tools/find_trimmed_sessions.py,sha256=JvMSetHD4DgXzKDFaZlAndBT_dYaw_
19
19
  claude_code_tools/gdoc2md.py,sha256=J83CZJomHquOBIl15fISqtDyGsmkqqMuRY-nN7-7K1I,6346
20
20
  claude_code_tools/md2gdoc.py,sha256=sA6gU2QsWanJpAwfSC6HnSPQuSywv0xUopXSvbRUX_o,17945
21
21
  claude_code_tools/node_menu_ui.py,sha256=CQ6PxxNQ5jbLRLYESJ-klLSxSIIuLegU8s-Sj5yRl8Q,12621
22
- claude_code_tools/search_index.py,sha256=NHEV9W1nR5SUqAZOaBj2fS5Ld7mhhyPJ0elQ3b_ns_g,48562
22
+ claude_code_tools/search_index.py,sha256=_ORSD2E6PF-Gjtzrnvp03KyfGueO5FA3WCzTbg7n208,50557
23
23
  claude_code_tools/session_lineage.py,sha256=BYKpAolPGLJUv97-xMXvNFMzgauUVNAsRx8Shw0X_hk,8430
24
24
  claude_code_tools/session_menu.py,sha256=5M1AlqhmCWly3r3P1u-GhxWB0_rbGKsKSlIPEgTaN9w,6095
25
25
  claude_code_tools/session_menu_cli.py,sha256=SnCdm1xyJQAC0ogZ5-PRc8SkAZVKHXYu6mtc0Lp_las,15426
@@ -36,6 +36,7 @@ docs/claude-code-chutes.md,sha256=jCnYAAHZm32NGHE0CzGGl3vpO_zlF_xdmr23YxuCjPg,80
36
36
  docs/claude-code-tmux-tutorials.md,sha256=S-9U3a1AaPEBPo3oKpWuyOfKK7yPFOIu21P_LDfGUJk,7558
37
37
  docs/dot-zshrc.md,sha256=DC2fOiGrUlIzol6N_47CW53a4BsnMEvCnhlRRVxFCTc,7160
38
38
  docs/find-claude-session.md,sha256=fACbQP0Bj5jqIpNWk0lGDOQQaji-K9Va3gUv2RA47VQ,4284
39
+ docs/linked-in-20260102.md,sha256=wCihbQGGqS-GpQ7z9-q6UObiJBJ8_VfbUufXTvqB6hY,1159
39
40
  docs/lmsh.md,sha256=Kf5tKt1lh7eDV-B6mrMi2hsjUMZv1EGfkrsNS29HYBA,2226
40
41
  docs/local-llm-setup.md,sha256=JnMF4m1e0s8DZxfB-8S3Y20W74KBMm2RXwBjTK0o27U,7596
41
42
  docs/reddit-aichat-resume-v2.md,sha256=Rpq4E-tMDpgjWiSfb-jS50AeUxgdnOJIwDHs7rdLTZw,2980
@@ -1806,8 +1807,8 @@ node_ui/node_modules/yoga-wasm-web/dist/wrapAsm-f766f97f.js,sha256=-82_XGQhP7kkD
1806
1807
  node_ui/node_modules/yoga-wasm-web/dist/wrapAsm.d.ts,sha256=2l7bSIMruV8KTC2I4XKJBDQx8nsgwVR43q9rvkClpUE,4877
1807
1808
  node_ui/node_modules/yoga-wasm-web/dist/yoga.wasm,sha256=R_tPgdJ0kyGEzRnHXtNPkC0T8FGTAVkHiaN_cHeXfic,88658
1808
1809
  node_ui/node_modules/yoga-wasm-web/dist/generated/YGEnums.d.ts,sha256=kE3_7yS8iqNd5sMfXtD9B3Tq_JcJkVOQkdwxhch1pI4,8893
1809
- claude_code_tools-1.4.1.dist-info/METADATA,sha256=5pqcjxo3yEyrSmCe4R0bylwzf8w-OWO_NmVFR6hL4jQ,42990
1810
- claude_code_tools-1.4.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
1811
- claude_code_tools-1.4.1.dist-info/entry_points.txt,sha256=-hVowB6m8tgqV_dCyzCLbt7vthEDiBxodGMqMvD4F2M,280
1812
- claude_code_tools-1.4.1.dist-info/licenses/LICENSE,sha256=BBQdOBLdFB3CEPmb3pqxeOThaFCIdsiLzmDANsCHhoM,1073
1813
- claude_code_tools-1.4.1.dist-info/RECORD,,
1810
+ claude_code_tools-1.4.6.dist-info/METADATA,sha256=O2zrPX_UYG6uRdKknR5S0P9ACZJEvOlzyxaCuy89_NU,42998
1811
+ claude_code_tools-1.4.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
1812
+ claude_code_tools-1.4.6.dist-info/entry_points.txt,sha256=-hVowB6m8tgqV_dCyzCLbt7vthEDiBxodGMqMvD4F2M,280
1813
+ claude_code_tools-1.4.6.dist-info/licenses/LICENSE,sha256=BBQdOBLdFB3CEPmb3pqxeOThaFCIdsiLzmDANsCHhoM,1073
1814
+ claude_code_tools-1.4.6.dist-info/RECORD,,
@@ -0,0 +1,32 @@
1
+ The top pain for users of Claude Code and similar CLI agents is...
2
+
3
+ Sesesion Continuity: What do you do when you've filled your context window?
4
+
5
+ Compaction? you lose valuable detail that you have to explain all over again.
6
+
7
+ Here's what I do instead, to recover the precise, full context I need, to continue my work:
8
+
9
+ In my Claude Code session, I type ">resume" -- This triggers a hook that copies the current session ID to the clipboard.
10
+
11
+ Then I run:
12
+
13
+ aichat resume <paste-session-id>
14
+
15
+ This launches a TUI that shows a few ways to continue my work: I select
16
+ the "rollover" option: it creates a new session and injects the session log file
17
+ path into the first user message.
18
+
19
+ Then I prompt it to retrieve the exact context I need, or use a slash command /aichat:recover-context
20
+
21
+ This works with Codex-CLI as well, and you can even do cross-agent handoff: start in
22
+ Claude-Code, continue with Codex-CLI or vice versa.
23
+
24
+ The aichat command is one of several productivity tools in my claude-code-tools repo:
25
+ If you'd like to try them out, see the repo for instructions on how to install the suite of tools
26
+
27
+ https://github.com/pchalasani/claude-code-tools
28
+
29
+
30
+
31
+
32
+