@acedatacloud/skills 2026.703.2 → 2026.703.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/README.md +3 -1
- package/package.json +1 -1
- package/skills/apple-notes/SKILL.md +126 -0
- package/skills/apple-notes/scripts/notes.py +572 -0
- package/skills/didi-ride/SKILL.md +139 -0
- package/skills/didi-ride/references/api_references.md +97 -0
- package/skills/didi-ride/scripts/didi.py +266 -0
package/AGENTS.md
CHANGED
|
@@ -31,6 +31,7 @@ Skills are located in the `skills/` directory (also mirrored to `.agents/skills/
|
|
|
31
31
|
- **face-transform** — Face analysis, beautification, age/gender transform, swap, cartoon
|
|
32
32
|
- **short-url** — Create and manage short URLs
|
|
33
33
|
- **onepage-pdf** — Convert an HTML page into one tall single-page PDF (local; no API token; needs Python + pymupdf + Chrome/Edge)
|
|
34
|
+
- **apple-notes** — Manage Apple Notes on macOS: create, search, read, export, organize notes (macOS-only; local; no API token; drives Notes.app via AppleScript)
|
|
34
35
|
- **acedatacloud** — Manage your AceDataCloud account — balance, usage/spend, API keys, services, orders, announcements
|
|
35
36
|
|
|
36
37
|
## Authentication
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
Compatible with **30+ AI coding agents** via the [agentskills.io](https://agentskills.io/) open standard: Claude Code, GitHub Copilot, Gemini CLI, OpenAI Codex, Cursor, Roo Code, Goose, and more.
|
|
14
14
|
|
|
15
|
-
## Available Skills (
|
|
15
|
+
## Available Skills (30)
|
|
16
16
|
|
|
17
17
|
### AI Music & Audio
|
|
18
18
|
|
|
@@ -50,6 +50,7 @@ Compatible with **30+ AI coding agents** via the [agentskills.io](https://agents
|
|
|
50
50
|
| [face-transform](skills/face-transform/) | Face analysis, beautification, age/gender transform, swap, cartoon |
|
|
51
51
|
| [short-url](skills/short-url/) | Create and manage short URLs |
|
|
52
52
|
| [onepage-pdf](skills/onepage-pdf/) | Convert an HTML page into one tall single-page PDF — no pagination breaks (local, no token) |
|
|
53
|
+
| [apple-notes](skills/apple-notes/) | Manage Apple Notes on macOS — create, search, read, export, and organize notes (macOS-only, local, no token) |
|
|
53
54
|
| [acedatacloud](skills/acedatacloud/) | Manage your AceDataCloud account — balance, usage/spend, API keys, services, orders, announcements |
|
|
54
55
|
|
|
55
56
|
### Connectors
|
|
@@ -70,6 +71,7 @@ These skills drive third-party connectors users wire up at [auth.acedata.cloud/u
|
|
|
70
71
|
| [slack](skills/slack/) | Read Slack channels, messages, and user info via Web API | `slack` |
|
|
71
72
|
| [wechat-official-account](skills/wechat-official-account/) | Manage WeChat MP — drafts, publishing, materials, user tags | `wechat` (BYOC) |
|
|
72
73
|
| [personal-wechat](skills/personal-wechat/) | Operate a personal WeChat account through a self-hosted Wisdom service | `personalwechat` (BYOC) |
|
|
74
|
+
| [didi-ride](skills/didi-ride/) | Book DiDi rides, estimate fares, query/cancel orders, plan routes via the DiDi MCP | `didi` (BYOC) |
|
|
73
75
|
| [wecom](skills/wecom/) | WeCom (企业微信) self-built app — contacts, app messages, WeDoc, schedules, meetings | `wecom` (BYOC) |
|
|
74
76
|
| [tencent-docs](skills/tencent-docs/) | Create / read / list / search / manage Tencent Docs — docs, sheets, slides, mind maps, flowcharts | `tencentdocs` (BYOC) |
|
|
75
77
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acedatacloud/skills",
|
|
3
|
-
"version": "2026.703.
|
|
3
|
+
"version": "2026.703.4",
|
|
4
4
|
"description": "Agent Skills for AceDataCloud AI services — music, image, video generation, LLM chat, web search. Compatible with Claude Code, GitHub Copilot, Gemini CLI, OpenAI Codex, and 30+ AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apple-notes
|
|
3
|
+
description: Manage Apple Notes on macOS — list, search, read, create, append, move, delete, and export notes, and manage folders. Use when the user mentions Apple Notes / 备忘录, "add a note", "my notes", searching their notes, organizing folders, or exporting a note to Markdown/HTML. macOS-only; runs locally against Notes.app — no cloud API, no token.
|
|
4
|
+
when_to_use: |
|
|
5
|
+
Trigger for anything on the user's local Apple Notes (Notes.app on macOS):
|
|
6
|
+
list / search / read / export a note, create a new one, append to an existing
|
|
7
|
+
one, move it between folders, or manage folders. There is no cloud API for
|
|
8
|
+
Apple Notes, so this runs on the user's own Mac (Claude Code on macOS, or
|
|
9
|
+
driven remotely through a CodingBridge node). Writes act on the user's REAL
|
|
10
|
+
notes, so create / append / move / delete are GATED behind an explicit
|
|
11
|
+
confirmation.
|
|
12
|
+
allowed_tools: [Bash]
|
|
13
|
+
license: Apache-2.0
|
|
14
|
+
compatibility: macOS-only. No API token, no connector — Apple Notes has no cloud API. Drives the local Notes.app via AppleScript (osascript); zero external dependencies (Python 3 standard library only). Requires granting Automation access to Notes.app on first run. Does not call api.acedata.cloud.
|
|
15
|
+
metadata:
|
|
16
|
+
author: acedatacloud
|
|
17
|
+
version: "1.0"
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# apple-notes — manage Apple Notes locally via AppleScript
|
|
21
|
+
|
|
22
|
+
Drives the user's **real** Apple Notes through `Notes.app` on macOS. Apple never
|
|
23
|
+
shipped a cloud API for Notes, so there is nothing to OAuth into and no token to
|
|
24
|
+
inject — this skill runs **on the user's own Mac** and talks to Notes.app with
|
|
25
|
+
AppleScript (`osascript`). It works when Claude Code runs on macOS directly, or
|
|
26
|
+
remotely through a CodingBridge node running on the user's Mac.
|
|
27
|
+
|
|
28
|
+
The skill ships [`scripts/notes.py`](scripts/notes.py) — self-contained, Python
|
|
29
|
+
**standard library only** (it shells out to `osascript`; no `pip install`, no
|
|
30
|
+
`brew`). Dynamic values are passed as environment variables, never interpolated
|
|
31
|
+
into the AppleScript source, so note content can't break the script or inject
|
|
32
|
+
commands.
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- **macOS** with the Notes app. `python3` (system Python is fine).
|
|
37
|
+
- **Automation permission.** The first call triggers a macOS prompt: *"Terminal
|
|
38
|
+
wants to control Notes."* Approve it, or enable it under **System Settings ›
|
|
39
|
+
Privacy & Security › Automation ›** (your terminal / agent) **› Notes**. On an
|
|
40
|
+
authorization error the CLI tells the user exactly this — do **not** loop-retry.
|
|
41
|
+
- Referencing Notes launches the app in the background (no window is forced open).
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
NOTES="${SKILL_DIR:-.}/scripts/notes.py" # run from this skill's directory
|
|
45
|
+
python3 "$NOTES" folders # smoke-test the connection
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
| Command | Read/Write | Purpose |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| `folders` | read | List folders with note counts |
|
|
53
|
+
| `list [--folder F] [--limit N]` | read | List notes (newest first): id, title, folder, dates |
|
|
54
|
+
| `search QUERY [--folder F] [--limit N]` | read | Case-insensitive substring match on title + body |
|
|
55
|
+
| `view NOTE_ID [--format json\|text\|html]` | read | Show one note (metadata + body) |
|
|
56
|
+
| `export NOTE_ID [--format md\|html\|text] [-o FILE]` | read | Export one note to Markdown/HTML/text |
|
|
57
|
+
| `create --title T [--body B \| --body-file F] [--folder F]` | **write** | Create a note |
|
|
58
|
+
| `append NOTE_ID [--body B \| --body-file F]` | **write** | Append text to an existing note |
|
|
59
|
+
| `move NOTE_ID --folder F` | **write** | Move a note to another folder |
|
|
60
|
+
| `new-folder NAME` | **write** | Create a folder |
|
|
61
|
+
| `delete NOTE_ID` | **write** | Move a note to *Recently Deleted* (recoverable) |
|
|
62
|
+
|
|
63
|
+
Most commands print JSON (and errors are always JSON: `{"error": ...}`). The
|
|
64
|
+
exceptions print raw note content to stdout: `view --format text|html` and
|
|
65
|
+
`export` without `-o`. `NOTE_ID` is the opaque `x-coredata://…` id returned by
|
|
66
|
+
`list` / `search` — always fetch a fresh id first; do not guess one.
|
|
67
|
+
|
|
68
|
+
## Read examples
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
python3 "$NOTES" folders
|
|
72
|
+
python3 "$NOTES" list --limit 20
|
|
73
|
+
python3 "$NOTES" list --folder "Work" --limit 50
|
|
74
|
+
python3 "$NOTES" search "invoice" --limit 20
|
|
75
|
+
python3 "$NOTES" view "x-coredata://…/p42"
|
|
76
|
+
python3 "$NOTES" export "x-coredata://…/p42" --format md -o note.md
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Writes are GATED (dry-run unless trailing `--confirm`)
|
|
80
|
+
|
|
81
|
+
`create`, `append`, `move`, `new-folder`, and `delete` change the user's real
|
|
82
|
+
notes. Without a trailing `--confirm` they **dry-run** and print what they would
|
|
83
|
+
do. `--confirm` is honored **only as the very last argument**. Always show the
|
|
84
|
+
dry-run, get an explicit "yes", then re-run with `--confirm` appended.
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
# Create — dry-run, then confirm
|
|
88
|
+
python3 "$NOTES" create --title "Groceries" --body "milk\neggs" # preview
|
|
89
|
+
python3 "$NOTES" create --title "Groceries" --body "milk\neggs" --confirm # writes
|
|
90
|
+
|
|
91
|
+
# Create in a folder, body from a file
|
|
92
|
+
python3 "$NOTES" create --title "Spec" --body-file draft.md --folder "Work" --confirm
|
|
93
|
+
|
|
94
|
+
# Append to an existing note
|
|
95
|
+
python3 "$NOTES" append "x-coredata://…/p42" --body "one more line" --confirm
|
|
96
|
+
|
|
97
|
+
# Move / organize
|
|
98
|
+
python3 "$NOTES" new-folder "Archive" --confirm
|
|
99
|
+
python3 "$NOTES" move "x-coredata://…/p42" --folder "Archive" --confirm
|
|
100
|
+
|
|
101
|
+
# Delete → goes to Recently Deleted (recoverable ~30 days)
|
|
102
|
+
python3 "$NOTES" delete "x-coredata://…/p42" --confirm
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Content is treated as **plain text** (each line becomes a paragraph; the
|
|
106
|
+
`--title` is the note's first line, which Notes shows as the title). HTML in the
|
|
107
|
+
input is escaped, so it can't inject markup. Use `\n` for line breaks in
|
|
108
|
+
`--body`, or pass a file with `--body-file`.
|
|
109
|
+
|
|
110
|
+
## Gotchas
|
|
111
|
+
|
|
112
|
+
- **This is the user's real Notes account.** Confirm before any write.
|
|
113
|
+
- **`list` scans the whole library** — for large accounts it can take a few
|
|
114
|
+
seconds. Narrow with `--folder`, and cap with `--limit` (default 50). `search`
|
|
115
|
+
filters inside Notes and is faster.
|
|
116
|
+
- **Attachments / images:** notes containing images or attachments can be read
|
|
117
|
+
and exported as text, but `append` may not preserve embedded attachments —
|
|
118
|
+
avoid editing attachment notes here; open them in Notes instead.
|
|
119
|
+
- **Export → Markdown** converts common Notes formatting (headings, bold/italic,
|
|
120
|
+
lists, links, line breaks). Rich objects (tables, drawings, scanned docs)
|
|
121
|
+
degrade to plain text.
|
|
122
|
+
- **`move` stays within one account** (e.g. iCloud). Cross-account moves fail.
|
|
123
|
+
- **Never assume a `NOTE_ID`** — ids are store-specific; always get them from
|
|
124
|
+
`list` / `search` first.
|
|
125
|
+
- On a persistent authorization error, the Automation permission is missing —
|
|
126
|
+
tell the user to grant it (see Requirements) and stop; do not retry in a loop.
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
apple-notes — manage Apple Notes on macOS via AppleScript (osascript).
|
|
4
|
+
|
|
5
|
+
Standard library only. There is no cloud API for Apple Notes, so this drives the
|
|
6
|
+
local Notes.app through `osascript`. Dynamic values (titles, bodies, ids, folder
|
|
7
|
+
names, queries) are passed as ENVIRONMENT VARIABLES and read inside AppleScript
|
|
8
|
+
with `system attribute` — they are never interpolated into the script source, so
|
|
9
|
+
note content cannot break the script or inject AppleScript/shell commands.
|
|
10
|
+
|
|
11
|
+
Read commands run directly. Writes (create / append / move / new-folder /
|
|
12
|
+
delete) are GATED by a trailing `--confirm` (honored only as the last arg);
|
|
13
|
+
without it they dry-run. `delete` moves the note to Recently Deleted (recoverable).
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
python3 notes.py folders
|
|
17
|
+
python3 notes.py list --limit 20
|
|
18
|
+
python3 notes.py search "invoice" --limit 10
|
|
19
|
+
python3 notes.py view <note-id>
|
|
20
|
+
python3 notes.py export <note-id> --format md -o note.md
|
|
21
|
+
python3 notes.py create --title "Groceries" --body "milk\neggs" --confirm
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import html as _html
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
from html.parser import HTMLParser
|
|
34
|
+
|
|
35
|
+
# Field / record separators used to pack AppleScript output (U+001F / U+001E).
|
|
36
|
+
US = "\x1f"
|
|
37
|
+
RS = "\x1e"
|
|
38
|
+
|
|
39
|
+
TIMEOUT = 90 # overridden by --timeout in main()
|
|
40
|
+
|
|
41
|
+
# GATED-write handling: a trailing "--confirm" arms writes; it is stripped before
|
|
42
|
+
# argparse so it is never a real flag (and is honored ONLY as the last argument).
|
|
43
|
+
_RAW = sys.argv[1:]
|
|
44
|
+
CONFIRM = bool(_RAW) and _RAW[-1] == "--confirm"
|
|
45
|
+
ARGV = _RAW[:-1] if CONFIRM else list(_RAW)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def out(obj) -> None:
|
|
49
|
+
print(json.dumps(obj, ensure_ascii=False, indent=2, default=str))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def die(msg: str, code: int = 1) -> None:
|
|
53
|
+
out({"error": msg})
|
|
54
|
+
sys.exit(code)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _int(s) -> int:
|
|
58
|
+
try:
|
|
59
|
+
return int(str(s).strip())
|
|
60
|
+
except (TypeError, ValueError):
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ── AppleScript ─────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
# ISO-8601 date helpers (locale-independent: build the string from integers).
|
|
67
|
+
_HANDLERS = r"""
|
|
68
|
+
on isoDate(d)
|
|
69
|
+
return (pad(year of d, 4)) & "-" & (pad((month of d) as integer, 2)) & "-" & (pad(day of d, 2)) & "T" & (pad(hours of d, 2)) & ":" & (pad(minutes of d, 2)) & ":" & (pad(seconds of d, 2))
|
|
70
|
+
end isoDate
|
|
71
|
+
on pad(n, w)
|
|
72
|
+
set s to ((n as integer) as string)
|
|
73
|
+
repeat while (length of s) < w
|
|
74
|
+
set s to "0" & s
|
|
75
|
+
end repeat
|
|
76
|
+
return s
|
|
77
|
+
end pad
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
_EMIT = (
|
|
81
|
+
' set fol to ""\n'
|
|
82
|
+
" try\n"
|
|
83
|
+
" set fol to name of container of n\n"
|
|
84
|
+
" end try\n"
|
|
85
|
+
" set outText to outText & (id of n) & US & (name of n) & US & fol"
|
|
86
|
+
" & US & (my isoDate(modification date of n)) & US & (my isoDate(creation date of n)) & RS\n"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
_SCRIPT_FOLDERS = r"""
|
|
90
|
+
set US to (character id 31)
|
|
91
|
+
set RS to (character id 30)
|
|
92
|
+
set outText to ""
|
|
93
|
+
tell application "Notes"
|
|
94
|
+
repeat with f in folders
|
|
95
|
+
set outText to outText & (name of f) & US & ((count of notes of f) as string) & RS
|
|
96
|
+
end repeat
|
|
97
|
+
end tell
|
|
98
|
+
return outText
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
_SCRIPT_LIST = _HANDLERS + (
|
|
102
|
+
'set US to (character id 31)\n'
|
|
103
|
+
'set RS to (character id 30)\n'
|
|
104
|
+
'set folderName to system attribute "ANOTE_FOLDER"\n'
|
|
105
|
+
'set outText to ""\n'
|
|
106
|
+
'tell application "Notes"\n'
|
|
107
|
+
' if folderName is "" then\n'
|
|
108
|
+
' set theNotes to every note\n'
|
|
109
|
+
' else\n'
|
|
110
|
+
' set theNotes to every note of folder folderName\n'
|
|
111
|
+
' end if\n'
|
|
112
|
+
' repeat with n in theNotes\n'
|
|
113
|
+
+ _EMIT +
|
|
114
|
+
' end repeat\n'
|
|
115
|
+
'end tell\n'
|
|
116
|
+
'return outText\n'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
_SCRIPT_SEARCH = _HANDLERS + (
|
|
120
|
+
'set US to (character id 31)\n'
|
|
121
|
+
'set RS to (character id 30)\n'
|
|
122
|
+
'set q to system attribute "ANOTE_QUERY"\n'
|
|
123
|
+
'set folderName to system attribute "ANOTE_FOLDER"\n'
|
|
124
|
+
'set outText to ""\n'
|
|
125
|
+
'tell application "Notes"\n'
|
|
126
|
+
' if folderName is "" then\n'
|
|
127
|
+
' set theNotes to (every note whose (name contains q) or (body contains q))\n'
|
|
128
|
+
' else\n'
|
|
129
|
+
' set theNotes to (every note of folder folderName whose (name contains q) or (body contains q))\n'
|
|
130
|
+
' end if\n'
|
|
131
|
+
' repeat with n in theNotes\n'
|
|
132
|
+
+ _EMIT +
|
|
133
|
+
' end repeat\n'
|
|
134
|
+
'end tell\n'
|
|
135
|
+
'return outText\n'
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
_SCRIPT_VIEW = _HANDLERS + (
|
|
139
|
+
'set US to (character id 31)\n'
|
|
140
|
+
'set theId to system attribute "ANOTE_ID"\n'
|
|
141
|
+
'tell application "Notes"\n'
|
|
142
|
+
' set n to first note whose id is theId\n'
|
|
143
|
+
' set fol to ""\n'
|
|
144
|
+
' try\n'
|
|
145
|
+
' set fol to name of container of n\n'
|
|
146
|
+
' end try\n'
|
|
147
|
+
' set outText to (id of n) & US & (name of n) & US & fol & US'
|
|
148
|
+
' & (my isoDate(creation date of n)) & US & (my isoDate(modification date of n)) & US & (body of n)\n'
|
|
149
|
+
'end tell\n'
|
|
150
|
+
'return outText\n'
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
_SCRIPT_CREATE = r"""
|
|
154
|
+
set theBody to system attribute "ANOTE_BODY"
|
|
155
|
+
set folderName to system attribute "ANOTE_FOLDER"
|
|
156
|
+
tell application "Notes"
|
|
157
|
+
if folderName is "" then
|
|
158
|
+
set newNote to make new note with properties {body:theBody}
|
|
159
|
+
else
|
|
160
|
+
tell folder folderName
|
|
161
|
+
set newNote to make new note with properties {body:theBody}
|
|
162
|
+
end tell
|
|
163
|
+
end if
|
|
164
|
+
return id of newNote
|
|
165
|
+
end tell
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
_SCRIPT_APPEND = r"""
|
|
169
|
+
set theId to system attribute "ANOTE_ID"
|
|
170
|
+
set extra to system attribute "ANOTE_BODY"
|
|
171
|
+
tell application "Notes"
|
|
172
|
+
set n to first note whose id is theId
|
|
173
|
+
set body of n to (body of n) & extra
|
|
174
|
+
return id of n
|
|
175
|
+
end tell
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
_SCRIPT_MOVE = r"""
|
|
179
|
+
set theId to system attribute "ANOTE_ID"
|
|
180
|
+
set destFolder to system attribute "ANOTE_FOLDER"
|
|
181
|
+
tell application "Notes"
|
|
182
|
+
set n to first note whose id is theId
|
|
183
|
+
move n to folder destFolder
|
|
184
|
+
return id of n
|
|
185
|
+
end tell
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
_SCRIPT_NEWFOLDER = r"""
|
|
189
|
+
set folderName to system attribute "ANOTE_FOLDER"
|
|
190
|
+
tell application "Notes"
|
|
191
|
+
make new folder with properties {name:folderName}
|
|
192
|
+
end tell
|
|
193
|
+
return folderName
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
_SCRIPT_DELETE = r"""
|
|
197
|
+
set theId to system attribute "ANOTE_ID"
|
|
198
|
+
tell application "Notes"
|
|
199
|
+
set n to first note whose id is theId
|
|
200
|
+
delete n
|
|
201
|
+
end tell
|
|
202
|
+
return theId
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _map_error(err: str) -> None:
|
|
207
|
+
low = err.lower()
|
|
208
|
+
if "-1743" in err or "not authoriz" in low or "not allowed to send" in low:
|
|
209
|
+
die(
|
|
210
|
+
"Not authorized to control Notes. Grant Automation access under "
|
|
211
|
+
"System Settings \u203a Privacy & Security \u203a Automation \u203a "
|
|
212
|
+
"(your terminal/agent) \u203a Notes, then retry."
|
|
213
|
+
)
|
|
214
|
+
if "-1728" in err or "can\u2019t get" in low or "can't get" in low or "-2753" in err:
|
|
215
|
+
die("Not found \u2014 check the note id or folder name (run `list` / `folders` for valid values).")
|
|
216
|
+
if "-600" in err or "isn\u2019t running" in low or "not running" in low:
|
|
217
|
+
die("Notes.app could not launch.")
|
|
218
|
+
die("AppleScript error: " + err if err else "osascript failed with no error message.")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def osa(script: str, **env: str) -> str:
|
|
222
|
+
full = dict(os.environ)
|
|
223
|
+
for key, value in env.items():
|
|
224
|
+
full[key] = "" if value is None else str(value)
|
|
225
|
+
try:
|
|
226
|
+
proc = subprocess.run(
|
|
227
|
+
["osascript", "-e", script],
|
|
228
|
+
capture_output=True,
|
|
229
|
+
text=True,
|
|
230
|
+
encoding="utf-8",
|
|
231
|
+
errors="replace",
|
|
232
|
+
env=full,
|
|
233
|
+
timeout=TIMEOUT,
|
|
234
|
+
)
|
|
235
|
+
except FileNotFoundError:
|
|
236
|
+
die("osascript not found \u2014 this skill requires macOS.")
|
|
237
|
+
except subprocess.TimeoutExpired:
|
|
238
|
+
die(f"osascript timed out after {TIMEOUT}s \u2014 narrow with --folder / --limit.")
|
|
239
|
+
if proc.returncode != 0:
|
|
240
|
+
_map_error((proc.stderr or "").strip())
|
|
241
|
+
return proc.stdout
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ── content helpers ─────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
def text_to_html(title: str, body: str) -> str:
|
|
247
|
+
"""Plain text -> Notes-safe HTML (one <div> per line). Input is escaped."""
|
|
248
|
+
parts = []
|
|
249
|
+
if title:
|
|
250
|
+
parts.append("<div>" + _html.escape(title) + "</div>")
|
|
251
|
+
for line in (body or "").split("\n"):
|
|
252
|
+
parts.append("<div><br></div>" if line.strip() == "" else "<div>" + _html.escape(line) + "</div>")
|
|
253
|
+
return "".join(parts)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class _Converter(HTMLParser):
|
|
257
|
+
"""Minimal Notes-HTML -> Markdown/plain-text converter (stdlib only)."""
|
|
258
|
+
|
|
259
|
+
def __init__(self, markdown: bool = True) -> None:
|
|
260
|
+
super().__init__(convert_charrefs=True)
|
|
261
|
+
self.md = markdown
|
|
262
|
+
self.buf: list[str] = []
|
|
263
|
+
self._href: str | None = None
|
|
264
|
+
self._list: list = []
|
|
265
|
+
|
|
266
|
+
def _nl(self) -> None:
|
|
267
|
+
if self.buf and not self.buf[-1].endswith("\n"):
|
|
268
|
+
self.buf.append("\n")
|
|
269
|
+
|
|
270
|
+
def handle_starttag(self, tag: str, attrs) -> None:
|
|
271
|
+
if tag == "br":
|
|
272
|
+
self.buf.append("\n")
|
|
273
|
+
elif tag in ("div", "p"):
|
|
274
|
+
self._nl()
|
|
275
|
+
elif tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
|
|
276
|
+
self._nl()
|
|
277
|
+
if self.md:
|
|
278
|
+
self.buf.append("#" * int(tag[1]) + " ")
|
|
279
|
+
elif tag in ("b", "strong") and self.md:
|
|
280
|
+
self.buf.append("**")
|
|
281
|
+
elif tag in ("i", "em") and self.md:
|
|
282
|
+
self.buf.append("*")
|
|
283
|
+
elif tag == "ul":
|
|
284
|
+
self._list.append("ul")
|
|
285
|
+
self._nl()
|
|
286
|
+
elif tag == "ol":
|
|
287
|
+
self._list.append(["ol", 0])
|
|
288
|
+
self._nl()
|
|
289
|
+
elif tag == "li":
|
|
290
|
+
self._nl()
|
|
291
|
+
if self.md and self._list:
|
|
292
|
+
top = self._list[-1]
|
|
293
|
+
if top == "ul":
|
|
294
|
+
self.buf.append("- ")
|
|
295
|
+
else:
|
|
296
|
+
top[1] += 1
|
|
297
|
+
self.buf.append(f"{top[1]}. ")
|
|
298
|
+
elif tag == "a":
|
|
299
|
+
self._href = dict(attrs).get("href")
|
|
300
|
+
if self.md and self._href:
|
|
301
|
+
self.buf.append("[")
|
|
302
|
+
|
|
303
|
+
def handle_endtag(self, tag: str) -> None:
|
|
304
|
+
if tag in ("b", "strong") and self.md:
|
|
305
|
+
self.buf.append("**")
|
|
306
|
+
elif tag in ("i", "em") and self.md:
|
|
307
|
+
self.buf.append("*")
|
|
308
|
+
elif tag in ("div", "p", "h1", "h2", "h3", "h4", "h5", "h6", "li"):
|
|
309
|
+
self._nl()
|
|
310
|
+
elif tag in ("ul", "ol"):
|
|
311
|
+
if self._list:
|
|
312
|
+
self._list.pop()
|
|
313
|
+
self._nl()
|
|
314
|
+
elif tag == "a":
|
|
315
|
+
if self.md and self._href:
|
|
316
|
+
self.buf.append(f"]({self._href})")
|
|
317
|
+
self._href = None
|
|
318
|
+
|
|
319
|
+
def handle_data(self, data: str) -> None:
|
|
320
|
+
self.buf.append(data)
|
|
321
|
+
|
|
322
|
+
def result(self) -> str:
|
|
323
|
+
text = re.sub(r"\n{3,}", "\n\n", "".join(self.buf))
|
|
324
|
+
return text.strip() + "\n"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _convert(body_html: str, markdown: bool) -> str:
|
|
328
|
+
conv = _Converter(markdown=markdown)
|
|
329
|
+
conv.feed(body_html or "")
|
|
330
|
+
return conv.result()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _read_body(args) -> str:
|
|
334
|
+
body_file = getattr(args, "body_file", None)
|
|
335
|
+
if body_file:
|
|
336
|
+
try:
|
|
337
|
+
with open(body_file, "r", encoding="utf-8") as fh:
|
|
338
|
+
return fh.read()
|
|
339
|
+
except OSError as exc:
|
|
340
|
+
die(f"cannot read --body-file: {exc}")
|
|
341
|
+
body = getattr(args, "body", None)
|
|
342
|
+
if body is None:
|
|
343
|
+
return ""
|
|
344
|
+
return body.replace("\\n", "\n").replace("\\t", "\t")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _parse_notes(raw: str) -> list:
|
|
348
|
+
items = []
|
|
349
|
+
for rec in raw.split(RS):
|
|
350
|
+
rec = rec.strip("\n")
|
|
351
|
+
if not rec:
|
|
352
|
+
continue
|
|
353
|
+
parts = rec.split(US)
|
|
354
|
+
if len(parts) < 5:
|
|
355
|
+
continue
|
|
356
|
+
items.append(
|
|
357
|
+
{"id": parts[0], "title": parts[1], "folder": parts[2], "modified": parts[3], "created": parts[4]}
|
|
358
|
+
)
|
|
359
|
+
return items
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# ── commands ────────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
def cmd_folders(args) -> None:
|
|
365
|
+
raw = osa(_SCRIPT_FOLDERS)
|
|
366
|
+
items = []
|
|
367
|
+
for rec in raw.split(RS):
|
|
368
|
+
rec = rec.strip("\n")
|
|
369
|
+
if not rec:
|
|
370
|
+
continue
|
|
371
|
+
parts = rec.split(US)
|
|
372
|
+
if len(parts) < 2:
|
|
373
|
+
continue
|
|
374
|
+
items.append({"folder": parts[0], "count": _int(parts[1])})
|
|
375
|
+
out({"folders": items, "count": len(items)})
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def cmd_list(args) -> None:
|
|
379
|
+
items = _parse_notes(osa(_SCRIPT_LIST, ANOTE_FOLDER=args.folder or ""))
|
|
380
|
+
items.sort(key=lambda x: x.get("modified") or "", reverse=True)
|
|
381
|
+
if args.limit and args.limit > 0:
|
|
382
|
+
items = items[: args.limit]
|
|
383
|
+
out({"notes": items, "count": len(items)})
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def cmd_search(args) -> None:
|
|
387
|
+
query = (args.query or "").strip()
|
|
388
|
+
if not query:
|
|
389
|
+
die("search needs a non-empty query.")
|
|
390
|
+
items = _parse_notes(osa(_SCRIPT_SEARCH, ANOTE_QUERY=args.query, ANOTE_FOLDER=args.folder or ""))
|
|
391
|
+
items.sort(key=lambda x: x.get("modified") or "", reverse=True)
|
|
392
|
+
if args.limit and args.limit > 0:
|
|
393
|
+
items = items[: args.limit]
|
|
394
|
+
out({"query": args.query, "notes": items, "count": len(items)})
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _fetch(note_id: str) -> dict:
|
|
398
|
+
raw = osa(_SCRIPT_VIEW, ANOTE_ID=note_id)
|
|
399
|
+
# maxsplit=5 so the HTML body (last field) keeps any US/RS chars it contains
|
|
400
|
+
parts = raw.split(US, 5)
|
|
401
|
+
if len(parts) < 6:
|
|
402
|
+
die("unexpected osascript output while reading the note.")
|
|
403
|
+
body = parts[5]
|
|
404
|
+
if body.endswith("\n"):
|
|
405
|
+
body = body[:-1]
|
|
406
|
+
return {
|
|
407
|
+
"id": parts[0],
|
|
408
|
+
"title": parts[1],
|
|
409
|
+
"folder": parts[2],
|
|
410
|
+
"created": parts[3],
|
|
411
|
+
"modified": parts[4],
|
|
412
|
+
"body_html": body,
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def cmd_view(args) -> None:
|
|
417
|
+
note = _fetch(args.note_id)
|
|
418
|
+
if args.format == "html":
|
|
419
|
+
print(note["body_html"])
|
|
420
|
+
return
|
|
421
|
+
if args.format == "text":
|
|
422
|
+
print(_convert(note["body_html"], markdown=False))
|
|
423
|
+
return
|
|
424
|
+
note["text"] = _convert(note["body_html"], markdown=False)
|
|
425
|
+
out(note)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def cmd_export(args) -> None:
|
|
429
|
+
note = _fetch(args.note_id)
|
|
430
|
+
if args.format == "html":
|
|
431
|
+
content = note["body_html"]
|
|
432
|
+
else:
|
|
433
|
+
content = _convert(note["body_html"], markdown=(args.format == "md"))
|
|
434
|
+
if args.output:
|
|
435
|
+
try:
|
|
436
|
+
with open(args.output, "w", encoding="utf-8") as fh:
|
|
437
|
+
fh.write(content if content.endswith("\n") else content + "\n")
|
|
438
|
+
except OSError as exc:
|
|
439
|
+
die(f"cannot write output: {exc}")
|
|
440
|
+
out(
|
|
441
|
+
{
|
|
442
|
+
"exported": args.output,
|
|
443
|
+
"title": note["title"],
|
|
444
|
+
"format": args.format,
|
|
445
|
+
"bytes": len(content.encode("utf-8")),
|
|
446
|
+
}
|
|
447
|
+
)
|
|
448
|
+
else:
|
|
449
|
+
print(content)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _dry(action: str, **fields) -> None:
|
|
453
|
+
payload = {"dry_run": True, "action": action}
|
|
454
|
+
payload.update(fields)
|
|
455
|
+
payload["hint"] = "re-run with --confirm as the LAST argument to apply"
|
|
456
|
+
out(payload)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def cmd_create(args) -> None:
|
|
460
|
+
body = _read_body(args)
|
|
461
|
+
if not CONFIRM:
|
|
462
|
+
preview = args.title + (("\n" + body) if body else "")
|
|
463
|
+
_dry("create", title=args.title, folder=args.folder or "(default)", preview=preview)
|
|
464
|
+
return
|
|
465
|
+
note_id = osa(_SCRIPT_CREATE, ANOTE_BODY=text_to_html(args.title, body), ANOTE_FOLDER=args.folder or "").strip()
|
|
466
|
+
out({"created": True, "id": note_id, "title": args.title, "folder": args.folder or "(default)"})
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def cmd_append(args) -> None:
|
|
470
|
+
body = _read_body(args)
|
|
471
|
+
if not body.strip():
|
|
472
|
+
die("nothing to append (empty body).")
|
|
473
|
+
if not CONFIRM:
|
|
474
|
+
_dry("append", note_id=args.note_id, preview=body)
|
|
475
|
+
return
|
|
476
|
+
note_id = osa(_SCRIPT_APPEND, ANOTE_ID=args.note_id, ANOTE_BODY=text_to_html("", body)).strip()
|
|
477
|
+
out({"appended": True, "id": note_id})
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def cmd_move(args) -> None:
|
|
481
|
+
if not CONFIRM:
|
|
482
|
+
_dry("move", note_id=args.note_id, to_folder=args.folder)
|
|
483
|
+
return
|
|
484
|
+
note_id = osa(_SCRIPT_MOVE, ANOTE_ID=args.note_id, ANOTE_FOLDER=args.folder).strip()
|
|
485
|
+
out({"moved": True, "id": note_id, "folder": args.folder})
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def cmd_new_folder(args) -> None:
|
|
489
|
+
if not CONFIRM:
|
|
490
|
+
_dry("new-folder", name=args.name)
|
|
491
|
+
return
|
|
492
|
+
name = osa(_SCRIPT_NEWFOLDER, ANOTE_FOLDER=args.name).strip()
|
|
493
|
+
out({"created_folder": True, "name": name})
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def cmd_delete(args) -> None:
|
|
497
|
+
if not CONFIRM:
|
|
498
|
+
_dry("delete", note_id=args.note_id, note="moves the note to Recently Deleted (recoverable)")
|
|
499
|
+
return
|
|
500
|
+
note_id = osa(_SCRIPT_DELETE, ANOTE_ID=args.note_id).strip()
|
|
501
|
+
out({"deleted": True, "id": note_id, "note": "moved to Recently Deleted (recoverable ~30 days)"})
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def main() -> None:
|
|
505
|
+
if sys.platform != "darwin":
|
|
506
|
+
die("apple-notes is macOS-only (it drives Notes.app via AppleScript).")
|
|
507
|
+
|
|
508
|
+
parser = argparse.ArgumentParser(prog="notes.py", description="Manage Apple Notes via AppleScript.")
|
|
509
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
510
|
+
common.add_argument("--timeout", type=int, default=90, help="osascript timeout in seconds (default 90)")
|
|
511
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
512
|
+
|
|
513
|
+
sub.add_parser("folders", parents=[common], help="list folders with note counts").set_defaults(func=cmd_folders)
|
|
514
|
+
|
|
515
|
+
p = sub.add_parser("list", parents=[common], help="list notes (newest first)")
|
|
516
|
+
p.add_argument("--folder")
|
|
517
|
+
p.add_argument("--limit", type=int, default=50)
|
|
518
|
+
p.set_defaults(func=cmd_list)
|
|
519
|
+
|
|
520
|
+
p = sub.add_parser("search", parents=[common], help="search notes by title + body")
|
|
521
|
+
p.add_argument("query")
|
|
522
|
+
p.add_argument("--folder")
|
|
523
|
+
p.add_argument("--limit", type=int, default=50)
|
|
524
|
+
p.set_defaults(func=cmd_search)
|
|
525
|
+
|
|
526
|
+
p = sub.add_parser("view", parents=[common], help="show one note")
|
|
527
|
+
p.add_argument("note_id")
|
|
528
|
+
p.add_argument("--format", choices=["json", "text", "html"], default="json")
|
|
529
|
+
p.set_defaults(func=cmd_view)
|
|
530
|
+
|
|
531
|
+
p = sub.add_parser("export", parents=[common], help="export one note")
|
|
532
|
+
p.add_argument("note_id")
|
|
533
|
+
p.add_argument("--format", choices=["md", "html", "text"], default="md")
|
|
534
|
+
p.add_argument("-o", "--output")
|
|
535
|
+
p.set_defaults(func=cmd_export)
|
|
536
|
+
|
|
537
|
+
p = sub.add_parser("create", parents=[common], help="create a note (GATED)")
|
|
538
|
+
p.add_argument("--title", required=True)
|
|
539
|
+
grp = p.add_mutually_exclusive_group()
|
|
540
|
+
grp.add_argument("--body")
|
|
541
|
+
grp.add_argument("--body-file")
|
|
542
|
+
p.add_argument("--folder")
|
|
543
|
+
p.set_defaults(func=cmd_create)
|
|
544
|
+
|
|
545
|
+
p = sub.add_parser("append", parents=[common], help="append text to a note (GATED)")
|
|
546
|
+
p.add_argument("note_id")
|
|
547
|
+
grp = p.add_mutually_exclusive_group(required=True)
|
|
548
|
+
grp.add_argument("--body")
|
|
549
|
+
grp.add_argument("--body-file")
|
|
550
|
+
p.set_defaults(func=cmd_append)
|
|
551
|
+
|
|
552
|
+
p = sub.add_parser("move", parents=[common], help="move a note to a folder (GATED)")
|
|
553
|
+
p.add_argument("note_id")
|
|
554
|
+
p.add_argument("--folder", required=True)
|
|
555
|
+
p.set_defaults(func=cmd_move)
|
|
556
|
+
|
|
557
|
+
p = sub.add_parser("new-folder", parents=[common], help="create a folder (GATED)")
|
|
558
|
+
p.add_argument("name")
|
|
559
|
+
p.set_defaults(func=cmd_new_folder)
|
|
560
|
+
|
|
561
|
+
p = sub.add_parser("delete", parents=[common], help="move a note to Recently Deleted (GATED)")
|
|
562
|
+
p.add_argument("note_id")
|
|
563
|
+
p.set_defaults(func=cmd_delete)
|
|
564
|
+
|
|
565
|
+
args = parser.parse_args(ARGV)
|
|
566
|
+
global TIMEOUT
|
|
567
|
+
TIMEOUT = args.timeout
|
|
568
|
+
args.func(args)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
if __name__ == "__main__":
|
|
572
|
+
main()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: didi-ride
|
|
3
|
+
description: 通过滴滴出行 MCP 打车、查价、查询订单、取消订单、规划路线(驾车/公交/步行/骑行)和搜索地点。Use when the user mentions 滴滴, 打车, 叫车, 回家/上班要打车, 查一下从 A 到 B 多少钱/怎么走, 查询订单, 司机在哪/多久到, 预约叫车, 路线规划, DiDi, ride-hailing, or booking a taxi.
|
|
4
|
+
when_to_use: |
|
|
5
|
+
Trigger for anything involving the user's DiDi (滴滴出行) account:
|
|
6
|
+
book a ride ("打车去…", "回家", "上班"), get a fare/route estimate,
|
|
7
|
+
query an existing order (driver location, ETA, trip progress), cancel
|
|
8
|
+
an order, plan a driving/transit/walking/cycling route, or search
|
|
9
|
+
places / reverse-geocode. Creating and cancelling orders act on real
|
|
10
|
+
money and a real driver, so those writes are gated behind explicit
|
|
11
|
+
confirmation.
|
|
12
|
+
connections: [didi]
|
|
13
|
+
allowed_tools: [Bash]
|
|
14
|
+
license: Apache-2.0
|
|
15
|
+
metadata:
|
|
16
|
+
author: acedatacloud
|
|
17
|
+
version: "1.0"
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# DiDi Ride (滴滴出行)
|
|
21
|
+
|
|
22
|
+
Drive the user's **DiDi (滴滴出行)** account through the DiDi MCP server:
|
|
23
|
+
book rides, estimate fares, track orders, cancel, and plan routes.
|
|
24
|
+
|
|
25
|
+
The `didi` BYOC connector injects one env var into the sandbox:
|
|
26
|
+
|
|
27
|
+
- `DIDI_MCP_KEY` — the user's DiDi MCP key. **Secret — never echo, print, or log it.**
|
|
28
|
+
|
|
29
|
+
If `DIDI_MCP_KEY` is missing, tell the user to connect the DiDi connector at
|
|
30
|
+
[auth.acedata.cloud/user/connections](https://auth.acedata.cloud/user/connections)
|
|
31
|
+
(they get the key by scanning the QR in the 滴滴出行 App or via
|
|
32
|
+
<https://mcp.didichuxing.com/claw>).
|
|
33
|
+
|
|
34
|
+
## CLI
|
|
35
|
+
|
|
36
|
+
The skill ships a stdlib-only helper that speaks the MCP Streamable-HTTP
|
|
37
|
+
protocol to DiDi. Point at it once:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
DIDI=$SKILL_DIR/scripts/didi.py
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Two commands:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python3 $DIDI list # list tools + their JSON input schemas
|
|
47
|
+
python3 $DIDI call <tool> '<json-args>' # call any tool
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Always check `references/api_references.md` for the exact tool + parameter
|
|
51
|
+
names before calling.** When unsure, run `python3 $DIDI list` — it returns the
|
|
52
|
+
authoritative input schema for every tool straight from DiDi. Do **not** guess
|
|
53
|
+
parameter names (common mistakes: `keyword` → `keywords`, `region` → `city`,
|
|
54
|
+
four coord fields → six `from_name/from_lat/from_lng/to_name/to_lat/to_lng`).
|
|
55
|
+
|
|
56
|
+
All argument **values must be strings** (including coordinates and
|
|
57
|
+
`product_category`), e.g. `{"product_category":"1"}` not `{"product_category":1}`.
|
|
58
|
+
|
|
59
|
+
## Write gating (real money / real driver)
|
|
60
|
+
|
|
61
|
+
`taxi_create_order` and `taxi_cancel_order` are **gated**: without a trailing
|
|
62
|
+
`--confirm` the helper only DRY-RUNS and changes nothing. Run once without
|
|
63
|
+
`--confirm` to preview, then re-run with `--confirm` as the **last** argument
|
|
64
|
+
after the user approves:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python3 $DIDI call taxi_create_order '{"estimate_trace_id":"...","product_category":"1"}' --confirm
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- Even when the user says "打车" / "取消订单", confirm the concrete details
|
|
71
|
+
(起终点、车型 for booking; the order for cancelling) before adding `--confirm`.
|
|
72
|
+
- Cancel intent ≠ cancel confirmation — always ask "确认取消吗?" first.
|
|
73
|
+
|
|
74
|
+
## Booking flow (最小可执行)
|
|
75
|
+
|
|
76
|
+
1. **Resolve addresses** — `maps_textsearch` (never invent coordinates; don't
|
|
77
|
+
reuse coordinates from earlier turns — the user may have moved).
|
|
78
|
+
- If the user references an address alias (家 / 公司 / etc.) that you don't
|
|
79
|
+
have, ask them; this skill has no stored preferences.
|
|
80
|
+
2. **Confirm start/end** — if `maps_textsearch` returns ≥2 candidates, list at
|
|
81
|
+
least the top 3 and let the user pick; a single exact match needs no
|
|
82
|
+
confirmation.
|
|
83
|
+
3. **Estimate** — `taxi_estimate`; record the returned `traceId` /
|
|
84
|
+
`estimate_trace_id`. It expires (`-32021`) — re-estimate if stale.
|
|
85
|
+
4. **Pick car type** — user's current message wins ("叫快车"→`product_category`
|
|
86
|
+
`1`, "专车"→`8`); otherwise ask. Only use categories present in the
|
|
87
|
+
`taxi_estimate` response; never silently substitute a different service level
|
|
88
|
+
(快车 `1` ≠ 特惠快车 `201`).
|
|
89
|
+
5. **Create order** — `taxi_create_order` with the latest `estimate_trace_id`
|
|
90
|
+
(dry-run → confirm → `--confirm`).
|
|
91
|
+
6. **Report** — order id, start/end, car type, estimated fare. Tell the user
|
|
92
|
+
they can send 「查询订单」 to check status.
|
|
93
|
+
|
|
94
|
+
## Query an order
|
|
95
|
+
|
|
96
|
+
Order id comes from (in priority): the user's message → the most recent order
|
|
97
|
+
created this conversation → else ask.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
python3 $DIDI call taxi_query_order '{"order_id":"ORDER_ID"}'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Status codes (`code`):
|
|
104
|
+
|
|
105
|
+
| code | 含义 | 输出 |
|
|
106
|
+
|------|------|------|
|
|
107
|
+
| 0 | 匹配中 | ⏳ 正在为您匹配司机 |
|
|
108
|
+
| 1 | 司机已接单 | 展示司机姓名、车型、车牌、电话、距离与预计到达时间 |
|
|
109
|
+
| 2 | 司机已到达 | 🔔 司机已到达上车点 |
|
|
110
|
+
| 4 | 行程进行中 | 🚗 行程已开始 |
|
|
111
|
+
| 5 | 订单完成 | ✅ 行程结束(展示费用,如有) |
|
|
112
|
+
| 6 | 系统取消 | ❌ 订单已被系统取消 |
|
|
113
|
+
| 7 | 已取消 | ❌ 订单已取消 |
|
|
114
|
+
| 3 / 8-12 | 其他终态 | 显示对应状态描述 |
|
|
115
|
+
|
|
116
|
+
## Routes & places (no order needed)
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
python3 $DIDI call maps_direction_driving '{"from_name":"...","from_lat":"...","from_lng":"...","to_name":"...","to_lat":"...","to_lng":"..."}'
|
|
120
|
+
python3 $DIDI call maps_direction_transit '{...}' # 公交
|
|
121
|
+
python3 $DIDI call maps_direction_walking '{...}' # 步行
|
|
122
|
+
python3 $DIDI call maps_direction_bicycling '{...}' # 骑行
|
|
123
|
+
python3 $DIDI call maps_place_around '{"keywords":"咖啡","lat":"...","lng":"..."}'
|
|
124
|
+
python3 $DIDI call maps_regeocode '{"lat":"...","lng":"..."}'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Gotchas
|
|
128
|
+
|
|
129
|
+
- **Never print the key or the raw endpoint URL** — the helper handles the
|
|
130
|
+
transport and keeps the key internal.
|
|
131
|
+
- `taxi_create_order` takes only `estimate_trace_id`, `product_category`, and
|
|
132
|
+
optional `caller_car_phone`. Don't pass the estimate's coordinate/name fields.
|
|
133
|
+
- No stored preferences: this skill doesn't remember home/work/car type — ask
|
|
134
|
+
the user, or wire richer memory in a higher layer.
|
|
135
|
+
- Auth failure surfaces as `-32002` (or HTTP 401/403): the key is missing/expired
|
|
136
|
+
→ have the user reconnect the connector.
|
|
137
|
+
|
|
138
|
+
> **Setup:** See [authentication](../_shared/authentication.md). This skill uses
|
|
139
|
+
> the `didi` connector's injected `DIDI_MCP_KEY`, not `ACEDATACLOUD_API_TOKEN`.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# DiDi MCP — Tool Reference
|
|
2
|
+
|
|
3
|
+
Authoritative schemas come from `python3 $DIDI list` (the DiDi server returns
|
|
4
|
+
each tool's `inputSchema`). This file summarizes the tools and the parameter
|
|
5
|
+
names that are easy to get wrong. **All values are strings.**
|
|
6
|
+
|
|
7
|
+
## Places & geocoding
|
|
8
|
+
|
|
9
|
+
### `maps_textsearch` — search a place by text
|
|
10
|
+
| param | required | notes |
|
|
11
|
+
|-------|----------|-------|
|
|
12
|
+
| `keywords` | yes | search text, e.g. `北京西站` (NOT `keyword`) |
|
|
13
|
+
| `city` | no | city name to scope the search, e.g. `北京` (NOT `region`) |
|
|
14
|
+
|
|
15
|
+
Returns candidate places with names + coordinates. Use these coordinates for
|
|
16
|
+
`taxi_estimate` and the `maps_direction_*` tools.
|
|
17
|
+
|
|
18
|
+
### `maps_regeocode` — coordinates → address
|
|
19
|
+
| param | required |
|
|
20
|
+
|-------|----------|
|
|
21
|
+
| `lat` | yes |
|
|
22
|
+
| `lng` | yes |
|
|
23
|
+
|
|
24
|
+
## Routes
|
|
25
|
+
|
|
26
|
+
`maps_direction_driving` / `maps_direction_transit` / `maps_direction_walking`
|
|
27
|
+
/ `maps_direction_bicycling` — all take the same six fields:
|
|
28
|
+
|
|
29
|
+
| param | required |
|
|
30
|
+
|-------|----------|
|
|
31
|
+
| `from_name` | yes |
|
|
32
|
+
| `from_lat` | yes |
|
|
33
|
+
| `from_lng` | yes |
|
|
34
|
+
| `to_name` | yes |
|
|
35
|
+
| `to_lat` | yes |
|
|
36
|
+
| `to_lng` | yes |
|
|
37
|
+
|
|
38
|
+
### `maps_place_around` — nearby POI search
|
|
39
|
+
| param | required | notes |
|
|
40
|
+
|-------|----------|-------|
|
|
41
|
+
| `keywords` | yes | POI keyword, e.g. `咖啡` |
|
|
42
|
+
| `lat` | yes | center latitude |
|
|
43
|
+
| `lng` | yes | center longitude |
|
|
44
|
+
|
|
45
|
+
## Ride hailing
|
|
46
|
+
|
|
47
|
+
### `taxi_estimate` — price/ETA estimate (do this before ordering)
|
|
48
|
+
Six coordinate/name fields (same as routes):
|
|
49
|
+
`from_name`, `from_lat`, `from_lng`, `to_name`, `to_lat`, `to_lng`.
|
|
50
|
+
|
|
51
|
+
Returns a list of available car types, each with a `productCategory` and price,
|
|
52
|
+
plus a `traceId` / `estimate_trace_id` required by `taxi_create_order`. The
|
|
53
|
+
trace id expires — a stale one returns `-32021`; re-estimate to refresh.
|
|
54
|
+
|
|
55
|
+
Common `product_category` values:
|
|
56
|
+
|
|
57
|
+
| code | 车型 |
|
|
58
|
+
|------|------|
|
|
59
|
+
| `1` | 快车 |
|
|
60
|
+
| `8` | 专车 |
|
|
61
|
+
| `201` | 特惠快车 |
|
|
62
|
+
|
|
63
|
+
> Treat these as hints — always match against the `productCategory` values the
|
|
64
|
+
> live `taxi_estimate` response actually returns. `1` (快车) and `201`
|
|
65
|
+
> (特惠快车) are different service levels; never swap one for the other.
|
|
66
|
+
|
|
67
|
+
### `taxi_create_order` — book the ride ⚠️ WRITE (needs `--confirm`)
|
|
68
|
+
| param | required | notes |
|
|
69
|
+
|-------|----------|-------|
|
|
70
|
+
| `estimate_trace_id` | yes | from the latest `taxi_estimate` |
|
|
71
|
+
| `product_category` | yes | chosen car type code (string) |
|
|
72
|
+
| `caller_car_phone` | no | omit unless the user gives a number |
|
|
73
|
+
|
|
74
|
+
Only these three fields. Do **not** pass coordinate/name fields.
|
|
75
|
+
|
|
76
|
+
### `taxi_query_order` — status + driver location
|
|
77
|
+
| param | required |
|
|
78
|
+
|-------|----------|
|
|
79
|
+
| `order_id` | yes |
|
|
80
|
+
|
|
81
|
+
Status `code`: `0` 匹配中 · `1` 司机已接单 · `2` 司机已到达 · `4` 行程中 ·
|
|
82
|
+
`5` 完成 · `6` 系统取消 · `7` 已取消 · `3`/`8`-`12` 其他终态.
|
|
83
|
+
|
|
84
|
+
### `taxi_cancel_order` — cancel ⚠️ WRITE (needs `--confirm`)
|
|
85
|
+
| param | required |
|
|
86
|
+
|-------|----------|
|
|
87
|
+
| `order_id` | yes |
|
|
88
|
+
|
|
89
|
+
Always ask "确认取消吗?" before adding `--confirm`.
|
|
90
|
+
|
|
91
|
+
## Errors
|
|
92
|
+
|
|
93
|
+
| code | meaning | action |
|
|
94
|
+
|------|---------|--------|
|
|
95
|
+
| `-32002` / HTTP 401 / 403 | auth failed | key missing/expired → reconnect connector |
|
|
96
|
+
| `-32021` | estimate trace expired | re-run `taxi_estimate` |
|
|
97
|
+
| HTTP 400 | bad params | re-check parameter names against `didi.py list` |
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""DiDi ride helper — talks to the DiDi MCP server over the MCP
|
|
3
|
+
Streamable-HTTP transport using only the Python standard library.
|
|
4
|
+
|
|
5
|
+
The `didi` BYOC connector injects one env var:
|
|
6
|
+
|
|
7
|
+
DIDI_MCP_KEY the user's DiDi MCP key (secret — never printed)
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 didi.py list # list available tools + schemas
|
|
11
|
+
python3 didi.py call <tool> '<json-args>' # call any tool
|
|
12
|
+
|
|
13
|
+
State-changing tools (`taxi_create_order`, `taxi_cancel_order`) are
|
|
14
|
+
gated: without a trailing `--confirm` they only DRY-RUN and change
|
|
15
|
+
nothing. `--confirm` is honored only as the LAST argument.
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
python3 didi.py call maps_textsearch '{"keywords":"北京西站","city":"北京"}'
|
|
19
|
+
python3 didi.py call taxi_estimate '{"from_name":"...","from_lat":"39.9","from_lng":"116.3","to_name":"...","to_lat":"39.9","to_lng":"116.4"}'
|
|
20
|
+
python3 didi.py call taxi_create_order '{"estimate_trace_id":"...","product_category":"1"}' --confirm
|
|
21
|
+
python3 didi.py call taxi_query_order '{"order_id":"..."}'
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
import urllib.error
|
|
30
|
+
import urllib.parse
|
|
31
|
+
import urllib.request
|
|
32
|
+
|
|
33
|
+
MCP_ENDPOINT = os.environ.get("DIDI_MCP_URL", "https://mcp.didichuxing.com/mcp-servers")
|
|
34
|
+
MCP_KEY = os.environ.get("DIDI_MCP_KEY", "").strip()
|
|
35
|
+
PROTOCOL_VERSION = "2025-06-18"
|
|
36
|
+
|
|
37
|
+
# Tools that create or cancel a real ride — must be confirmed explicitly.
|
|
38
|
+
WRITE_TOOLS = {"taxi_create_order", "taxi_cancel_order"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def die(payload: dict, code: int = 1) -> None:
|
|
42
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
43
|
+
sys.exit(code)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MCPClient:
|
|
47
|
+
"""Minimal MCP Streamable-HTTP client (stdlib only).
|
|
48
|
+
|
|
49
|
+
Never logs or echoes the key; the endpoint URL (which carries the
|
|
50
|
+
key as a query param) is kept internal and never printed.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, endpoint: str, key: str) -> None:
|
|
54
|
+
sep = "&" if "?" in endpoint else "?"
|
|
55
|
+
self._url = f"{endpoint}{sep}key={urllib.parse.quote(key, safe='')}"
|
|
56
|
+
self._session_id: str | None = None
|
|
57
|
+
self._rid = 0
|
|
58
|
+
|
|
59
|
+
def _post(self, payload: dict, expect_id):
|
|
60
|
+
body = json.dumps(payload).encode("utf-8")
|
|
61
|
+
req = urllib.request.Request(self._url, data=body, method="POST")
|
|
62
|
+
req.add_header("Content-Type", "application/json")
|
|
63
|
+
req.add_header("Accept", "application/json, text/event-stream")
|
|
64
|
+
req.add_header("MCP-Protocol-Version", PROTOCOL_VERSION)
|
|
65
|
+
if self._session_id:
|
|
66
|
+
req.add_header("Mcp-Session-Id", self._session_id)
|
|
67
|
+
try:
|
|
68
|
+
resp = urllib.request.urlopen(req, timeout=90)
|
|
69
|
+
except urllib.error.HTTPError as exc:
|
|
70
|
+
detail = exc.read().decode("utf-8", "replace")[:600]
|
|
71
|
+
die(
|
|
72
|
+
{
|
|
73
|
+
"error": f"HTTP {exc.code} from DiDi MCP",
|
|
74
|
+
"detail": detail,
|
|
75
|
+
"hint": "If 401/403, the DiDi connector key is missing or expired — "
|
|
76
|
+
"reconnect at https://auth.acedata.cloud/user/connections",
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
except urllib.error.URLError as exc:
|
|
80
|
+
die({"error": "network error reaching DiDi MCP", "detail": str(exc.reason)})
|
|
81
|
+
except Exception as exc: # noqa: BLE001
|
|
82
|
+
# Never let an unexpected traceback surface — self._url carries the
|
|
83
|
+
# key as a query param (DiDi mandates ?key=), so emit only the
|
|
84
|
+
# exception type, never its message or the URL.
|
|
85
|
+
die({"error": "unexpected error calling DiDi MCP", "detail": type(exc).__name__})
|
|
86
|
+
sid = resp.headers.get("Mcp-Session-Id")
|
|
87
|
+
if sid:
|
|
88
|
+
self._session_id = sid
|
|
89
|
+
ctype = (resp.headers.get("Content-Type") or "").lower()
|
|
90
|
+
raw = resp.read().decode("utf-8", "replace")
|
|
91
|
+
return _parse_response(raw, ctype, expect_id)
|
|
92
|
+
|
|
93
|
+
def initialize(self) -> None:
|
|
94
|
+
self._rid += 1
|
|
95
|
+
result = self._post(
|
|
96
|
+
{
|
|
97
|
+
"jsonrpc": "2.0",
|
|
98
|
+
"id": self._rid,
|
|
99
|
+
"method": "initialize",
|
|
100
|
+
"params": {
|
|
101
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
102
|
+
"capabilities": {},
|
|
103
|
+
"clientInfo": {"name": "acedata-didi-skill", "version": "1.0"},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
expect_id=self._rid,
|
|
107
|
+
)
|
|
108
|
+
if isinstance(result, dict) and result.get("error"):
|
|
109
|
+
die({"error": "DiDi MCP initialize failed", "detail": result["error"]})
|
|
110
|
+
# Fire-and-forget the initialized notification (no response expected).
|
|
111
|
+
self._post({"jsonrpc": "2.0", "method": "notifications/initialized"}, expect_id=None)
|
|
112
|
+
|
|
113
|
+
def list_tools(self):
|
|
114
|
+
self._rid += 1
|
|
115
|
+
return self._post(
|
|
116
|
+
{"jsonrpc": "2.0", "id": self._rid, "method": "tools/list"},
|
|
117
|
+
expect_id=self._rid,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def call_tool(self, name: str, arguments: dict):
|
|
121
|
+
self._rid += 1
|
|
122
|
+
return self._post(
|
|
123
|
+
{
|
|
124
|
+
"jsonrpc": "2.0",
|
|
125
|
+
"id": self._rid,
|
|
126
|
+
"method": "tools/call",
|
|
127
|
+
"params": {"name": name, "arguments": arguments},
|
|
128
|
+
},
|
|
129
|
+
expect_id=self._rid,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _parse_response(raw: str, ctype: str, expect_id):
|
|
134
|
+
"""Return the JSON-RPC envelope for our request id.
|
|
135
|
+
|
|
136
|
+
Handles both plain application/json and text/event-stream (SSE),
|
|
137
|
+
where the response arrives as one or more `data: {...}` lines.
|
|
138
|
+
"""
|
|
139
|
+
messages: list[dict] = []
|
|
140
|
+
if "text/event-stream" in ctype:
|
|
141
|
+
for line in raw.splitlines():
|
|
142
|
+
line = line.strip()
|
|
143
|
+
if not line.startswith("data:"):
|
|
144
|
+
continue
|
|
145
|
+
chunk = line[5:].strip()
|
|
146
|
+
if not chunk or chunk == "[DONE]":
|
|
147
|
+
continue
|
|
148
|
+
try:
|
|
149
|
+
messages.append(json.loads(chunk))
|
|
150
|
+
except json.JSONDecodeError:
|
|
151
|
+
continue
|
|
152
|
+
else:
|
|
153
|
+
raw = raw.strip()
|
|
154
|
+
if not raw:
|
|
155
|
+
return {}
|
|
156
|
+
try:
|
|
157
|
+
messages.append(json.loads(raw))
|
|
158
|
+
except json.JSONDecodeError:
|
|
159
|
+
die({"error": "unparseable response from DiDi MCP", "detail": raw[:600]})
|
|
160
|
+
if expect_id is not None:
|
|
161
|
+
for msg in messages:
|
|
162
|
+
if isinstance(msg, dict) and msg.get("id") == expect_id:
|
|
163
|
+
return msg
|
|
164
|
+
return messages[-1] if messages else {}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def emit(envelope: dict) -> None:
|
|
168
|
+
if not isinstance(envelope, dict):
|
|
169
|
+
print(json.dumps(envelope, ensure_ascii=False, indent=2))
|
|
170
|
+
return
|
|
171
|
+
if envelope.get("error"):
|
|
172
|
+
die({"error": envelope["error"]})
|
|
173
|
+
result = envelope.get("result", envelope)
|
|
174
|
+
# tools/call results wrap text in a content[] array.
|
|
175
|
+
if isinstance(result, dict) and isinstance(result.get("content"), list):
|
|
176
|
+
texts = [
|
|
177
|
+
c.get("text", "")
|
|
178
|
+
for c in result["content"]
|
|
179
|
+
if isinstance(c, dict) and c.get("type") == "text"
|
|
180
|
+
]
|
|
181
|
+
if texts:
|
|
182
|
+
print("\n".join(texts))
|
|
183
|
+
if result.get("isError"):
|
|
184
|
+
sys.exit(1)
|
|
185
|
+
return
|
|
186
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _format_tools(envelope: dict) -> None:
|
|
190
|
+
tools = (envelope.get("result") or {}).get("tools") if isinstance(envelope, dict) else None
|
|
191
|
+
if not tools:
|
|
192
|
+
emit(envelope)
|
|
193
|
+
return
|
|
194
|
+
slim = [
|
|
195
|
+
{
|
|
196
|
+
"name": t.get("name"),
|
|
197
|
+
"description": t.get("description"),
|
|
198
|
+
"inputSchema": t.get("inputSchema"),
|
|
199
|
+
}
|
|
200
|
+
for t in tools
|
|
201
|
+
]
|
|
202
|
+
print(json.dumps(slim, ensure_ascii=False, indent=2))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def main() -> None:
|
|
206
|
+
if not MCP_KEY:
|
|
207
|
+
die(
|
|
208
|
+
{
|
|
209
|
+
"error": "DIDI_MCP_KEY not set",
|
|
210
|
+
"hint": "Connect the DiDi connector at "
|
|
211
|
+
"https://auth.acedata.cloud/user/connections to inject the key.",
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
argv = sys.argv[1:]
|
|
216
|
+
confirm = bool(argv) and argv[-1] == "--confirm"
|
|
217
|
+
if confirm:
|
|
218
|
+
argv = argv[:-1]
|
|
219
|
+
if not argv:
|
|
220
|
+
die({"error": "usage: didi.py <list|call> ..."})
|
|
221
|
+
|
|
222
|
+
cmd, rest = argv[0], argv[1:]
|
|
223
|
+
client = MCPClient(MCP_ENDPOINT, MCP_KEY)
|
|
224
|
+
|
|
225
|
+
if cmd == "list":
|
|
226
|
+
client.initialize()
|
|
227
|
+
_format_tools(client.list_tools())
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
if cmd == "call":
|
|
231
|
+
if not rest:
|
|
232
|
+
die({"error": "call needs a tool name", "usage": "didi.py call <tool> '<json-args>'"})
|
|
233
|
+
tool = rest[0]
|
|
234
|
+
args_raw = rest[1] if len(rest) > 1 else "{}"
|
|
235
|
+
try:
|
|
236
|
+
arguments = json.loads(args_raw)
|
|
237
|
+
except json.JSONDecodeError as exc:
|
|
238
|
+
die({"error": "arguments must be valid JSON", "detail": str(exc), "got": args_raw})
|
|
239
|
+
if not isinstance(arguments, dict):
|
|
240
|
+
die({"error": "arguments JSON must be an object", "got": args_raw})
|
|
241
|
+
|
|
242
|
+
if tool in WRITE_TOOLS and not confirm:
|
|
243
|
+
print(
|
|
244
|
+
json.dumps(
|
|
245
|
+
{
|
|
246
|
+
"dry_run": True,
|
|
247
|
+
"tool": tool,
|
|
248
|
+
"arguments": arguments,
|
|
249
|
+
"note": "state-changing call — re-run with --confirm as the LAST "
|
|
250
|
+
"argument to actually perform it",
|
|
251
|
+
},
|
|
252
|
+
ensure_ascii=False,
|
|
253
|
+
indent=2,
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
client.initialize()
|
|
259
|
+
emit(client.call_tool(tool, arguments))
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
die({"error": f"unknown command: {cmd}", "usage": "didi.py <list|call> ..."})
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == "__main__":
|
|
266
|
+
main()
|