abelworkflow 0.1.0
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.
- package/.gitignore +13 -0
- package/.skill-lock.json +29 -0
- package/AGENTS.md +45 -0
- package/README.md +147 -0
- package/bin/abelworkflow.mjs +2 -0
- package/commands/oc/diagnose.md +63 -0
- package/commands/oc/implementation.md +157 -0
- package/commands/oc/init.md +27 -0
- package/commands/oc/plan.md +88 -0
- package/commands/oc/research.md +126 -0
- package/lib/cli.mjs +222 -0
- package/package.json +23 -0
- package/skills/confidence-check/SKILL.md +124 -0
- package/skills/confidence-check/confidence.ts +335 -0
- package/skills/context7-auto-research/.env +4 -0
- package/skills/context7-auto-research/.env.example +4 -0
- package/skills/context7-auto-research/SKILL.md +83 -0
- package/skills/context7-auto-research/context7-api.js +283 -0
- package/skills/dev-browser/SKILL.md +225 -0
- package/skills/dev-browser/bun.lock +443 -0
- package/skills/dev-browser/package-lock.json +2988 -0
- package/skills/dev-browser/package.json +31 -0
- package/skills/dev-browser/references/scraping.md +155 -0
- package/skills/dev-browser/resolve-skill-dir.sh +35 -0
- package/skills/dev-browser/scripts/start-relay.ts +32 -0
- package/skills/dev-browser/scripts/start-server.ts +117 -0
- package/skills/dev-browser/server.sh +24 -0
- package/skills/dev-browser/src/client.ts +474 -0
- package/skills/dev-browser/src/index.ts +287 -0
- package/skills/dev-browser/src/relay.ts +731 -0
- package/skills/dev-browser/src/snapshot/browser-script.ts +877 -0
- package/skills/dev-browser/src/snapshot/index.ts +14 -0
- package/skills/dev-browser/src/snapshot/inject.ts +13 -0
- package/skills/dev-browser/src/types.ts +34 -0
- package/skills/dev-browser/tsconfig.json +36 -0
- package/skills/dev-browser/vitest.config.ts +12 -0
- package/skills/git-commit/SKILL.md +124 -0
- package/skills/grok-search/.env.example +24 -0
- package/skills/grok-search/SKILL.md +114 -0
- package/skills/grok-search/requirements.txt +2 -0
- package/skills/grok-search/scripts/groksearch_cli.py +1214 -0
- package/skills/grok-search/scripts/groksearch_entry.py +116 -0
- package/skills/prompt-enhancer/ADVANCED.md +74 -0
- package/skills/prompt-enhancer/SKILL.md +71 -0
- package/skills/prompt-enhancer/TEMPLATE.md +91 -0
- package/skills/prompt-enhancer/scripts/enhance.py +142 -0
- package/skills/sequential-think/SKILL.md +198 -0
- package/skills/sequential-think/scripts/.env.example +5 -0
- package/skills/sequential-think/scripts/sequential_think_cli.py +253 -0
- package/skills/time/SKILL.md +116 -0
- package/skills/time/scripts/time_cli.py +104 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Sequential Think CLI - Standalone iterative thinking engine for complex problem-solving."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass, field, asdict
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional, Dict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ============================================================================
|
|
14
|
+
# Data Models
|
|
15
|
+
# ============================================================================
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ThoughtData:
|
|
19
|
+
thought: str
|
|
20
|
+
thought_number: int
|
|
21
|
+
total_thoughts: int
|
|
22
|
+
next_thought_needed: bool = True
|
|
23
|
+
is_revision: bool = False
|
|
24
|
+
revises_thought: Optional[int] = None
|
|
25
|
+
branch_from_thought: Optional[int] = None
|
|
26
|
+
branch_id: Optional[str] = None
|
|
27
|
+
needs_more_thoughts: bool = False
|
|
28
|
+
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ============================================================================
|
|
32
|
+
# Thought History Manager
|
|
33
|
+
# ============================================================================
|
|
34
|
+
|
|
35
|
+
class ThoughtHistoryManager:
|
|
36
|
+
_instance = None
|
|
37
|
+
|
|
38
|
+
def __new__(cls):
|
|
39
|
+
if cls._instance is None:
|
|
40
|
+
cls._instance = super().__new__(cls)
|
|
41
|
+
cls._instance._history: List[ThoughtData] = []
|
|
42
|
+
cls._instance._branches: Dict[str, List[ThoughtData]] = {}
|
|
43
|
+
cls._instance._history_file: Optional[Path] = None
|
|
44
|
+
return cls._instance
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def history_file(self) -> Path:
|
|
48
|
+
if self._history_file is None:
|
|
49
|
+
config_dir = Path.home() / ".config" / "sequential-think"
|
|
50
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
self._history_file = config_dir / "thought_history.json"
|
|
52
|
+
return self._history_file
|
|
53
|
+
|
|
54
|
+
def _load_history(self) -> None:
|
|
55
|
+
if self.history_file.exists():
|
|
56
|
+
try:
|
|
57
|
+
with open(self.history_file, 'r', encoding='utf-8') as f:
|
|
58
|
+
data = json.load(f)
|
|
59
|
+
self._history = [ThoughtData(**t) for t in data.get("history", [])]
|
|
60
|
+
self._branches = {
|
|
61
|
+
k: [ThoughtData(**t) for t in v]
|
|
62
|
+
for k, v in data.get("branches", {}).items()
|
|
63
|
+
}
|
|
64
|
+
except (json.JSONDecodeError, IOError, TypeError):
|
|
65
|
+
self._history = []
|
|
66
|
+
self._branches = {}
|
|
67
|
+
|
|
68
|
+
def _save_history(self) -> None:
|
|
69
|
+
data = {
|
|
70
|
+
"history": [asdict(t) for t in self._history],
|
|
71
|
+
"branches": {k: [asdict(t) for t in v] for k, v in self._branches.items()}
|
|
72
|
+
}
|
|
73
|
+
with open(self.history_file, 'w', encoding='utf-8') as f:
|
|
74
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
75
|
+
|
|
76
|
+
def add_thought(self, thought: ThoughtData) -> Dict:
|
|
77
|
+
self._load_history()
|
|
78
|
+
|
|
79
|
+
# Auto-adjust total if thought_number exceeds it
|
|
80
|
+
if thought.thought_number > thought.total_thoughts:
|
|
81
|
+
thought.total_thoughts = thought.thought_number
|
|
82
|
+
|
|
83
|
+
self._history.append(thought)
|
|
84
|
+
|
|
85
|
+
# Track branches
|
|
86
|
+
if thought.branch_from_thought and thought.branch_id:
|
|
87
|
+
if thought.branch_id not in self._branches:
|
|
88
|
+
self._branches[thought.branch_id] = []
|
|
89
|
+
self._branches[thought.branch_id].append(thought)
|
|
90
|
+
|
|
91
|
+
self._save_history()
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"thoughtNumber": thought.thought_number,
|
|
95
|
+
"totalThoughts": thought.total_thoughts,
|
|
96
|
+
"nextThoughtNeeded": thought.next_thought_needed,
|
|
97
|
+
"branches": list(self._branches.keys()),
|
|
98
|
+
"thoughtHistoryLength": len(self._history)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def get_history(self) -> Dict:
|
|
102
|
+
self._load_history()
|
|
103
|
+
return {
|
|
104
|
+
"history": [asdict(t) for t in self._history],
|
|
105
|
+
"branches": {k: [asdict(t) for t in v] for k, v in self._branches.items()},
|
|
106
|
+
"totalThoughts": len(self._history)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def clear_history(self) -> Dict:
|
|
110
|
+
self._history = []
|
|
111
|
+
self._branches = {}
|
|
112
|
+
if self.history_file.exists():
|
|
113
|
+
self.history_file.unlink()
|
|
114
|
+
return {"status": "cleared", "message": "Thought history cleared"}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
manager = ThoughtHistoryManager()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ============================================================================
|
|
121
|
+
# Formatters
|
|
122
|
+
# ============================================================================
|
|
123
|
+
|
|
124
|
+
def format_thought_text(thought: ThoughtData) -> str:
|
|
125
|
+
prefix = ""
|
|
126
|
+
context = ""
|
|
127
|
+
|
|
128
|
+
if thought.is_revision:
|
|
129
|
+
prefix = "🔄 Revision"
|
|
130
|
+
context = f" (revising thought {thought.revises_thought})"
|
|
131
|
+
elif thought.branch_from_thought:
|
|
132
|
+
prefix = "🌿 Branch"
|
|
133
|
+
context = f" (from thought {thought.branch_from_thought}, ID: {thought.branch_id})"
|
|
134
|
+
else:
|
|
135
|
+
prefix = "💭 Thought"
|
|
136
|
+
|
|
137
|
+
header = f"{prefix} {thought.thought_number}/{thought.total_thoughts}{context}"
|
|
138
|
+
border = "─" * max(len(header), min(len(thought.thought), 60)) + "────"
|
|
139
|
+
|
|
140
|
+
return f"""
|
|
141
|
+
┌{border}┐
|
|
142
|
+
│ {header.ljust(len(border) - 2)} │
|
|
143
|
+
├{border}┤
|
|
144
|
+
│ {thought.thought[:len(border) - 2].ljust(len(border) - 2)} │
|
|
145
|
+
└{border}┘"""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def format_history_text(history: Dict) -> str:
|
|
149
|
+
if not history["history"]:
|
|
150
|
+
return "No thoughts recorded yet."
|
|
151
|
+
|
|
152
|
+
lines = ["=" * 60, "THOUGHT HISTORY", "=" * 60, ""]
|
|
153
|
+
|
|
154
|
+
for t in history["history"]:
|
|
155
|
+
thought = ThoughtData(**t)
|
|
156
|
+
lines.append(format_thought_text(thought))
|
|
157
|
+
|
|
158
|
+
if history["branches"]:
|
|
159
|
+
lines.extend(["", "-" * 60, "BRANCHES:", "-" * 60])
|
|
160
|
+
for branch_id, thoughts in history["branches"].items():
|
|
161
|
+
lines.append(f"\n[{branch_id}]")
|
|
162
|
+
for t in thoughts:
|
|
163
|
+
lines.append(f" Thought {t['thought_number']}: {t['thought'][:50]}...")
|
|
164
|
+
|
|
165
|
+
return "\n".join(lines)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ============================================================================
|
|
169
|
+
# Commands
|
|
170
|
+
# ============================================================================
|
|
171
|
+
|
|
172
|
+
def cmd_think(args) -> None:
|
|
173
|
+
thought = ThoughtData(
|
|
174
|
+
thought=args.thought,
|
|
175
|
+
thought_number=args.thought_number,
|
|
176
|
+
total_thoughts=args.total_thoughts,
|
|
177
|
+
next_thought_needed=not args.no_next,
|
|
178
|
+
is_revision=args.is_revision,
|
|
179
|
+
revises_thought=args.revises_thought,
|
|
180
|
+
branch_from_thought=args.branch_from,
|
|
181
|
+
branch_id=args.branch_id,
|
|
182
|
+
needs_more_thoughts=args.needs_more
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
result = manager.add_thought(thought)
|
|
186
|
+
|
|
187
|
+
# Print formatted thought to stderr for visibility
|
|
188
|
+
if not args.quiet:
|
|
189
|
+
print(format_thought_text(thought), file=sys.stderr)
|
|
190
|
+
|
|
191
|
+
# Output JSON result
|
|
192
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def cmd_history(args) -> None:
|
|
196
|
+
history = manager.get_history()
|
|
197
|
+
|
|
198
|
+
if args.format == "json":
|
|
199
|
+
print(json.dumps(history, ensure_ascii=False, indent=2))
|
|
200
|
+
else:
|
|
201
|
+
print(format_history_text(history))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def cmd_clear(args) -> None:
|
|
205
|
+
result = manager.clear_history()
|
|
206
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ============================================================================
|
|
210
|
+
# Main
|
|
211
|
+
# ============================================================================
|
|
212
|
+
|
|
213
|
+
def main():
|
|
214
|
+
parser = argparse.ArgumentParser(
|
|
215
|
+
prog="sequential_think_cli",
|
|
216
|
+
description="Sequential Think CLI - Iterative thinking engine for complex problem-solving"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
220
|
+
|
|
221
|
+
# think command
|
|
222
|
+
p_think = subparsers.add_parser("think", help="Process a thought in the chain")
|
|
223
|
+
p_think.add_argument("--thought", "-t", required=True, help="Current thinking step content")
|
|
224
|
+
p_think.add_argument("--thought-number", "-n", type=int, required=True, help="Current position (1-based)")
|
|
225
|
+
p_think.add_argument("--total-thoughts", "-T", type=int, required=True, help="Estimated total thoughts")
|
|
226
|
+
p_think.add_argument("--no-next", action="store_true", help="Mark as final thought (no more needed)")
|
|
227
|
+
p_think.add_argument("--is-revision", action="store_true", help="This thought revises previous thinking")
|
|
228
|
+
p_think.add_argument("--revises-thought", type=int, help="Which thought number is being reconsidered")
|
|
229
|
+
p_think.add_argument("--branch-from", type=int, help="Branching point thought number")
|
|
230
|
+
p_think.add_argument("--branch-id", help="Identifier for current branch")
|
|
231
|
+
p_think.add_argument("--needs-more", action="store_true", help="Signal more thoughts needed beyond estimate")
|
|
232
|
+
p_think.add_argument("--quiet", "-q", action="store_true", help="Suppress formatted output to stderr")
|
|
233
|
+
|
|
234
|
+
# history command
|
|
235
|
+
p_history = subparsers.add_parser("history", help="View thought history")
|
|
236
|
+
p_history.add_argument("--format", "-f", choices=["json", "text"], default="text", help="Output format")
|
|
237
|
+
|
|
238
|
+
# clear command
|
|
239
|
+
subparsers.add_parser("clear", help="Clear thought history")
|
|
240
|
+
|
|
241
|
+
args = parser.parse_args()
|
|
242
|
+
|
|
243
|
+
commands = {
|
|
244
|
+
"think": cmd_think,
|
|
245
|
+
"history": cmd_history,
|
|
246
|
+
"clear": cmd_clear,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
commands[args.command](args)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
main()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: time
|
|
3
|
+
description: |
|
|
4
|
+
Time and timezone utilities for getting current time and converting between timezones. Use when: (1) Getting current time in any timezone, (2) Converting time between different timezones, (3) Working with IANA timezone names, (4) Scheduling across timezones, (5) Time-sensitive operations. Triggers: "what time is it", "current time", "convert time", "timezone", "time in [city]". Supports both MCP server and standalone CLI.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Time
|
|
8
|
+
|
|
9
|
+
Time and timezone conversion utilities. Supports both MCP server and standalone CLI.
|
|
10
|
+
|
|
11
|
+
## Execution Methods
|
|
12
|
+
|
|
13
|
+
### Method 1: MCP Tools (if available)
|
|
14
|
+
Use `mcp__time__*` tools directly:
|
|
15
|
+
- `mcp__time__get_current_time` - Get current time in a timezone
|
|
16
|
+
- `mcp__time__convert_time` - Convert time between timezones
|
|
17
|
+
|
|
18
|
+
### Method 2: CLI Script (no MCP dependency)
|
|
19
|
+
Run `scripts/time_cli.py` via Bash:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Prerequisites: pip install pytz (or use Python 3.9+ with zoneinfo)
|
|
23
|
+
|
|
24
|
+
# Get current time in a timezone
|
|
25
|
+
python scripts/time_cli.py get --timezone "Asia/Shanghai"
|
|
26
|
+
python scripts/time_cli.py get --timezone "America/New_York"
|
|
27
|
+
python scripts/time_cli.py get # Uses system timezone
|
|
28
|
+
|
|
29
|
+
# Convert time between timezones
|
|
30
|
+
python scripts/time_cli.py convert \
|
|
31
|
+
--time "16:30" \
|
|
32
|
+
--from "America/New_York" \
|
|
33
|
+
--to "Asia/Tokyo"
|
|
34
|
+
|
|
35
|
+
# List available timezones
|
|
36
|
+
python scripts/time_cli.py list [--filter "Asia"]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Tool Capability Matrix
|
|
40
|
+
|
|
41
|
+
| Tool | Parameters | Output |
|
|
42
|
+
|------|------------|--------|
|
|
43
|
+
| `get_current_time` | `timezone` (required, IANA name) | `{timezone, datetime, is_dst}` |
|
|
44
|
+
| `convert_time` | `source_timezone`, `time` (HH:MM), `target_timezone` | `{source, target, time_difference}` |
|
|
45
|
+
|
|
46
|
+
## Common IANA Timezone Names
|
|
47
|
+
|
|
48
|
+
| Region | Timezone |
|
|
49
|
+
|--------|----------|
|
|
50
|
+
| China | `Asia/Shanghai` |
|
|
51
|
+
| Japan | `Asia/Tokyo` |
|
|
52
|
+
| Korea | `Asia/Seoul` |
|
|
53
|
+
| US East | `America/New_York` |
|
|
54
|
+
| US West | `America/Los_Angeles` |
|
|
55
|
+
| UK | `Europe/London` |
|
|
56
|
+
| Germany | `Europe/Berlin` |
|
|
57
|
+
| France | `Europe/Paris` |
|
|
58
|
+
| Australia | `Australia/Sydney` |
|
|
59
|
+
| UTC | `UTC` |
|
|
60
|
+
|
|
61
|
+
## Workflow
|
|
62
|
+
|
|
63
|
+
### Getting Current Time
|
|
64
|
+
1. Identify target timezone (use IANA name)
|
|
65
|
+
2. Call `get_current_time` with timezone parameter
|
|
66
|
+
3. Response includes ISO 8601 datetime and DST status
|
|
67
|
+
|
|
68
|
+
### Converting Time
|
|
69
|
+
1. Identify source timezone and time (24-hour format HH:MM)
|
|
70
|
+
2. Identify target timezone
|
|
71
|
+
3. Call `convert_time` with all parameters
|
|
72
|
+
4. Response includes both times and time difference
|
|
73
|
+
|
|
74
|
+
## Output Format
|
|
75
|
+
|
|
76
|
+
### get_current_time Response
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"timezone": "Asia/Shanghai",
|
|
80
|
+
"datetime": "2024-01-01T21:00:00+08:00",
|
|
81
|
+
"is_dst": false
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### convert_time Response
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"source": {
|
|
89
|
+
"timezone": "America/New_York",
|
|
90
|
+
"datetime": "2024-01-01T16:30:00-05:00",
|
|
91
|
+
"is_dst": false
|
|
92
|
+
},
|
|
93
|
+
"target": {
|
|
94
|
+
"timezone": "Asia/Tokyo",
|
|
95
|
+
"datetime": "2024-01-02T06:30:00+09:00",
|
|
96
|
+
"is_dst": false
|
|
97
|
+
},
|
|
98
|
+
"time_difference": "+14.0h"
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Error Handling
|
|
103
|
+
|
|
104
|
+
| Error | Recovery |
|
|
105
|
+
|-------|----------|
|
|
106
|
+
| Invalid timezone | Check IANA timezone name spelling |
|
|
107
|
+
| Invalid time format | Use 24-hour format HH:MM |
|
|
108
|
+
| MCP unavailable | Fall back to CLI script |
|
|
109
|
+
|
|
110
|
+
## Anti-Patterns
|
|
111
|
+
|
|
112
|
+
| Prohibited | Correct |
|
|
113
|
+
|------------|---------|
|
|
114
|
+
| Use city names directly | Use IANA timezone names (e.g., `Asia/Tokyo` not `Tokyo`) |
|
|
115
|
+
| Use 12-hour format | Use 24-hour format (e.g., `16:30` not `4:30 PM`) |
|
|
116
|
+
| Assume timezone | Always specify timezone explicitly |
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Time CLI - Standalone time and timezone utilities."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from zoneinfo import ZoneInfo, available_timezones
|
|
11
|
+
except ImportError:
|
|
12
|
+
from pytz import timezone as ZoneInfo, all_timezones as _all_tz
|
|
13
|
+
def available_timezones():
|
|
14
|
+
return set(_all_tz)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_current_time(tz_name: str) -> dict:
|
|
18
|
+
"""Get current time in specified timezone."""
|
|
19
|
+
try:
|
|
20
|
+
tz = ZoneInfo(tz_name)
|
|
21
|
+
now = datetime.now(tz)
|
|
22
|
+
return {
|
|
23
|
+
"timezone": tz_name,
|
|
24
|
+
"datetime": now.isoformat(),
|
|
25
|
+
"is_dst": bool(now.dst()) if hasattr(now, 'dst') and now.dst() else False
|
|
26
|
+
}
|
|
27
|
+
except Exception as e:
|
|
28
|
+
return {"error": str(e)}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def convert_time(source_tz: str, time_str: str, target_tz: str) -> dict:
|
|
32
|
+
"""Convert time between timezones."""
|
|
33
|
+
try:
|
|
34
|
+
hour, minute = map(int, time_str.split(':'))
|
|
35
|
+
source = ZoneInfo(source_tz)
|
|
36
|
+
target = ZoneInfo(target_tz)
|
|
37
|
+
|
|
38
|
+
today = datetime.now(source).date()
|
|
39
|
+
source_dt = datetime(today.year, today.month, today.day, hour, minute, tzinfo=source)
|
|
40
|
+
target_dt = source_dt.astimezone(target)
|
|
41
|
+
|
|
42
|
+
source_offset = source_dt.utcoffset().total_seconds() / 3600
|
|
43
|
+
target_offset = target_dt.utcoffset().total_seconds() / 3600
|
|
44
|
+
diff = target_offset - source_offset
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
"source": {
|
|
48
|
+
"timezone": source_tz,
|
|
49
|
+
"datetime": source_dt.isoformat(),
|
|
50
|
+
"is_dst": bool(source_dt.dst()) if hasattr(source_dt, 'dst') and source_dt.dst() else False
|
|
51
|
+
},
|
|
52
|
+
"target": {
|
|
53
|
+
"timezone": target_tz,
|
|
54
|
+
"datetime": target_dt.isoformat(),
|
|
55
|
+
"is_dst": bool(target_dt.dst()) if hasattr(target_dt, 'dst') and target_dt.dst() else False
|
|
56
|
+
},
|
|
57
|
+
"time_difference": f"{diff:+.1f}h"
|
|
58
|
+
}
|
|
59
|
+
except Exception as e:
|
|
60
|
+
return {"error": str(e)}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def list_timezones(filter_str: str = None) -> list:
|
|
64
|
+
"""List available timezones."""
|
|
65
|
+
zones = sorted(available_timezones())
|
|
66
|
+
if filter_str:
|
|
67
|
+
zones = [z for z in zones if filter_str.lower() in z.lower()]
|
|
68
|
+
return zones
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main():
|
|
72
|
+
parser = argparse.ArgumentParser(description="Time and timezone utilities")
|
|
73
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
74
|
+
|
|
75
|
+
# get command
|
|
76
|
+
get_parser = subparsers.add_parser("get", help="Get current time")
|
|
77
|
+
get_parser.add_argument("--timezone", "-tz", default="UTC", help="IANA timezone name")
|
|
78
|
+
|
|
79
|
+
# convert command
|
|
80
|
+
convert_parser = subparsers.add_parser("convert", help="Convert time between timezones")
|
|
81
|
+
convert_parser.add_argument("--time", "-t", required=True, help="Time in HH:MM format")
|
|
82
|
+
convert_parser.add_argument("--from", "-f", dest="source", required=True, help="Source timezone")
|
|
83
|
+
convert_parser.add_argument("--to", "-o", dest="target", required=True, help="Target timezone")
|
|
84
|
+
|
|
85
|
+
# list command
|
|
86
|
+
list_parser = subparsers.add_parser("list", help="List available timezones")
|
|
87
|
+
list_parser.add_argument("--filter", "-f", help="Filter timezones by substring")
|
|
88
|
+
|
|
89
|
+
args = parser.parse_args()
|
|
90
|
+
|
|
91
|
+
if args.command == "get":
|
|
92
|
+
result = get_current_time(args.timezone)
|
|
93
|
+
print(json.dumps(result, indent=2))
|
|
94
|
+
elif args.command == "convert":
|
|
95
|
+
result = convert_time(args.source, args.time, args.target)
|
|
96
|
+
print(json.dumps(result, indent=2))
|
|
97
|
+
elif args.command == "list":
|
|
98
|
+
zones = list_timezones(args.filter)
|
|
99
|
+
for z in zones:
|
|
100
|
+
print(z)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
main()
|