abelworkflow 1.2.3 → 1.3.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/README.md +26 -25
- package/extensions/gpt-responses-compat.ts +166 -27
- package/lib/cli/main.mjs +18 -86
- package/lib/cli/pi.mjs +43 -0
- package/lib/cli/prompts.mjs +3 -3
- package/lib/installer/assets.mjs +35 -31
- package/lib/installer/install.mjs +29 -9
- package/lib/installer/links.mjs +97 -84
- package/lib/paths.mjs +0 -2
- package/lib/providers/claude.mjs +14 -3
- package/lib/providers/codex.mjs +14 -9
- package/lib/providers/pi.mjs +61 -68
- package/lib/providers/skills.mjs +22 -22
- package/lib/providers/url.mjs +32 -1
- package/lib/templates/codex/agents/default.toml +16 -54
- package/lib/templates/codex/agents/explorer.toml +25 -52
- package/lib/templates/codex/agents/planner.toml +23 -77
- package/lib/templates/codex/agents/reviewer.toml +16 -62
- package/lib/templates/codex/agents/worker.toml +30 -64
- package/lib/templates/codex/config-base.toml +0 -6
- package/lib/templates/{workflow/gitignore.template → gitignore.template} +0 -3
- package/lib/tools/cli-installer.mjs +40 -12
- package/package.json +13 -18
- package/lib/templates/workflow/AGENTS.md +0 -50
- package/lib/templates/workflow/commands/abel-design.md +0 -175
- package/lib/templates/workflow/commands/abel-diagnose.md +0 -63
- package/lib/templates/workflow/commands/abel-implement.md +0 -170
- package/lib/templates/workflow/commands/abel-init.md +0 -25
- package/skills/dev-browser/SKILL.md +0 -281
- package/skills/dev-browser/dist/scripts/start.d.ts +0 -1
- package/skills/dev-browser/dist/scripts/start.js +0 -90
- package/skills/dev-browser/dist/src/client.d.ts +0 -92
- package/skills/dev-browser/dist/src/client.js +0 -310
- package/skills/dev-browser/dist/src/entrypoint.d.ts +0 -29
- package/skills/dev-browser/dist/src/entrypoint.js +0 -113
- package/skills/dev-browser/dist/src/index.d.ts +0 -3
- package/skills/dev-browser/dist/src/index.js +0 -1
- package/skills/dev-browser/dist/src/page-api.d.ts +0 -24
- package/skills/dev-browser/dist/src/page-api.js +0 -103
- package/skills/dev-browser/dist/src/relay.d.ts +0 -26
- package/skills/dev-browser/dist/src/relay.js +0 -567
- package/skills/dev-browser/dist/src/runtime.d.ts +0 -34
- package/skills/dev-browser/dist/src/runtime.js +0 -44
- package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts +0 -22
- package/skills/dev-browser/dist/src/snapshot/browser-script.js +0 -868
- package/skills/dev-browser/dist/src/snapshot/index.d.ts +0 -13
- package/skills/dev-browser/dist/src/snapshot/index.js +0 -13
- package/skills/dev-browser/dist/src/snapshot/inject.d.ts +0 -12
- package/skills/dev-browser/dist/src/snapshot/inject.js +0 -12
- package/skills/dev-browser/dist/src/standalone.d.ts +0 -31
- package/skills/dev-browser/dist/src/standalone.js +0 -173
- package/skills/dev-browser/dist/src/startup.d.ts +0 -46
- package/skills/dev-browser/dist/src/startup.js +0 -77
- package/skills/dev-browser/dist/src/target-registry.d.ts +0 -28
- package/skills/dev-browser/dist/src/target-registry.js +0 -134
- package/skills/dev-browser/dist/src/types.d.ts +0 -26
- package/skills/dev-browser/dist/src/types.js +0 -1
- package/skills/dev-browser/package-lock.json +0 -1545
- package/skills/dev-browser/package.json +0 -35
- package/skills/dev-browser/references/scraping.md +0 -144
- package/skills/git-commit/SKILL.md +0 -124
- package/skills/time/SKILL.md +0 -119
- package/skills/time/scripts/time_cli.py +0 -143
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dev-browser",
|
|
3
|
-
"version": "0.0.1",
|
|
4
|
-
"type": "module",
|
|
5
|
-
"engines": {
|
|
6
|
-
"node": ">=22"
|
|
7
|
-
},
|
|
8
|
-
"packageManager": "npm@10.9.2",
|
|
9
|
-
"exports": {
|
|
10
|
-
"./client": {
|
|
11
|
-
"types": "./dist/src/client.d.ts",
|
|
12
|
-
"import": "./dist/src/client.js"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
17
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
18
|
-
"start": "node dist/scripts/start.js standalone",
|
|
19
|
-
"start-server": "node dist/scripts/start.js standalone",
|
|
20
|
-
"start-extension": "node dist/scripts/start.js extension",
|
|
21
|
-
"test": "vitest run",
|
|
22
|
-
"test:watch": "vitest"
|
|
23
|
-
},
|
|
24
|
-
"dependencies": {
|
|
25
|
-
"@hono/node-server": "^1.19.13",
|
|
26
|
-
"@hono/node-ws": "^1.3.1",
|
|
27
|
-
"hono": "^4.12.30",
|
|
28
|
-
"playwright": "^1.56.0"
|
|
29
|
-
},
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@types/node": "^22.0.0",
|
|
32
|
-
"typescript": "^5.0.0",
|
|
33
|
-
"vitest": "^2.1.0"
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
# Data Scraping Guide
|
|
2
|
-
|
|
3
|
-
For large datasets (followers, posts, search results), **intercept and replay network requests** rather than scrolling and parsing the DOM. This is faster, more reliable, and handles pagination automatically.
|
|
4
|
-
|
|
5
|
-
## Why Not Scroll?
|
|
6
|
-
|
|
7
|
-
Scrolling is slow, unreliable, and wastes time. APIs return structured data with pagination built in. Always prefer API replay.
|
|
8
|
-
|
|
9
|
-
## Start Small, Then Scale
|
|
10
|
-
|
|
11
|
-
**Don't try to automate everything at once.** Work incrementally:
|
|
12
|
-
|
|
13
|
-
1. **Capture one request** - verify you're intercepting the right endpoint
|
|
14
|
-
2. **Inspect one response** - understand the schema before writing extraction code
|
|
15
|
-
3. **Extract a few items** - make sure your parsing logic works
|
|
16
|
-
4. **Then scale up** - add pagination loop only after the basics work
|
|
17
|
-
|
|
18
|
-
This prevents wasting time debugging a complex script when the issue is a simple path like `data.user.timeline` vs `data.user.result.timeline`.
|
|
19
|
-
|
|
20
|
-
## External Script Setup
|
|
21
|
-
|
|
22
|
-
Keep the installed skill directory read-only. Put scripts and temporary artifacts in the OS temporary directory or the task workspace, and remove them when the task finishes. External scripts receive the installed skill directory as their first argument and dynamically import the compiled client by file URL.
|
|
23
|
-
|
|
24
|
-
Never persist complete authentication headers. Keep captured headers in memory, and write only redacted metadata or final non-sensitive results.
|
|
25
|
-
|
|
26
|
-
```javascript
|
|
27
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
28
|
-
import { tmpdir } from "node:os";
|
|
29
|
-
import { join } from "node:path";
|
|
30
|
-
import { pathToFileURL } from "node:url";
|
|
31
|
-
|
|
32
|
-
const skillDir = process.argv[2];
|
|
33
|
-
if (!skillDir) throw new Error("Pass the installed dev-browser skill directory");
|
|
34
|
-
const { connect, waitForPageLoad } = await import(
|
|
35
|
-
pathToFileURL(join(skillDir, "dist", "src", "client.js")).href
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
const workDir = await mkdtemp(join(tmpdir(), "dev-browser-scrape-"));
|
|
39
|
-
let client;
|
|
40
|
-
try {
|
|
41
|
-
client = await connect();
|
|
42
|
-
const page = await client.page("site");
|
|
43
|
-
let capturedRequest;
|
|
44
|
-
|
|
45
|
-
page.on("request", (request) => {
|
|
46
|
-
const url = request.url();
|
|
47
|
-
if (url.includes("/api/") || url.includes("/graphql/")) {
|
|
48
|
-
capturedRequest = {
|
|
49
|
-
url,
|
|
50
|
-
method: request.method(),
|
|
51
|
-
headers: request.headers(),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
await page.goto("https://example.com/profile");
|
|
57
|
-
await waitForPageLoad(page);
|
|
58
|
-
await page.waitForTimeout(3000);
|
|
59
|
-
|
|
60
|
-
if (!capturedRequest) throw new Error("No matching API request captured");
|
|
61
|
-
const capturedUrl = new URL(capturedRequest.url);
|
|
62
|
-
console.log({
|
|
63
|
-
url: `${capturedUrl.origin}${capturedUrl.pathname}`,
|
|
64
|
-
method: capturedRequest.method,
|
|
65
|
-
headerNames: Object.keys(capturedRequest.headers),
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const results = new Map();
|
|
69
|
-
let cursor;
|
|
70
|
-
do {
|
|
71
|
-
const params = { count: 20, ...(cursor ? { cursor } : {}) };
|
|
72
|
-
const url = new URL(capturedRequest.url);
|
|
73
|
-
url.searchParams.set("params", JSON.stringify(params));
|
|
74
|
-
const response = await page.evaluate(
|
|
75
|
-
async ({ url, headers }) => {
|
|
76
|
-
const result = await fetch(url, { headers });
|
|
77
|
-
return result.json();
|
|
78
|
-
},
|
|
79
|
-
{ url: url.href, headers: capturedRequest.headers }
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const entries = response?.data?.entries ?? [];
|
|
83
|
-
cursor = undefined;
|
|
84
|
-
for (const entry of entries) {
|
|
85
|
-
if (entry.type === "cursor-bottom") cursor = entry.value;
|
|
86
|
-
else if (entry.id) results.set(entry.id, entry);
|
|
87
|
-
}
|
|
88
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
89
|
-
} while (cursor);
|
|
90
|
-
|
|
91
|
-
const resultPath = join(workDir, "results.json");
|
|
92
|
-
await writeFile(resultPath, JSON.stringify([...results.values()], null, 2));
|
|
93
|
-
console.log({ count: results.size, resultPath });
|
|
94
|
-
} finally {
|
|
95
|
-
try {
|
|
96
|
-
await client?.disconnect();
|
|
97
|
-
} finally {
|
|
98
|
-
await rm(workDir, { recursive: true, force: true });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Run it with quoted paths on Linux, PowerShell, or Command Prompt:
|
|
104
|
-
|
|
105
|
-
```text
|
|
106
|
-
node "PATH_TO_SCRIPT.mjs" "PATH_TO_INSTALLED_DEV_BROWSER"
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
If a result must survive the script, write the sanitized deliverable to the task workspace instead of `workDir`, then delete it at task completion unless the user asks to retain it.
|
|
110
|
-
|
|
111
|
-
## Step-by-Step Workflow
|
|
112
|
-
|
|
113
|
-
### 1. Capture One Request
|
|
114
|
-
|
|
115
|
-
Match a narrow API or GraphQL URL pattern. Keep the request URL and headers in memory. Log only the URL origin/path, method, and header names while confirming the endpoint; query parameters can contain credentials.
|
|
116
|
-
|
|
117
|
-
### 2. Inspect One Response
|
|
118
|
-
|
|
119
|
-
Inspect the response in memory before building pagination. Find:
|
|
120
|
-
|
|
121
|
-
- Where the data array lives (e.g., `data.user.result.timeline.instructions[].entries`)
|
|
122
|
-
- Where pagination cursors are (e.g., `cursor-bottom` entries)
|
|
123
|
-
- What fields you need to extract
|
|
124
|
-
|
|
125
|
-
### 3. Replay API with Pagination
|
|
126
|
-
|
|
127
|
-
Replay through `page.evaluate(fetch)` so the browser supplies its session cookies. Pass only the in-memory headers required by the endpoint, deduplicate with a `Map`, stop when the cursor or results end, and rate-limit requests.
|
|
128
|
-
|
|
129
|
-
## Key Patterns
|
|
130
|
-
|
|
131
|
-
| Pattern | Description |
|
|
132
|
-
| ----------------------- | ------------------------------------------------------ |
|
|
133
|
-
| `page.on('request')` | Capture outgoing request URL + headers |
|
|
134
|
-
| `page.on('response')` | Capture response data to understand schema |
|
|
135
|
-
| `page.evaluate(fetch)` | Replay requests in browser context (inherits auth) |
|
|
136
|
-
| `Map` for deduplication | APIs often return overlapping data across pages |
|
|
137
|
-
| Cursor-based pagination | Look for `cursor`, `next_token`, `offset` in responses |
|
|
138
|
-
|
|
139
|
-
## Tips
|
|
140
|
-
|
|
141
|
-
- **Extension mode**: `page.context().cookies()` doesn't work - keep intercepted auth headers in memory instead
|
|
142
|
-
- **Rate limiting**: Add 500ms+ delays between requests to avoid blocks
|
|
143
|
-
- **Stop conditions**: Check for empty results, missing cursor, or reaching a date/ID threshold
|
|
144
|
-
- **GraphQL APIs**: URL params often include `variables` and `features` JSON objects - capture and reuse them
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: git-commit
|
|
3
|
-
description: 'Execute git commit with conventional commit message analysis, intelligent staging, and message generation. Use when user asks to commit changes, create a git commit, or mentions "/commit". Supports: (1) Auto-detecting type and scope from changes, (2) Generating conventional commit messages from diff, (3) Interactive commit with optional type/scope/description overrides, (4) Intelligent file staging for logical grouping'
|
|
4
|
-
license: MIT
|
|
5
|
-
allowed-tools: Bash
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Git Commit with Conventional Commits
|
|
9
|
-
|
|
10
|
-
## Overview
|
|
11
|
-
|
|
12
|
-
Create standardized, semantic git commits using the Conventional Commits specification. Analyze the actual diff to determine appropriate type, scope, and message.
|
|
13
|
-
|
|
14
|
-
## Conventional Commit Format
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
<type>[optional scope]: <description>
|
|
18
|
-
|
|
19
|
-
[optional body]
|
|
20
|
-
|
|
21
|
-
[optional footer(s)]
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Commit Types
|
|
25
|
-
|
|
26
|
-
| Type | Purpose |
|
|
27
|
-
| ---------- | ------------------------------ |
|
|
28
|
-
| `feat` | New feature |
|
|
29
|
-
| `fix` | Bug fix |
|
|
30
|
-
| `docs` | Documentation only |
|
|
31
|
-
| `style` | Formatting/style (no logic) |
|
|
32
|
-
| `refactor` | Code refactor (no feature/fix) |
|
|
33
|
-
| `perf` | Performance improvement |
|
|
34
|
-
| `test` | Add/update tests |
|
|
35
|
-
| `build` | Build system/dependencies |
|
|
36
|
-
| `ci` | CI/config changes |
|
|
37
|
-
| `chore` | Maintenance/misc |
|
|
38
|
-
| `revert` | Revert commit |
|
|
39
|
-
|
|
40
|
-
## Breaking Changes
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
# Exclamation mark after type/scope
|
|
44
|
-
feat!: remove deprecated endpoint
|
|
45
|
-
|
|
46
|
-
# BREAKING CHANGE footer
|
|
47
|
-
feat: allow config to extend other configs
|
|
48
|
-
|
|
49
|
-
BREAKING CHANGE: `extends` key behavior changed
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Workflow
|
|
53
|
-
|
|
54
|
-
### 1. Analyze Diff
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
# If files are staged, use staged diff
|
|
58
|
-
git diff --staged
|
|
59
|
-
|
|
60
|
-
# If nothing staged, use working tree diff
|
|
61
|
-
git diff
|
|
62
|
-
|
|
63
|
-
# Also check status
|
|
64
|
-
git status --porcelain
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### 2. Stage Files (if needed)
|
|
68
|
-
|
|
69
|
-
If nothing is staged or you want to group changes differently:
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
# Stage specific files
|
|
73
|
-
git add path/to/file1 path/to/file2
|
|
74
|
-
|
|
75
|
-
# Stage by pattern
|
|
76
|
-
git add *.test.*
|
|
77
|
-
git add src/components/*
|
|
78
|
-
|
|
79
|
-
# Interactive staging
|
|
80
|
-
git add -p
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
**Never commit secrets** (.env, credentials.json, private keys).
|
|
84
|
-
|
|
85
|
-
### 3. Generate Commit Message
|
|
86
|
-
|
|
87
|
-
Analyze the diff to determine:
|
|
88
|
-
|
|
89
|
-
- **Type**: What kind of change is this?
|
|
90
|
-
- **Scope**: What area/module is affected?
|
|
91
|
-
- **Description**: One-line summary of what changed (present tense, imperative mood, <72 chars)
|
|
92
|
-
|
|
93
|
-
### 4. Execute Commit
|
|
94
|
-
|
|
95
|
-
```bash
|
|
96
|
-
# Single line
|
|
97
|
-
git commit -m "<type>[scope]: <description>"
|
|
98
|
-
|
|
99
|
-
# Multi-line with body/footer
|
|
100
|
-
git commit -m "$(cat <<'EOF'
|
|
101
|
-
<type>[scope]: <description>
|
|
102
|
-
|
|
103
|
-
<optional body>
|
|
104
|
-
|
|
105
|
-
<optional footer>
|
|
106
|
-
EOF
|
|
107
|
-
)"
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Best Practices
|
|
111
|
-
|
|
112
|
-
- One logical change per commit
|
|
113
|
-
- Present tense: "add" not "added"
|
|
114
|
-
- Imperative mood: "fix bug" not "fixes bug"
|
|
115
|
-
- Reference issues: `Closes #123`, `Refs #456`
|
|
116
|
-
- Keep description under 72 characters
|
|
117
|
-
|
|
118
|
-
## Git Safety Protocol
|
|
119
|
-
|
|
120
|
-
- NEVER update git config
|
|
121
|
-
- NEVER run destructive commands (--force, hard reset) without explicit request
|
|
122
|
-
- NEVER skip hooks (--no-verify) unless user asks
|
|
123
|
-
- NEVER force push to main/master
|
|
124
|
-
- If commit fails due to hooks, fix and create NEW commit (don't amend)
|
package/skills/time/SKILL.md
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
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
|
-
# Prerequisite: Python 3.9+
|
|
23
|
-
# Windows also requires IANA data: python -m pip install tzdata
|
|
24
|
-
|
|
25
|
-
# Get current time in a timezone
|
|
26
|
-
python scripts/time_cli.py get --timezone "Asia/Shanghai"
|
|
27
|
-
python scripts/time_cli.py get --timezone "America/New_York"
|
|
28
|
-
python scripts/time_cli.py get # Uses UTC
|
|
29
|
-
|
|
30
|
-
# Convert time between timezones
|
|
31
|
-
python scripts/time_cli.py convert \
|
|
32
|
-
--time "16:30" \
|
|
33
|
-
--from "America/New_York" \
|
|
34
|
-
--to "Asia/Tokyo"
|
|
35
|
-
|
|
36
|
-
# List available timezones
|
|
37
|
-
python scripts/time_cli.py list [--filter "Asia"]
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Tool Capability Matrix
|
|
41
|
-
|
|
42
|
-
| Tool | Parameters | Output |
|
|
43
|
-
|------|------------|--------|
|
|
44
|
-
| `get_current_time` | `timezone` (required, IANA name) | `{timezone, datetime, is_dst}` |
|
|
45
|
-
| `convert_time` | `source_timezone`, `time` (HH:MM), `target_timezone` | `{source, target, time_difference}` |
|
|
46
|
-
|
|
47
|
-
## Common IANA Timezone Names
|
|
48
|
-
|
|
49
|
-
| Region | Timezone |
|
|
50
|
-
|--------|----------|
|
|
51
|
-
| China | `Asia/Shanghai` |
|
|
52
|
-
| Japan | `Asia/Tokyo` |
|
|
53
|
-
| Korea | `Asia/Seoul` |
|
|
54
|
-
| US East | `America/New_York` |
|
|
55
|
-
| US West | `America/Los_Angeles` |
|
|
56
|
-
| UK | `Europe/London` |
|
|
57
|
-
| Germany | `Europe/Berlin` |
|
|
58
|
-
| France | `Europe/Paris` |
|
|
59
|
-
| Australia | `Australia/Sydney` |
|
|
60
|
-
| UTC | `UTC` |
|
|
61
|
-
|
|
62
|
-
## Workflow
|
|
63
|
-
|
|
64
|
-
### Getting Current Time
|
|
65
|
-
1. Identify target timezone (use IANA name)
|
|
66
|
-
2. Call `get_current_time` with timezone parameter
|
|
67
|
-
3. Response includes ISO 8601 datetime and DST status
|
|
68
|
-
|
|
69
|
-
### Converting Time
|
|
70
|
-
1. Identify source timezone and time (24-hour format HH:MM)
|
|
71
|
-
2. Identify target timezone
|
|
72
|
-
3. Call `convert_time` with all parameters
|
|
73
|
-
4. Response includes both times and time difference
|
|
74
|
-
|
|
75
|
-
## Output Format
|
|
76
|
-
|
|
77
|
-
### get_current_time Response
|
|
78
|
-
```json
|
|
79
|
-
{
|
|
80
|
-
"timezone": "Asia/Shanghai",
|
|
81
|
-
"datetime": "2024-01-01T21:00:00+08:00",
|
|
82
|
-
"is_dst": false
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### convert_time Response
|
|
87
|
-
```json
|
|
88
|
-
{
|
|
89
|
-
"source": {
|
|
90
|
-
"timezone": "America/New_York",
|
|
91
|
-
"datetime": "2024-01-01T16:30:00-05:00",
|
|
92
|
-
"is_dst": false
|
|
93
|
-
},
|
|
94
|
-
"target": {
|
|
95
|
-
"timezone": "Asia/Tokyo",
|
|
96
|
-
"datetime": "2024-01-02T06:30:00+09:00",
|
|
97
|
-
"is_dst": false
|
|
98
|
-
},
|
|
99
|
-
"time_difference": "+14.0h"
|
|
100
|
-
}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
## Error Handling
|
|
104
|
-
|
|
105
|
-
| Error | Recovery |
|
|
106
|
-
|-------|----------|
|
|
107
|
-
| Invalid timezone | Check IANA timezone name spelling |
|
|
108
|
-
| Invalid time format | Use 24-hour format HH:MM |
|
|
109
|
-
| Nonexistent or ambiguous DST time | Choose an unambiguous local time |
|
|
110
|
-
| Missing timezone data on Windows | Run `python -m pip install tzdata` |
|
|
111
|
-
| MCP unavailable | Fall back to CLI script |
|
|
112
|
-
|
|
113
|
-
## Anti-Patterns
|
|
114
|
-
|
|
115
|
-
| Prohibited | Correct |
|
|
116
|
-
|------------|---------|
|
|
117
|
-
| Use city names directly | Use IANA timezone names (e.g., `Asia/Tokyo` not `Tokyo`) |
|
|
118
|
-
| Use 12-hour format | Use 24-hour format (e.g., `16:30` not `4:30 PM`) |
|
|
119
|
-
| Assume timezone | Always specify timezone explicitly |
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Time CLI - Standalone time and timezone utilities."""
|
|
3
|
-
|
|
4
|
-
import argparse
|
|
5
|
-
import json
|
|
6
|
-
import re
|
|
7
|
-
import sys
|
|
8
|
-
from datetime import datetime, timezone
|
|
9
|
-
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError, available_timezones
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def load_timezone(tz_name: str) -> ZoneInfo:
|
|
13
|
-
try:
|
|
14
|
-
return ZoneInfo(tz_name)
|
|
15
|
-
except ZoneInfoNotFoundError as error:
|
|
16
|
-
try:
|
|
17
|
-
ZoneInfo("UTC")
|
|
18
|
-
except ZoneInfoNotFoundError:
|
|
19
|
-
raise ValueError(
|
|
20
|
-
"IANA timezone data is unavailable; on Windows install it with: "
|
|
21
|
-
"python -m pip install tzdata"
|
|
22
|
-
) from error
|
|
23
|
-
raise ValueError(f"Unknown IANA timezone: {tz_name}") from error
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def localize_time(source: ZoneInfo, year: int, month: int, day: int, hour: int, minute: int) -> datetime:
|
|
27
|
-
naive = datetime(year, month, day, hour, minute)
|
|
28
|
-
candidates = []
|
|
29
|
-
for fold in (0, 1):
|
|
30
|
-
candidate = naive.replace(tzinfo=source, fold=fold)
|
|
31
|
-
round_trip = candidate.astimezone(timezone.utc).astimezone(source)
|
|
32
|
-
if round_trip.replace(tzinfo=None) == naive:
|
|
33
|
-
candidates.append(candidate)
|
|
34
|
-
|
|
35
|
-
if not candidates:
|
|
36
|
-
raise ValueError("local time is nonexistent because of a DST transition")
|
|
37
|
-
if len({candidate.utcoffset() for candidate in candidates}) > 1:
|
|
38
|
-
raise ValueError("local time is ambiguous because of a DST transition")
|
|
39
|
-
return candidates[0]
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def get_current_time(tz_name: str) -> dict:
|
|
43
|
-
"""Get current time in specified timezone."""
|
|
44
|
-
try:
|
|
45
|
-
tz = load_timezone(tz_name)
|
|
46
|
-
now = datetime.now(tz)
|
|
47
|
-
return {
|
|
48
|
-
"timezone": tz_name,
|
|
49
|
-
"datetime": now.isoformat(),
|
|
50
|
-
"is_dst": bool(now.dst()) if hasattr(now, 'dst') and now.dst() else False
|
|
51
|
-
}
|
|
52
|
-
except Exception as e:
|
|
53
|
-
return {"error": str(e)}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def convert_time(source_tz: str, time_str: str, target_tz: str) -> dict:
|
|
57
|
-
"""Convert time between timezones."""
|
|
58
|
-
try:
|
|
59
|
-
if not re.fullmatch(r"(?:[01]\d|2[0-3]):[0-5]\d", time_str):
|
|
60
|
-
raise ValueError("time must use 24-hour HH:MM format")
|
|
61
|
-
hour, minute = map(int, time_str.split(':'))
|
|
62
|
-
source = load_timezone(source_tz)
|
|
63
|
-
target = load_timezone(target_tz)
|
|
64
|
-
|
|
65
|
-
today = datetime.now(source).date()
|
|
66
|
-
source_dt = localize_time(source, today.year, today.month, today.day, hour, minute)
|
|
67
|
-
target_dt = source_dt.astimezone(target)
|
|
68
|
-
|
|
69
|
-
source_offset = source_dt.utcoffset().total_seconds() / 3600
|
|
70
|
-
target_offset = target_dt.utcoffset().total_seconds() / 3600
|
|
71
|
-
diff = target_offset - source_offset
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
"source": {
|
|
75
|
-
"timezone": source_tz,
|
|
76
|
-
"datetime": source_dt.isoformat(),
|
|
77
|
-
"is_dst": bool(source_dt.dst()) if hasattr(source_dt, 'dst') and source_dt.dst() else False
|
|
78
|
-
},
|
|
79
|
-
"target": {
|
|
80
|
-
"timezone": target_tz,
|
|
81
|
-
"datetime": target_dt.isoformat(),
|
|
82
|
-
"is_dst": bool(target_dt.dst()) if hasattr(target_dt, 'dst') and target_dt.dst() else False
|
|
83
|
-
},
|
|
84
|
-
"time_difference": f"{diff:+.1f}h"
|
|
85
|
-
}
|
|
86
|
-
except Exception as e:
|
|
87
|
-
return {"error": str(e)}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def list_timezones(filter_str: str = None) -> list:
|
|
91
|
-
"""List available timezones."""
|
|
92
|
-
zones = sorted(available_timezones())
|
|
93
|
-
if not zones:
|
|
94
|
-
load_timezone("UTC")
|
|
95
|
-
if filter_str:
|
|
96
|
-
zones = [z for z in zones if filter_str.lower() in z.lower()]
|
|
97
|
-
return zones
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def print_result(result: dict) -> int:
|
|
101
|
-
if "error" in result:
|
|
102
|
-
print(json.dumps(result, indent=2), file=sys.stderr)
|
|
103
|
-
return 1
|
|
104
|
-
print(json.dumps(result, indent=2))
|
|
105
|
-
return 0
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def main() -> int:
|
|
109
|
-
parser = argparse.ArgumentParser(description="Time and timezone utilities")
|
|
110
|
-
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
111
|
-
|
|
112
|
-
# get command
|
|
113
|
-
get_parser = subparsers.add_parser("get", help="Get current time")
|
|
114
|
-
get_parser.add_argument("--timezone", "-tz", default="UTC", help="IANA timezone name")
|
|
115
|
-
|
|
116
|
-
# convert command
|
|
117
|
-
convert_parser = subparsers.add_parser("convert", help="Convert time between timezones")
|
|
118
|
-
convert_parser.add_argument("--time", "-t", required=True, help="Time in HH:MM format")
|
|
119
|
-
convert_parser.add_argument("--from", "-f", dest="source", required=True, help="Source timezone")
|
|
120
|
-
convert_parser.add_argument("--to", "-o", dest="target", required=True, help="Target timezone")
|
|
121
|
-
|
|
122
|
-
# list command
|
|
123
|
-
list_parser = subparsers.add_parser("list", help="List available timezones")
|
|
124
|
-
list_parser.add_argument("--filter", "-f", help="Filter timezones by substring")
|
|
125
|
-
|
|
126
|
-
args = parser.parse_args()
|
|
127
|
-
|
|
128
|
-
if args.command == "get":
|
|
129
|
-
return print_result(get_current_time(args.timezone))
|
|
130
|
-
if args.command == "convert":
|
|
131
|
-
return print_result(convert_time(args.source, args.time, args.target))
|
|
132
|
-
if args.command == "list":
|
|
133
|
-
try:
|
|
134
|
-
for z in list_timezones(args.filter):
|
|
135
|
-
print(z)
|
|
136
|
-
return 0
|
|
137
|
-
except Exception as error:
|
|
138
|
-
return print_result({"error": str(error)})
|
|
139
|
-
return 1
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if __name__ == "__main__":
|
|
143
|
-
sys.exit(main())
|