hcom 0.4.2.post3__py3-none-any.whl → 0.6.0__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.
Potentially problematic release.
This version of hcom might be problematic. Click here for more details.
- hcom/__init__.py +2 -2
- hcom/__main__.py +3 -3743
- hcom/cli.py +4613 -0
- hcom/shared.py +1036 -0
- hcom/ui.py +2965 -0
- hcom-0.6.0.dist-info/METADATA +269 -0
- hcom-0.6.0.dist-info/RECORD +10 -0
- hcom-0.4.2.post3.dist-info/METADATA +0 -452
- hcom-0.4.2.post3.dist-info/RECORD +0 -7
- {hcom-0.4.2.post3.dist-info → hcom-0.6.0.dist-info}/WHEEL +0 -0
- {hcom-0.4.2.post3.dist-info → hcom-0.6.0.dist-info}/entry_points.txt +0 -0
- {hcom-0.4.2.post3.dist-info → hcom-0.6.0.dist-info}/top_level.txt +0 -0
hcom/shared.py
ADDED
|
@@ -0,0 +1,1036 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared constants and utilities for hcom"""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
__version__ = "0.6.0"
|
|
9
|
+
|
|
10
|
+
# ===== Core ANSI Codes =====
|
|
11
|
+
RESET = "\033[0m"
|
|
12
|
+
DIM = "\033[2m"
|
|
13
|
+
BOLD = "\033[1m"
|
|
14
|
+
REVERSE = "\033[7m"
|
|
15
|
+
|
|
16
|
+
# Foreground colors
|
|
17
|
+
FG_GREEN = "\033[32m"
|
|
18
|
+
FG_CYAN = "\033[36m"
|
|
19
|
+
FG_WHITE = "\033[37m"
|
|
20
|
+
FG_BLACK = "\033[30m"
|
|
21
|
+
FG_GRAY = '\033[90m'
|
|
22
|
+
FG_YELLOW = '\033[33m'
|
|
23
|
+
FG_RED = '\033[31m'
|
|
24
|
+
FG_BLUE = '\033[34m'
|
|
25
|
+
|
|
26
|
+
# TUI-specific foreground
|
|
27
|
+
FG_ORANGE = '\033[38;5;208m'
|
|
28
|
+
FG_GOLD = '\033[38;5;220m'
|
|
29
|
+
FG_LIGHTGRAY = '\033[38;5;250m'
|
|
30
|
+
|
|
31
|
+
# Stale instance color (brownish-grey, distinct from exited)
|
|
32
|
+
FG_STALE = '\033[38;5;137m' # Tan/brownish-grey
|
|
33
|
+
|
|
34
|
+
# Background colors
|
|
35
|
+
BG_BLUE = "\033[44m"
|
|
36
|
+
BG_GREEN = "\033[42m"
|
|
37
|
+
BG_CYAN = "\033[46m"
|
|
38
|
+
BG_YELLOW = "\033[43m"
|
|
39
|
+
BG_RED = "\033[41m"
|
|
40
|
+
BG_GRAY = "\033[100m"
|
|
41
|
+
|
|
42
|
+
# Stale background (brownish-grey to match foreground)
|
|
43
|
+
BG_STALE = '\033[48;5;137m' # Tan/brownish-grey background
|
|
44
|
+
|
|
45
|
+
# TUI-specific background
|
|
46
|
+
BG_ORANGE = '\033[48;5;208m'
|
|
47
|
+
BG_CHARCOAL = '\033[48;5;236m'
|
|
48
|
+
|
|
49
|
+
# Terminal control
|
|
50
|
+
CLEAR_SCREEN = '\033[2J'
|
|
51
|
+
CURSOR_HOME = '\033[H'
|
|
52
|
+
HIDE_CURSOR = '\033[?25l'
|
|
53
|
+
SHOW_CURSOR = '\033[?25h'
|
|
54
|
+
|
|
55
|
+
# Box drawing
|
|
56
|
+
BOX_H = '─'
|
|
57
|
+
|
|
58
|
+
# ===== Default Config =====
|
|
59
|
+
DEFAULT_CONFIG_HEADER = [
|
|
60
|
+
"# HCOM Configuration",
|
|
61
|
+
"#",
|
|
62
|
+
"# All HCOM_* settings (and any env var ie. Claude Code settings)",
|
|
63
|
+
"# can be set here or via environment variables.",
|
|
64
|
+
"# Environment variables and cli args override config file values.",
|
|
65
|
+
"# Put each value on separate lines without comments.",
|
|
66
|
+
"#",
|
|
67
|
+
"# HCOM settings:",
|
|
68
|
+
"# HCOM_TIMEOUT - seconds before disconnecting idle instance (default: 1800)",
|
|
69
|
+
"# HCOM_SUBAGENT_TIMEOUT - seconds before disconnecting idle subagents (default: 30)",
|
|
70
|
+
"# HCOM_TERMINAL - Terminal mode: \"new\", \"here\", or custom command with {script}",
|
|
71
|
+
"# HCOM_HINTS - Text appended to all messages received by instances",
|
|
72
|
+
"# HCOM_TAG - Group tag for instances (creates tag-* instances)",
|
|
73
|
+
"# HCOM_AGENT - Claude code subagent from .claude/agents/, comma-separated for multiple",
|
|
74
|
+
"# HCOM_CLAUDE_ARGS - Default Claude args (e.g., '-p --model sonnet-4')",
|
|
75
|
+
"#",
|
|
76
|
+
"#",
|
|
77
|
+
"ANTHROPIC_MODEL=",
|
|
78
|
+
"CLAUDE_CODE_SUBAGENT_MODEL=",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
DEFAULT_CONFIG_DEFAULTS = [
|
|
82
|
+
'HCOM_AGENT=',
|
|
83
|
+
'HCOM_TAG=',
|
|
84
|
+
'HCOM_HINTS=',
|
|
85
|
+
'HCOM_TIMEOUT=1800',
|
|
86
|
+
'HCOM_SUBAGENT_TIMEOUT=30',
|
|
87
|
+
'HCOM_TERMINAL=new',
|
|
88
|
+
r'''HCOM_CLAUDE_ARGS="'say hi in hcom chat'"''',
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
# ===== Status Configuration =====
|
|
92
|
+
# Status values stored directly in instance files (no event mapping)
|
|
93
|
+
# 'enabled' field is separate from status (participation vs activity)
|
|
94
|
+
|
|
95
|
+
# Valid status values
|
|
96
|
+
STATUS_VALUES = ['active', 'delivered', 'waiting', 'blocked', 'exited', 'stale', 'unknown']
|
|
97
|
+
|
|
98
|
+
# Status icons
|
|
99
|
+
STATUS_ICONS = {
|
|
100
|
+
'active': '▶',
|
|
101
|
+
'delivered': '▷',
|
|
102
|
+
'waiting': '◉',
|
|
103
|
+
'blocked': '■',
|
|
104
|
+
'exited': '○',
|
|
105
|
+
'stale': '⊙',
|
|
106
|
+
'unknown': '◦'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Status colors (foreground)
|
|
110
|
+
STATUS_COLORS = {
|
|
111
|
+
'active': FG_GREEN,
|
|
112
|
+
'delivered': FG_CYAN,
|
|
113
|
+
'waiting': FG_BLUE,
|
|
114
|
+
'blocked': FG_RED,
|
|
115
|
+
'exited': FG_GRAY,
|
|
116
|
+
'stale': FG_STALE,
|
|
117
|
+
'unknown': FG_GRAY
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# STATUS_MAP for watch command (foreground color, icon)
|
|
121
|
+
STATUS_MAP = {
|
|
122
|
+
status: (STATUS_COLORS[status], STATUS_ICONS[status])
|
|
123
|
+
for status in STATUS_VALUES
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Background colors for statusline display blocks
|
|
127
|
+
STATUS_BG_COLORS = {
|
|
128
|
+
'active': BG_GREEN,
|
|
129
|
+
'delivered': BG_CYAN,
|
|
130
|
+
'waiting': BG_BLUE,
|
|
131
|
+
'blocked': BG_RED,
|
|
132
|
+
'exited': BG_GRAY,
|
|
133
|
+
'stale': BG_STALE,
|
|
134
|
+
'unknown': BG_GRAY
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Background color map for TUI statusline (background color, icon)
|
|
138
|
+
STATUS_BG_MAP = {
|
|
139
|
+
status: (STATUS_BG_COLORS[status], STATUS_ICONS[status])
|
|
140
|
+
for status in STATUS_VALUES
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Display order (priority-based sorting)
|
|
144
|
+
STATUS_ORDER = [
|
|
145
|
+
"active", "delivered", "waiting",
|
|
146
|
+
"blocked", "stale", "exited", "unknown"
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
# TUI-specific (alias for STATUS_COLORS)
|
|
150
|
+
STATUS_FG = STATUS_COLORS
|
|
151
|
+
|
|
152
|
+
# ===== Pure Utility Functions =====
|
|
153
|
+
def format_timestamp(iso_str: str, fmt: str = '%H:%M') -> str:
|
|
154
|
+
"""Format ISO timestamp for display - pure function"""
|
|
155
|
+
from datetime import datetime
|
|
156
|
+
try:
|
|
157
|
+
if 'T' in iso_str:
|
|
158
|
+
dt = datetime.fromisoformat(iso_str.replace('Z', '+00:00'))
|
|
159
|
+
return dt.strftime(fmt)
|
|
160
|
+
return iso_str
|
|
161
|
+
except Exception:
|
|
162
|
+
return iso_str[:5] if len(iso_str) >= 5 else iso_str
|
|
163
|
+
|
|
164
|
+
def format_age(seconds: float) -> str:
|
|
165
|
+
"""Format time ago in human readable form - pure function"""
|
|
166
|
+
if seconds < 60:
|
|
167
|
+
return f"{int(seconds)}s"
|
|
168
|
+
elif seconds < 3600:
|
|
169
|
+
return f"{int(seconds/60)}m"
|
|
170
|
+
else:
|
|
171
|
+
return f"{int(seconds/3600)}h"
|
|
172
|
+
|
|
173
|
+
def get_status_counts(instances: dict[str, dict]) -> dict[str, int]:
|
|
174
|
+
"""Count instances by status type - pure data transformation"""
|
|
175
|
+
counts = {s: 0 for s in STATUS_ORDER}
|
|
176
|
+
for info in instances.values():
|
|
177
|
+
status = info.get('status', 'unknown')
|
|
178
|
+
counts[status] = counts.get(status, 0) + 1
|
|
179
|
+
return counts
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ===== Config Parsing Utilities =====
|
|
183
|
+
def parse_env_value(value: str) -> str:
|
|
184
|
+
"""Parse ENV file value with proper quote and escape handling"""
|
|
185
|
+
value = value.strip()
|
|
186
|
+
|
|
187
|
+
if not value:
|
|
188
|
+
return value
|
|
189
|
+
|
|
190
|
+
if value.startswith('"') and value.endswith('"') and len(value) >= 2:
|
|
191
|
+
inner = value[1:-1]
|
|
192
|
+
inner = inner.replace('\\\\', '\x00')
|
|
193
|
+
inner = inner.replace('\\n', '\n')
|
|
194
|
+
inner = inner.replace('\\t', '\t')
|
|
195
|
+
inner = inner.replace('\\"', '"')
|
|
196
|
+
inner = inner.replace('\x00', '\\')
|
|
197
|
+
return inner
|
|
198
|
+
|
|
199
|
+
if value.startswith("'") and value.endswith("'") and len(value) >= 2:
|
|
200
|
+
return value[1:-1]
|
|
201
|
+
|
|
202
|
+
return value
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def format_env_value(value: str) -> str:
|
|
206
|
+
"""Format value for ENV file with proper quoting (inverse of parse_env_value)"""
|
|
207
|
+
if not value:
|
|
208
|
+
return value
|
|
209
|
+
|
|
210
|
+
if "'" in value:
|
|
211
|
+
escaped = value.replace('\\', '\\\\').replace('"', '\\"')
|
|
212
|
+
return f'"{escaped}"'
|
|
213
|
+
|
|
214
|
+
return value
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def parse_env_file(config_path: Path) -> dict[str, str]:
|
|
218
|
+
"""Parse ENV file (KEY=VALUE format) with security validation"""
|
|
219
|
+
config: dict[str, str] = {}
|
|
220
|
+
|
|
221
|
+
dangerous_chars = ['`', '$', ';', '|', '&', '\n', '\r']
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
content = config_path.read_text(encoding='utf-8')
|
|
225
|
+
for line in content.splitlines():
|
|
226
|
+
line = line.strip()
|
|
227
|
+
if not line or line.startswith('#'):
|
|
228
|
+
continue
|
|
229
|
+
if '=' in line:
|
|
230
|
+
key, _, value = line.partition('=')
|
|
231
|
+
key = key.strip()
|
|
232
|
+
value = value.strip()
|
|
233
|
+
|
|
234
|
+
if key == 'HCOM_TERMINAL':
|
|
235
|
+
if any(c in value for c in dangerous_chars):
|
|
236
|
+
print(
|
|
237
|
+
f"Warning: Unsafe characters in HCOM_TERMINAL "
|
|
238
|
+
f"({', '.join(repr(c) for c in dangerous_chars if c in value)}), "
|
|
239
|
+
f"ignoring custom terminal command",
|
|
240
|
+
file=sys.stderr
|
|
241
|
+
)
|
|
242
|
+
continue
|
|
243
|
+
if value not in ('new', 'here', 'print') and '{script}' not in value:
|
|
244
|
+
print(
|
|
245
|
+
"Warning: HCOM_TERMINAL custom command must include {script} placeholder, "
|
|
246
|
+
"ignoring",
|
|
247
|
+
file=sys.stderr
|
|
248
|
+
)
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
parsed = parse_env_value(value)
|
|
252
|
+
if key:
|
|
253
|
+
config[key] = parsed
|
|
254
|
+
except (FileNotFoundError, PermissionError, UnicodeDecodeError):
|
|
255
|
+
pass
|
|
256
|
+
return config
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# =====================CLAUDE ARGS============================
|
|
263
|
+
# Helpers for parsing and composing Claude CLI arguments.
|
|
264
|
+
# Used by hcom.py and ui.py for consistent arg parsing and launch command building.
|
|
265
|
+
|
|
266
|
+
import shlex
|
|
267
|
+
from dataclasses import dataclass
|
|
268
|
+
from typing import Iterable, Literal, Mapping, Sequence, Tuple
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
CanonicalFlag = Literal["--model", "--allowedTools"]
|
|
272
|
+
|
|
273
|
+
# All flag keys stored in lowercase (aside from short-form switches) for comparisons;
|
|
274
|
+
# values use canonical casing when recorded.
|
|
275
|
+
_FLAG_ALIASES: Mapping[str, CanonicalFlag] = {
|
|
276
|
+
"--model": "--model",
|
|
277
|
+
"--allowedtools": "--allowedTools",
|
|
278
|
+
"--allowed-tools": "--allowedTools",
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
_CANONICAL_PREFIXES: Mapping[str, CanonicalFlag] = {
|
|
282
|
+
"--model=": "--model",
|
|
283
|
+
"--allowedtools=": "--allowedTools",
|
|
284
|
+
"--allowed-tools=": "--allowedTools",
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
_BACKGROUND_SWITCHES = {"-p", "--print"}
|
|
288
|
+
_SYSTEM_FLAGS = {"--system-prompt", "--append-system-prompt"}
|
|
289
|
+
_BOOLEAN_FLAGS = {
|
|
290
|
+
"--verbose",
|
|
291
|
+
"--continue", "-c",
|
|
292
|
+
"--dangerously-skip-permissions",
|
|
293
|
+
"--include-partial-messages",
|
|
294
|
+
"--allow-dangerously-skip-permissions",
|
|
295
|
+
"--replay-user-messages",
|
|
296
|
+
"--mcp-debug",
|
|
297
|
+
"--fork-session",
|
|
298
|
+
"--ide",
|
|
299
|
+
"--strict-mcp-config",
|
|
300
|
+
"-v", "--version",
|
|
301
|
+
"-h", "--help",
|
|
302
|
+
}
|
|
303
|
+
_SYSTEM_PREFIXES: Mapping[str, str] = {
|
|
304
|
+
"--system-prompt=": "--system-prompt",
|
|
305
|
+
"--append-system-prompt=": "--append-system-prompt",
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Flags with optional values (lowercase).
|
|
309
|
+
_OPTIONAL_VALUE_FLAGS = {
|
|
310
|
+
"--resume", "-r",
|
|
311
|
+
"--debug", "-d",
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
_OPTIONAL_VALUE_FLAG_PREFIXES = {
|
|
315
|
+
"--resume=", "-r=",
|
|
316
|
+
"--debug=", "-d=",
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
_OPTIONAL_ALIAS_GROUPS = (
|
|
320
|
+
frozenset({"--resume", "-r"}),
|
|
321
|
+
frozenset({"--debug", "-d"}),
|
|
322
|
+
)
|
|
323
|
+
_OPTIONAL_ALIAS_LOOKUP: Mapping[str, set[str]] = {
|
|
324
|
+
alias: set(group)
|
|
325
|
+
for group in _OPTIONAL_ALIAS_GROUPS
|
|
326
|
+
for alias in group
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
# Flags that require a following value (lowercase).
|
|
330
|
+
_VALUE_FLAGS = {
|
|
331
|
+
"--add-dir",
|
|
332
|
+
"--agents",
|
|
333
|
+
"--allowed-tools",
|
|
334
|
+
"--allowedtools",
|
|
335
|
+
"--disallowedtools",
|
|
336
|
+
"--disallowed-tools",
|
|
337
|
+
"--fallback-model",
|
|
338
|
+
"--input-format",
|
|
339
|
+
"--max-turns",
|
|
340
|
+
"--mcp-config",
|
|
341
|
+
"--model",
|
|
342
|
+
"--output-format",
|
|
343
|
+
"--permission-mode",
|
|
344
|
+
"--permission-prompt-tool",
|
|
345
|
+
"--plugin-dir",
|
|
346
|
+
"--session-id",
|
|
347
|
+
"--setting-sources",
|
|
348
|
+
"--settings",
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
_VALUE_FLAG_PREFIXES = {
|
|
352
|
+
"--add-dir=",
|
|
353
|
+
"--agents=",
|
|
354
|
+
"--allowedtools=",
|
|
355
|
+
"--allowed-tools=",
|
|
356
|
+
"--disallowedtools=",
|
|
357
|
+
"--disallowed-tools=",
|
|
358
|
+
"--fallback-model=",
|
|
359
|
+
"--input-format=",
|
|
360
|
+
"--max-turns=",
|
|
361
|
+
"--mcp-config=",
|
|
362
|
+
"--model=",
|
|
363
|
+
"--output-format=",
|
|
364
|
+
"--permission-mode=",
|
|
365
|
+
"--permission-prompt-tool=",
|
|
366
|
+
"--plugin-dir=",
|
|
367
|
+
"--session-id=",
|
|
368
|
+
"--setting-sources=",
|
|
369
|
+
"--settings=",
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@dataclass(frozen=True)
|
|
374
|
+
class ClaudeArgsSpec:
|
|
375
|
+
"""Normalized representation of Claude CLI arguments."""
|
|
376
|
+
|
|
377
|
+
source: Literal["cli", "env", "none"]
|
|
378
|
+
raw_tokens: Tuple[str, ...]
|
|
379
|
+
clean_tokens: Tuple[str, ...]
|
|
380
|
+
positional_tokens: Tuple[str, ...]
|
|
381
|
+
positional_indexes: Tuple[int, ...]
|
|
382
|
+
system_entries: Tuple[Tuple[str, str], ...]
|
|
383
|
+
system_flag: str | None
|
|
384
|
+
system_value: str | None
|
|
385
|
+
user_system: str | None
|
|
386
|
+
user_append: str | None
|
|
387
|
+
is_background: bool
|
|
388
|
+
flag_values: Mapping[CanonicalFlag, str]
|
|
389
|
+
errors: Tuple[str, ...] = ()
|
|
390
|
+
|
|
391
|
+
def has_flag(
|
|
392
|
+
self,
|
|
393
|
+
names: Iterable[str] | None = None,
|
|
394
|
+
prefixes: Iterable[str] | None = None,
|
|
395
|
+
) -> bool:
|
|
396
|
+
"""Check for user-provided flags (only scans before -- separator)."""
|
|
397
|
+
name_set = {n.lower() for n in (names or ())}
|
|
398
|
+
prefix_tuple = tuple(p.lower() for p in (prefixes or ()))
|
|
399
|
+
|
|
400
|
+
# Only scan tokens before --
|
|
401
|
+
try:
|
|
402
|
+
dash_idx = self.clean_tokens.index('--')
|
|
403
|
+
tokens_to_scan = self.clean_tokens[:dash_idx]
|
|
404
|
+
except ValueError:
|
|
405
|
+
tokens_to_scan = self.clean_tokens
|
|
406
|
+
|
|
407
|
+
for token in tokens_to_scan:
|
|
408
|
+
lower = token.lower()
|
|
409
|
+
if lower in name_set:
|
|
410
|
+
return True
|
|
411
|
+
if any(lower.startswith(prefix) for prefix in prefix_tuple):
|
|
412
|
+
return True
|
|
413
|
+
return False
|
|
414
|
+
|
|
415
|
+
def rebuild_tokens(self, include_system: bool = True) -> list[str]:
|
|
416
|
+
"""Return token list suitable for invoking Claude."""
|
|
417
|
+
tokens = list(self.clean_tokens)
|
|
418
|
+
if include_system and self.system_entries:
|
|
419
|
+
for flag, value in self.system_entries:
|
|
420
|
+
tokens.extend([flag, value])
|
|
421
|
+
return tokens
|
|
422
|
+
|
|
423
|
+
def to_env_string(self) -> str:
|
|
424
|
+
"""Render tokens into a shell-safe env string."""
|
|
425
|
+
return shlex.join(self.rebuild_tokens())
|
|
426
|
+
|
|
427
|
+
def update(
|
|
428
|
+
self,
|
|
429
|
+
*,
|
|
430
|
+
background: bool | None = None,
|
|
431
|
+
system_flag: str | None = None,
|
|
432
|
+
system_value: str | None = None,
|
|
433
|
+
prompt: str | None = None,
|
|
434
|
+
) -> "ClaudeArgsSpec":
|
|
435
|
+
"""Return new spec with requested updates applied."""
|
|
436
|
+
tokens = list(self.clean_tokens)
|
|
437
|
+
|
|
438
|
+
if background is not None:
|
|
439
|
+
tokens = _toggle_background(tokens, self.positional_indexes, background)
|
|
440
|
+
|
|
441
|
+
if prompt is not None:
|
|
442
|
+
if prompt == "":
|
|
443
|
+
# Empty string = delete positional arg
|
|
444
|
+
tokens = _remove_positional(tokens)
|
|
445
|
+
else:
|
|
446
|
+
tokens = _set_prompt(tokens, prompt)
|
|
447
|
+
|
|
448
|
+
updated_entries = list(self.system_entries)
|
|
449
|
+
|
|
450
|
+
# Interpret explicit updates
|
|
451
|
+
if system_flag is not None or system_value is not None:
|
|
452
|
+
if system_value == "":
|
|
453
|
+
updated_entries.clear()
|
|
454
|
+
else:
|
|
455
|
+
current_flag = updated_entries[-1][0] if updated_entries else None
|
|
456
|
+
current_value = updated_entries[-1][1] if updated_entries else None
|
|
457
|
+
|
|
458
|
+
if system_flag is not None:
|
|
459
|
+
if system_flag:
|
|
460
|
+
current_flag = system_flag
|
|
461
|
+
else:
|
|
462
|
+
current_flag = None
|
|
463
|
+
|
|
464
|
+
if system_value is not None:
|
|
465
|
+
current_value = system_value
|
|
466
|
+
|
|
467
|
+
if current_flag is None or current_value is None:
|
|
468
|
+
updated_entries.clear()
|
|
469
|
+
else:
|
|
470
|
+
if updated_entries:
|
|
471
|
+
updated_entries[-1] = (current_flag, current_value)
|
|
472
|
+
else:
|
|
473
|
+
updated_entries.append((current_flag, current_value))
|
|
474
|
+
|
|
475
|
+
combined = list(tokens)
|
|
476
|
+
for flag, value in updated_entries:
|
|
477
|
+
combined.extend([flag, value])
|
|
478
|
+
|
|
479
|
+
return _parse_tokens(combined, self.source)
|
|
480
|
+
|
|
481
|
+
def has_errors(self) -> bool:
|
|
482
|
+
return bool(self.errors)
|
|
483
|
+
|
|
484
|
+
def get_flag_value(self, flag_name: str) -> str | None:
|
|
485
|
+
"""Get value of any flag by searching clean_tokens.
|
|
486
|
+
|
|
487
|
+
Searches for both space-separated (--flag value) and equals-form (--flag=value).
|
|
488
|
+
Handles registered aliases (e.g., --allowed-tools and --allowedtools return same value).
|
|
489
|
+
Returns None if flag not found.
|
|
490
|
+
|
|
491
|
+
Examples:
|
|
492
|
+
spec.get_flag_value('--output-format')
|
|
493
|
+
spec.get_flag_value('--model')
|
|
494
|
+
spec.get_flag_value('-r') # Short form for --resume
|
|
495
|
+
"""
|
|
496
|
+
flag_lower = flag_name.lower()
|
|
497
|
+
|
|
498
|
+
# Build list of possible flag names (original + aliases)
|
|
499
|
+
possible_flags = {flag_lower}
|
|
500
|
+
|
|
501
|
+
# Add canonical form if this is an alias
|
|
502
|
+
if flag_lower in _FLAG_ALIASES:
|
|
503
|
+
canonical = _FLAG_ALIASES[flag_lower]
|
|
504
|
+
possible_flags.add(canonical.lower())
|
|
505
|
+
|
|
506
|
+
# Add all aliases that map to same canonical
|
|
507
|
+
for alias, canonical in _FLAG_ALIASES.items():
|
|
508
|
+
if canonical.lower() == flag_lower or alias.lower() == flag_lower:
|
|
509
|
+
possible_flags.add(alias.lower())
|
|
510
|
+
possible_flags.add(canonical.lower())
|
|
511
|
+
|
|
512
|
+
# Include optional flag aliases (e.g., -r <-> --resume)
|
|
513
|
+
if flag_lower in _OPTIONAL_ALIAS_LOOKUP:
|
|
514
|
+
possible_flags.update(_OPTIONAL_ALIAS_LOOKUP[flag_lower])
|
|
515
|
+
|
|
516
|
+
# Check for --flag=value form in clean_tokens
|
|
517
|
+
for token in self.clean_tokens:
|
|
518
|
+
token_lower = token.lower()
|
|
519
|
+
for possible_flag in possible_flags:
|
|
520
|
+
if token_lower.startswith(possible_flag + '='):
|
|
521
|
+
return token[len(possible_flag) + 1:]
|
|
522
|
+
|
|
523
|
+
# Check for --flag value form (space-separated)
|
|
524
|
+
i = 0
|
|
525
|
+
while i < len(self.clean_tokens):
|
|
526
|
+
token_lower = self.clean_tokens[i].lower()
|
|
527
|
+
if token_lower in possible_flags:
|
|
528
|
+
# Found flag, check if next token is the value
|
|
529
|
+
if i + 1 < len(self.clean_tokens):
|
|
530
|
+
next_token = self.clean_tokens[i + 1]
|
|
531
|
+
# Ensure next token isn't another flag
|
|
532
|
+
if not _looks_like_new_flag(next_token.lower()):
|
|
533
|
+
return next_token
|
|
534
|
+
return None # Flag present but no value
|
|
535
|
+
i += 1
|
|
536
|
+
|
|
537
|
+
return None
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def resolve_claude_args(
|
|
541
|
+
cli_args: Sequence[str] | None,
|
|
542
|
+
env_value: str | None,
|
|
543
|
+
) -> ClaudeArgsSpec:
|
|
544
|
+
"""Resolve Claude args from CLI (highest precedence) or env string."""
|
|
545
|
+
if cli_args:
|
|
546
|
+
return _parse_tokens(cli_args, "cli")
|
|
547
|
+
|
|
548
|
+
if env_value is not None:
|
|
549
|
+
try:
|
|
550
|
+
tokens = _split_env(env_value)
|
|
551
|
+
except ValueError as err:
|
|
552
|
+
return _parse_tokens([], "env", initial_errors=[f"invalid Claude args: {err}"])
|
|
553
|
+
return _parse_tokens(tokens, "env")
|
|
554
|
+
|
|
555
|
+
return _parse_tokens([], "none")
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def merge_system_prompts(
|
|
559
|
+
user_append: str | None,
|
|
560
|
+
user_system: str | None,
|
|
561
|
+
agent_content: str | None,
|
|
562
|
+
*,
|
|
563
|
+
prefer_system_flag: bool,
|
|
564
|
+
) -> Tuple[str | None, str]:
|
|
565
|
+
"""Merge user/agent system prompts, returning content and flag."""
|
|
566
|
+
if not agent_content:
|
|
567
|
+
if user_system:
|
|
568
|
+
return user_system, "--system-prompt"
|
|
569
|
+
if user_append:
|
|
570
|
+
return user_append, "--append-system-prompt"
|
|
571
|
+
return None, ""
|
|
572
|
+
|
|
573
|
+
blocks = []
|
|
574
|
+
if user_system:
|
|
575
|
+
blocks.append(user_system)
|
|
576
|
+
if user_append:
|
|
577
|
+
blocks.append(user_append)
|
|
578
|
+
blocks.append(agent_content)
|
|
579
|
+
|
|
580
|
+
merged = "\n\n".join(blocks)
|
|
581
|
+
if user_system or prefer_system_flag:
|
|
582
|
+
return merged, "--system-prompt"
|
|
583
|
+
return merged, "--append-system-prompt"
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def extract_system_prompt_args(tokens: Sequence[str]) -> Tuple[list[str], str | None, str | None]:
|
|
587
|
+
"""Public helper mirroring legacy behaviour."""
|
|
588
|
+
spec = _parse_tokens(tokens, "cli")
|
|
589
|
+
return list(spec.clean_tokens), spec.user_append, spec.user_system
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def add_background_defaults(spec: ClaudeArgsSpec) -> ClaudeArgsSpec:
|
|
593
|
+
"""Add HCOM-specific background mode defaults if missing.
|
|
594
|
+
|
|
595
|
+
When background mode is detected (-p/--print), adds:
|
|
596
|
+
- --output-format stream-json (if not already set)
|
|
597
|
+
- --verbose (if not already set)
|
|
598
|
+
|
|
599
|
+
Returns unchanged spec if not in background mode or flags already present.
|
|
600
|
+
"""
|
|
601
|
+
if not spec.is_background:
|
|
602
|
+
return spec
|
|
603
|
+
|
|
604
|
+
tokens = list(spec.clean_tokens)
|
|
605
|
+
modified = False
|
|
606
|
+
|
|
607
|
+
# Find -- separator index if present
|
|
608
|
+
try:
|
|
609
|
+
dash_idx = tokens.index('--')
|
|
610
|
+
insert_idx = dash_idx
|
|
611
|
+
except ValueError:
|
|
612
|
+
insert_idx = len(tokens)
|
|
613
|
+
|
|
614
|
+
# Add --output-format stream-json if missing (insert before --)
|
|
615
|
+
if not spec.has_flag(['--output-format'], ('--output-format=',)):
|
|
616
|
+
tokens.insert(insert_idx, 'stream-json')
|
|
617
|
+
tokens.insert(insert_idx, '--output-format')
|
|
618
|
+
modified = True
|
|
619
|
+
insert_idx += 2 # Adjust insert position
|
|
620
|
+
|
|
621
|
+
# Add --verbose if missing (insert before --)
|
|
622
|
+
if not spec.has_flag(['--verbose']):
|
|
623
|
+
tokens.insert(insert_idx, '--verbose')
|
|
624
|
+
modified = True
|
|
625
|
+
|
|
626
|
+
if not modified:
|
|
627
|
+
return spec
|
|
628
|
+
|
|
629
|
+
# Re-parse to get updated spec with system entries preserved
|
|
630
|
+
combined = tokens[:]
|
|
631
|
+
for flag, value in spec.system_entries:
|
|
632
|
+
combined.extend([flag, value])
|
|
633
|
+
|
|
634
|
+
return _parse_tokens(combined, spec.source)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def validate_conflicts(spec: ClaudeArgsSpec) -> list[str]:
|
|
638
|
+
"""Check for conflicting flag combinations.
|
|
639
|
+
|
|
640
|
+
Returns list of warning messages for:
|
|
641
|
+
- Multiple system prompts (informational, not an error)
|
|
642
|
+
- Other known conflicts
|
|
643
|
+
|
|
644
|
+
Empty list means no conflicts detected.
|
|
645
|
+
"""
|
|
646
|
+
warnings = []
|
|
647
|
+
|
|
648
|
+
# Check for multiple system prompt entries
|
|
649
|
+
if len(spec.system_entries) > 1:
|
|
650
|
+
flags = [f for f, _ in spec.system_entries]
|
|
651
|
+
warnings.append(
|
|
652
|
+
f"Multiple system prompts detected: {', '.join(flags)}. "
|
|
653
|
+
f"All will be included in order."
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# Could add more conflict checks here:
|
|
657
|
+
# - --print with interactive-only flags
|
|
658
|
+
# - Conflicting permission modes
|
|
659
|
+
# etc.
|
|
660
|
+
|
|
661
|
+
return warnings
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _parse_tokens(
|
|
665
|
+
tokens: Sequence[str],
|
|
666
|
+
source: Literal["cli", "env", "none"],
|
|
667
|
+
initial_errors: Sequence[str] | None = None,
|
|
668
|
+
) -> ClaudeArgsSpec:
|
|
669
|
+
errors = list(initial_errors or [])
|
|
670
|
+
clean: list[str] = []
|
|
671
|
+
positional: list[str] = []
|
|
672
|
+
positional_indexes: list[int] = []
|
|
673
|
+
flag_values: dict[CanonicalFlag, str] = {}
|
|
674
|
+
system_entries: list[Tuple[str, str]] = []
|
|
675
|
+
|
|
676
|
+
pending_system: str | None = None
|
|
677
|
+
pending_canonical: CanonicalFlag | None = None
|
|
678
|
+
pending_canonical_token: str | None = None
|
|
679
|
+
pending_generic_flag: str | None = None
|
|
680
|
+
after_double_dash = False
|
|
681
|
+
|
|
682
|
+
system_flag: str | None = None
|
|
683
|
+
system_value: str | None = None
|
|
684
|
+
user_system: str | None = None
|
|
685
|
+
user_append: str | None = None
|
|
686
|
+
is_background = False
|
|
687
|
+
|
|
688
|
+
i = 0
|
|
689
|
+
raw_tokens = tuple(tokens)
|
|
690
|
+
|
|
691
|
+
while i < len(tokens):
|
|
692
|
+
token = tokens[i]
|
|
693
|
+
token_lower = token.lower()
|
|
694
|
+
advance = True
|
|
695
|
+
|
|
696
|
+
if pending_system:
|
|
697
|
+
if _looks_like_new_flag(token_lower):
|
|
698
|
+
errors.append(f"{pending_system} requires a value before '{token}'")
|
|
699
|
+
pending_system = None
|
|
700
|
+
advance = False
|
|
701
|
+
else:
|
|
702
|
+
system_entries.append((pending_system, token))
|
|
703
|
+
system_flag = pending_system
|
|
704
|
+
system_value = token
|
|
705
|
+
if pending_system == "--system-prompt":
|
|
706
|
+
user_system = token
|
|
707
|
+
else:
|
|
708
|
+
user_append = token
|
|
709
|
+
pending_system = None
|
|
710
|
+
if advance:
|
|
711
|
+
i += 1
|
|
712
|
+
continue
|
|
713
|
+
|
|
714
|
+
if pending_canonical:
|
|
715
|
+
if _looks_like_new_flag(token_lower):
|
|
716
|
+
display = pending_canonical_token or pending_canonical
|
|
717
|
+
errors.append(f"{display} requires a value before '{token}'")
|
|
718
|
+
pending_canonical = None
|
|
719
|
+
pending_canonical_token = None
|
|
720
|
+
advance = False
|
|
721
|
+
else:
|
|
722
|
+
idx = len(clean)
|
|
723
|
+
clean.append(token)
|
|
724
|
+
if after_double_dash:
|
|
725
|
+
positional.append(token)
|
|
726
|
+
positional_indexes.append(idx)
|
|
727
|
+
flag_values[pending_canonical] = token
|
|
728
|
+
pending_canonical = None
|
|
729
|
+
pending_canonical_token = None
|
|
730
|
+
if advance:
|
|
731
|
+
i += 1
|
|
732
|
+
continue
|
|
733
|
+
|
|
734
|
+
if pending_generic_flag:
|
|
735
|
+
if _looks_like_new_flag(token_lower):
|
|
736
|
+
errors.append(f"{pending_generic_flag} requires a value before '{token}'")
|
|
737
|
+
pending_generic_flag = None
|
|
738
|
+
advance = False
|
|
739
|
+
else:
|
|
740
|
+
idx = len(clean)
|
|
741
|
+
clean.append(token)
|
|
742
|
+
if after_double_dash:
|
|
743
|
+
positional.append(token)
|
|
744
|
+
positional_indexes.append(idx)
|
|
745
|
+
pending_generic_flag = None
|
|
746
|
+
if advance:
|
|
747
|
+
i += 1
|
|
748
|
+
continue
|
|
749
|
+
|
|
750
|
+
if after_double_dash:
|
|
751
|
+
idx = len(clean)
|
|
752
|
+
clean.append(token)
|
|
753
|
+
positional.append(token)
|
|
754
|
+
positional_indexes.append(idx)
|
|
755
|
+
i += 1
|
|
756
|
+
continue
|
|
757
|
+
|
|
758
|
+
if token_lower == "--":
|
|
759
|
+
clean.append(token)
|
|
760
|
+
after_double_dash = True
|
|
761
|
+
i += 1
|
|
762
|
+
continue
|
|
763
|
+
|
|
764
|
+
if token_lower in _BACKGROUND_SWITCHES:
|
|
765
|
+
is_background = True
|
|
766
|
+
clean.append(token)
|
|
767
|
+
i += 1
|
|
768
|
+
continue
|
|
769
|
+
|
|
770
|
+
if token_lower in _BOOLEAN_FLAGS:
|
|
771
|
+
clean.append(token)
|
|
772
|
+
i += 1
|
|
773
|
+
continue
|
|
774
|
+
|
|
775
|
+
system_assignment = _extract_system_assignment(token, token_lower)
|
|
776
|
+
if system_assignment:
|
|
777
|
+
assigned_flag, value = system_assignment
|
|
778
|
+
system_entries.append((assigned_flag, value))
|
|
779
|
+
system_flag = assigned_flag
|
|
780
|
+
system_value = value
|
|
781
|
+
if assigned_flag == "--system-prompt":
|
|
782
|
+
user_system = value
|
|
783
|
+
else:
|
|
784
|
+
user_append = value
|
|
785
|
+
i += 1
|
|
786
|
+
continue
|
|
787
|
+
|
|
788
|
+
canonical_assignment = _extract_canonical_prefixed(token, token_lower)
|
|
789
|
+
if canonical_assignment:
|
|
790
|
+
canonical_flag, value = canonical_assignment
|
|
791
|
+
clean.append(token)
|
|
792
|
+
flag_values[canonical_flag] = value
|
|
793
|
+
i += 1
|
|
794
|
+
continue
|
|
795
|
+
|
|
796
|
+
if any(token_lower.startswith(prefix) for prefix in _VALUE_FLAG_PREFIXES):
|
|
797
|
+
clean.append(token)
|
|
798
|
+
i += 1
|
|
799
|
+
continue
|
|
800
|
+
|
|
801
|
+
if token_lower in _FLAG_ALIASES:
|
|
802
|
+
pending_canonical = _FLAG_ALIASES[token_lower]
|
|
803
|
+
pending_canonical_token = token
|
|
804
|
+
clean.append(token)
|
|
805
|
+
i += 1
|
|
806
|
+
continue
|
|
807
|
+
|
|
808
|
+
# Handle optional value flags (--resume, --debug, etc.)
|
|
809
|
+
optional_assignment = None
|
|
810
|
+
for prefix in _OPTIONAL_VALUE_FLAG_PREFIXES:
|
|
811
|
+
if token_lower.startswith(prefix):
|
|
812
|
+
# --resume=value or --debug=filter form
|
|
813
|
+
optional_assignment = token
|
|
814
|
+
break
|
|
815
|
+
|
|
816
|
+
if optional_assignment:
|
|
817
|
+
clean.append(token)
|
|
818
|
+
i += 1
|
|
819
|
+
continue
|
|
820
|
+
|
|
821
|
+
if token_lower in _OPTIONAL_VALUE_FLAGS:
|
|
822
|
+
# Peek ahead - only consume value if it's not a flag
|
|
823
|
+
if i + 1 < len(tokens):
|
|
824
|
+
next_token = tokens[i + 1]
|
|
825
|
+
next_lower = next_token.lower()
|
|
826
|
+
if not _looks_like_new_flag(next_lower):
|
|
827
|
+
# Has a value, treat as value flag
|
|
828
|
+
pending_generic_flag = token
|
|
829
|
+
clean.append(token)
|
|
830
|
+
i += 1
|
|
831
|
+
continue
|
|
832
|
+
# No value or next is a flag - just add the flag alone
|
|
833
|
+
clean.append(token)
|
|
834
|
+
i += 1
|
|
835
|
+
continue
|
|
836
|
+
|
|
837
|
+
if token_lower in _VALUE_FLAGS:
|
|
838
|
+
pending_generic_flag = token
|
|
839
|
+
clean.append(token)
|
|
840
|
+
i += 1
|
|
841
|
+
continue
|
|
842
|
+
|
|
843
|
+
if token_lower in _SYSTEM_FLAGS:
|
|
844
|
+
pending_system = "--system-prompt" if token_lower == "--system-prompt" else "--append-system-prompt"
|
|
845
|
+
i += 1
|
|
846
|
+
continue
|
|
847
|
+
|
|
848
|
+
idx = len(clean)
|
|
849
|
+
clean.append(token)
|
|
850
|
+
if not _looks_like_new_flag(token_lower):
|
|
851
|
+
positional.append(token)
|
|
852
|
+
positional_indexes.append(idx)
|
|
853
|
+
i += 1
|
|
854
|
+
|
|
855
|
+
if pending_system:
|
|
856
|
+
errors.append(f"{pending_system} requires a value at end of arguments")
|
|
857
|
+
if pending_canonical:
|
|
858
|
+
display = pending_canonical_token or pending_canonical
|
|
859
|
+
errors.append(f"{display} requires a value at end of arguments")
|
|
860
|
+
if pending_generic_flag:
|
|
861
|
+
errors.append(f"{pending_generic_flag} requires a value at end of arguments")
|
|
862
|
+
|
|
863
|
+
last_flag = system_entries[-1][0] if system_entries else None
|
|
864
|
+
last_value = system_entries[-1][1] if system_entries else None
|
|
865
|
+
|
|
866
|
+
return ClaudeArgsSpec(
|
|
867
|
+
source=source,
|
|
868
|
+
raw_tokens=raw_tokens,
|
|
869
|
+
clean_tokens=tuple(clean),
|
|
870
|
+
positional_tokens=tuple(positional),
|
|
871
|
+
positional_indexes=tuple(positional_indexes),
|
|
872
|
+
system_entries=tuple(system_entries),
|
|
873
|
+
system_flag=last_flag,
|
|
874
|
+
system_value=last_value,
|
|
875
|
+
user_system=user_system,
|
|
876
|
+
user_append=user_append,
|
|
877
|
+
is_background=is_background,
|
|
878
|
+
flag_values=dict(flag_values),
|
|
879
|
+
errors=tuple(errors),
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _split_env(env_value: str) -> list[str]:
|
|
884
|
+
return shlex.split(env_value)
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _extract_system_assignment(token: str, token_lower: str) -> tuple[str, str] | None:
|
|
888
|
+
for prefix, canonical in _SYSTEM_PREFIXES.items():
|
|
889
|
+
if token_lower.startswith(prefix):
|
|
890
|
+
value = token[len(prefix):]
|
|
891
|
+
return canonical, value
|
|
892
|
+
return None
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _extract_canonical_prefixed(token: str, token_lower: str) -> tuple[CanonicalFlag, str] | None:
|
|
896
|
+
for prefix, canonical in _CANONICAL_PREFIXES.items():
|
|
897
|
+
if token_lower.startswith(prefix):
|
|
898
|
+
return canonical, token[len(prefix):]
|
|
899
|
+
return None
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _looks_like_new_flag(token_lower: str) -> bool:
|
|
903
|
+
"""Check if token looks like a flag (not a value).
|
|
904
|
+
|
|
905
|
+
Used to detect when a flag is missing its value (next token is another flag).
|
|
906
|
+
Recognizes known flags explicitly, no catch-all hyphen check.
|
|
907
|
+
"""
|
|
908
|
+
if token_lower in _BACKGROUND_SWITCHES:
|
|
909
|
+
return True
|
|
910
|
+
if token_lower in _SYSTEM_FLAGS:
|
|
911
|
+
return True
|
|
912
|
+
if token_lower in _BOOLEAN_FLAGS:
|
|
913
|
+
return True
|
|
914
|
+
if token_lower in _FLAG_ALIASES:
|
|
915
|
+
return True
|
|
916
|
+
if token_lower in _OPTIONAL_VALUE_FLAGS:
|
|
917
|
+
return True
|
|
918
|
+
if token_lower in _VALUE_FLAGS:
|
|
919
|
+
return True
|
|
920
|
+
if token_lower == "--":
|
|
921
|
+
return True
|
|
922
|
+
if any(token_lower.startswith(prefix) for prefix in _OPTIONAL_VALUE_FLAG_PREFIXES):
|
|
923
|
+
return True
|
|
924
|
+
if any(token_lower.startswith(prefix) for prefix in _VALUE_FLAG_PREFIXES):
|
|
925
|
+
return True
|
|
926
|
+
if any(token_lower.startswith(prefix) for prefix in _SYSTEM_PREFIXES):
|
|
927
|
+
return True
|
|
928
|
+
if any(token_lower.startswith(prefix) for prefix in _CANONICAL_PREFIXES):
|
|
929
|
+
return True
|
|
930
|
+
# NOTE: No catch-all token_lower.startswith("-") check here!
|
|
931
|
+
# That would reject valid values like "- check something" or "-1"
|
|
932
|
+
# Instead, we explicitly list known boolean flags above
|
|
933
|
+
return False
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def _toggle_background(tokens: Sequence[str], positional_indexes: Tuple[int, ...], desired: bool) -> list[str]:
|
|
937
|
+
"""Toggle background flag, preserving positional arguments.
|
|
938
|
+
|
|
939
|
+
Args:
|
|
940
|
+
tokens: Token list to process
|
|
941
|
+
positional_indexes: Indexes of positional arguments (not to be filtered)
|
|
942
|
+
desired: True to enable background mode, False to disable
|
|
943
|
+
|
|
944
|
+
Returns:
|
|
945
|
+
Modified token list with background flag toggled
|
|
946
|
+
"""
|
|
947
|
+
tokens_list = list(tokens)
|
|
948
|
+
|
|
949
|
+
# Only filter tokens that are NOT positionals
|
|
950
|
+
filtered = []
|
|
951
|
+
for idx, token in enumerate(tokens_list):
|
|
952
|
+
if idx in positional_indexes:
|
|
953
|
+
# Keep positionals even if they look like flags
|
|
954
|
+
filtered.append(token)
|
|
955
|
+
elif token.lower() not in _BACKGROUND_SWITCHES:
|
|
956
|
+
filtered.append(token)
|
|
957
|
+
|
|
958
|
+
has_background = len(filtered) != len(tokens_list)
|
|
959
|
+
|
|
960
|
+
if desired:
|
|
961
|
+
if has_background:
|
|
962
|
+
return tokens_list
|
|
963
|
+
return ["-p"] + filtered
|
|
964
|
+
return filtered
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _set_prompt(tokens: Sequence[str], value: str) -> list[str]:
|
|
968
|
+
tokens_list = list(tokens)
|
|
969
|
+
index = _find_first_positional_index(tokens_list)
|
|
970
|
+
if index is None:
|
|
971
|
+
tokens_list.append(value)
|
|
972
|
+
else:
|
|
973
|
+
tokens_list[index] = value
|
|
974
|
+
return tokens_list
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
def _remove_positional(tokens: Sequence[str]) -> list[str]:
|
|
978
|
+
"""Remove first positional argument from tokens"""
|
|
979
|
+
tokens_list = list(tokens)
|
|
980
|
+
index = _find_first_positional_index(tokens_list)
|
|
981
|
+
if index is not None:
|
|
982
|
+
tokens_list.pop(index)
|
|
983
|
+
return tokens_list
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def _find_first_positional_index(tokens: Sequence[str]) -> int | None:
|
|
987
|
+
pending_system = False
|
|
988
|
+
pending_canonical = False
|
|
989
|
+
pending_generic = False
|
|
990
|
+
after_double_dash = False
|
|
991
|
+
|
|
992
|
+
for idx, token in enumerate(tokens):
|
|
993
|
+
token_lower = token.lower()
|
|
994
|
+
|
|
995
|
+
if after_double_dash:
|
|
996
|
+
return idx
|
|
997
|
+
if token_lower == "--":
|
|
998
|
+
after_double_dash = True
|
|
999
|
+
continue
|
|
1000
|
+
if pending_system:
|
|
1001
|
+
pending_system = False
|
|
1002
|
+
continue
|
|
1003
|
+
if pending_canonical:
|
|
1004
|
+
pending_canonical = False
|
|
1005
|
+
continue
|
|
1006
|
+
if pending_generic:
|
|
1007
|
+
pending_generic = False
|
|
1008
|
+
continue
|
|
1009
|
+
if token_lower in _BACKGROUND_SWITCHES:
|
|
1010
|
+
continue
|
|
1011
|
+
if token_lower in _BOOLEAN_FLAGS:
|
|
1012
|
+
continue
|
|
1013
|
+
if _extract_system_assignment(token, token_lower):
|
|
1014
|
+
continue
|
|
1015
|
+
if token_lower in _SYSTEM_FLAGS:
|
|
1016
|
+
pending_system = True
|
|
1017
|
+
continue
|
|
1018
|
+
if _extract_canonical_prefixed(token, token_lower):
|
|
1019
|
+
continue
|
|
1020
|
+
if any(token_lower.startswith(prefix) for prefix in _OPTIONAL_VALUE_FLAG_PREFIXES):
|
|
1021
|
+
continue
|
|
1022
|
+
if any(token_lower.startswith(prefix) for prefix in _VALUE_FLAG_PREFIXES):
|
|
1023
|
+
continue
|
|
1024
|
+
if token_lower in _FLAG_ALIASES:
|
|
1025
|
+
pending_canonical = True
|
|
1026
|
+
continue
|
|
1027
|
+
if token_lower in _OPTIONAL_VALUE_FLAGS:
|
|
1028
|
+
pending_generic = True
|
|
1029
|
+
continue
|
|
1030
|
+
if token_lower in _VALUE_FLAGS:
|
|
1031
|
+
pending_generic = True
|
|
1032
|
+
continue
|
|
1033
|
+
if _looks_like_new_flag(token_lower):
|
|
1034
|
+
continue
|
|
1035
|
+
return idx
|
|
1036
|
+
return None
|