indesign-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli_anything/indesign/README.md +32 -0
- cli_anything/indesign/__init__.py +1 -0
- cli_anything/indesign/__main__.py +5 -0
- cli_anything/indesign/core/artifacts.py +57 -0
- cli_anything/indesign/core/catalog.py +405 -0
- cli_anything/indesign/core/domains.py +178 -0
- cli_anything/indesign/core/envelope.py +65 -0
- cli_anything/indesign/core/errors.py +30 -0
- cli_anything/indesign/core/health.py +46 -0
- cli_anything/indesign/core/hidden_backend.py +116 -0
- cli_anything/indesign/core/hidden_handler_schemas.py +223 -0
- cli_anything/indesign/core/mcp_backend.py +152 -0
- cli_anything/indesign/core/node_setup.py +35 -0
- cli_anything/indesign/core/paths.py +41 -0
- cli_anything/indesign/core/plugins/__init__.py +2 -0
- cli_anything/indesign/core/plugins/backend.py +90 -0
- cli_anything/indesign/core/plugins/discovery.py +69 -0
- cli_anything/indesign/core/plugins/host_actions.py +76 -0
- cli_anything/indesign/core/plugins/install.py +38 -0
- cli_anything/indesign/core/plugins/manifest.py +279 -0
- cli_anything/indesign/core/plugins/validate.py +181 -0
- cli_anything/indesign/core/router.py +217 -0
- cli_anything/indesign/core/runtime.py +59 -0
- cli_anything/indesign/core/scripts.py +44 -0
- cli_anything/indesign/core/session.py +68 -0
- cli_anything/indesign/indesign_cli.py +320 -0
- cli_anything/indesign/node/hidden_handler_bridge.mjs +111 -0
- cli_anything/indesign/server/package-lock.json +168 -0
- cli_anything/indesign/server/package.json +45 -0
- cli_anything/indesign/server/src/advanced/index.js +76 -0
- cli_anything/indesign/server/src/core/InDesignMCPServer.js +273 -0
- cli_anything/indesign/server/src/core/scriptExecutor.js +271 -0
- cli_anything/indesign/server/src/core/sessionManager.js +545 -0
- cli_anything/indesign/server/src/handlers/advancedTemplateHandlers.js +1072 -0
- cli_anything/indesign/server/src/handlers/bookHandlers.js +490 -0
- cli_anything/indesign/server/src/handlers/documentHandlers.js +1472 -0
- cli_anything/indesign/server/src/handlers/exportHandlers.js +208 -0
- cli_anything/indesign/server/src/handlers/graphicsHandlers.js +605 -0
- cli_anything/indesign/server/src/handlers/groupHandlers.js +358 -0
- cli_anything/indesign/server/src/handlers/helpHandlers.js +347 -0
- cli_anything/indesign/server/src/handlers/index.js +77 -0
- cli_anything/indesign/server/src/handlers/layerHandlers.js +75 -0
- cli_anything/indesign/server/src/handlers/masterSpreadHandlers.js +451 -0
- cli_anything/indesign/server/src/handlers/pageHandlers.js +698 -0
- cli_anything/indesign/server/src/handlers/pageItemHandlers.js +704 -0
- cli_anything/indesign/server/src/handlers/presentationHandlers.js +220 -0
- cli_anything/indesign/server/src/handlers/spreadHandlers.js +348 -0
- cli_anything/indesign/server/src/handlers/styleHandlers.js +458 -0
- cli_anything/indesign/server/src/handlers/textHandlers.js +431 -0
- cli_anything/indesign/server/src/handlers/utilityHandlers.js +83 -0
- cli_anything/indesign/server/src/index.js +17 -0
- cli_anything/indesign/server/src/types/index.js +106 -0
- cli_anything/indesign/server/src/types/toolDefinitionsAdvancedTemplates.js +144 -0
- cli_anything/indesign/server/src/types/toolDefinitionsBook.js +224 -0
- cli_anything/indesign/server/src/types/toolDefinitionsContent.js +353 -0
- cli_anything/indesign/server/src/types/toolDefinitionsDocument.js +409 -0
- cli_anything/indesign/server/src/types/toolDefinitionsExport.js +65 -0
- cli_anything/indesign/server/src/types/toolDefinitionsLayer.js +40 -0
- cli_anything/indesign/server/src/types/toolDefinitionsMasterSpread.js +160 -0
- cli_anything/indesign/server/src/types/toolDefinitionsPage.js +271 -0
- cli_anything/indesign/server/src/types/toolDefinitionsPageItemGroup.js +437 -0
- cli_anything/indesign/server/src/types/toolDefinitionsPresentation.js +83 -0
- cli_anything/indesign/server/src/types/toolDefinitionsSpread.js +158 -0
- cli_anything/indesign/server/src/types/toolDefinitionsUtility.js +40 -0
- cli_anything/indesign/server/src/utils/stringUtils.js +107 -0
- cli_anything/indesign/skills/SKILL.md +198 -0
- indesign_cli-0.2.0.dist-info/METADATA +267 -0
- indesign_cli-0.2.0.dist-info/RECORD +72 -0
- indesign_cli-0.2.0.dist-info/WHEEL +5 -0
- indesign_cli-0.2.0.dist-info/entry_points.txt +3 -0
- indesign_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- indesign_cli-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# indesign-cli
|
|
2
|
+
|
|
3
|
+
Agent 专用 InDesign CLI harness。
|
|
4
|
+
|
|
5
|
+
常用命令:
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
indesign-cli --version
|
|
9
|
+
indesign-cli tool domains
|
|
10
|
+
indesign-cli tool list --domain template
|
|
11
|
+
indesign-cli tool schema template.run_jsx_file
|
|
12
|
+
indesign-cli tool call template.run_jsx_file --args args.json
|
|
13
|
+
indesign-cli script run scripts/check.jsx
|
|
14
|
+
indesign-cli script run --stdin
|
|
15
|
+
indesign-cli export verify output/result.pdf
|
|
16
|
+
indesign-cli server setup
|
|
17
|
+
indesign-cli server health
|
|
18
|
+
indesign-cli session show
|
|
19
|
+
indesign-cli skill install --target D:\AI\html-indesign
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`cli-anything-indesign` 仍可作为旧项目兼容别名使用。
|
|
23
|
+
|
|
24
|
+
`script run` 是 Agent 做真实 InDesign 验证的主入口:
|
|
25
|
+
|
|
26
|
+
- 文件模式保留 `$.fileName` 和相对 `#include` 行为。
|
|
27
|
+
- stdin 模式适合临时探针脚本,并支持中文输入。
|
|
28
|
+
- JSX 可以返回普通字符串,也可以返回 `JSON.stringify(...)` 的字符串。
|
|
29
|
+
- 返回 JSON 字符串时,CLI 输出会包含 `data.result_json`,避免调用方二次解析 `data.parsed.result`。
|
|
30
|
+
- 成功和失败都会记录到当前目录的 `.indesign-cli/session.json`。
|
|
31
|
+
|
|
32
|
+
本 CLI 复用当前项目的 MCP server 和 ExtendScript/COM 执行链路,不重新实现 InDesign 自动化能力。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import zipfile
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .errors import CliError
|
|
10
|
+
from .paths import scrub_path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_timestamp(value: str) -> datetime:
|
|
14
|
+
normalized = value.strip()
|
|
15
|
+
if normalized.endswith("Z"):
|
|
16
|
+
normalized = normalized[:-1] + "+00:00"
|
|
17
|
+
normalized = re.sub(r"(\.\d{6})\d+([+-]\d{2}:\d{2})$", r"\1\2", normalized)
|
|
18
|
+
return datetime.fromisoformat(normalized)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def verify_artifact(path: Path, created_after: datetime | None = None, cwd: Path | None = None) -> dict[str, Any]:
|
|
22
|
+
path_info = scrub_path(str(path), cwd or Path.cwd())
|
|
23
|
+
if not path.exists():
|
|
24
|
+
raise CliError("Artifact not found", code="ARTIFACT_NOT_FOUND", details={"path": path_info})
|
|
25
|
+
stat = path.stat()
|
|
26
|
+
if stat.st_size <= 0:
|
|
27
|
+
raise CliError("Artifact is empty", code="ARTIFACT_EMPTY", details={"path": path_info})
|
|
28
|
+
if created_after and datetime.fromtimestamp(stat.st_mtime, created_after.tzinfo) < created_after:
|
|
29
|
+
raise CliError("Artifact is older than expected", code="ARTIFACT_TOO_OLD", details={"path": path_info})
|
|
30
|
+
|
|
31
|
+
suffix = path.suffix.lower()
|
|
32
|
+
if suffix == ".pdf":
|
|
33
|
+
with path.open("rb") as handle:
|
|
34
|
+
if handle.read(4) != b"%PDF":
|
|
35
|
+
raise CliError("PDF signature is invalid", code="ARTIFACT_SIGNATURE_INVALID")
|
|
36
|
+
return {
|
|
37
|
+
"path": path_info,
|
|
38
|
+
"kind": "pdf",
|
|
39
|
+
"size_bytes": stat.st_size,
|
|
40
|
+
"signature_ok": True,
|
|
41
|
+
"mtime": stat.st_mtime,
|
|
42
|
+
}
|
|
43
|
+
if suffix == ".idml":
|
|
44
|
+
try:
|
|
45
|
+
with zipfile.ZipFile(path) as archive:
|
|
46
|
+
if "designmap.xml" not in archive.namelist():
|
|
47
|
+
raise CliError("IDML designmap.xml missing", code="ARTIFACT_SIGNATURE_INVALID")
|
|
48
|
+
except zipfile.BadZipFile as exc:
|
|
49
|
+
raise CliError("IDML ZIP structure is invalid", code="ARTIFACT_SIGNATURE_INVALID") from exc
|
|
50
|
+
return {
|
|
51
|
+
"path": path_info,
|
|
52
|
+
"kind": "idml",
|
|
53
|
+
"size_bytes": stat.st_size,
|
|
54
|
+
"signature_ok": True,
|
|
55
|
+
"mtime": stat.st_mtime,
|
|
56
|
+
}
|
|
57
|
+
raise CliError(f"Unsupported artifact type: {suffix}", code="ARTIFACT_UNSUPPORTED")
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .domains import DOMAINS, infer_domain
|
|
9
|
+
from .errors import CliError
|
|
10
|
+
from .hidden_handler_schemas import HIDDEN_HANDLER_SCHEMAS
|
|
11
|
+
from .plugins.manifest import PluginRecord
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
CLI_PRIMITIVES = [
|
|
15
|
+
{
|
|
16
|
+
"id": "export.verify",
|
|
17
|
+
"domain": "export",
|
|
18
|
+
"name": "verify",
|
|
19
|
+
"one_line_purpose": "验证 PDF 或 IDML 产物是否存在且格式正确",
|
|
20
|
+
"arg_names": ["path", "created_after"],
|
|
21
|
+
"source": "cli",
|
|
22
|
+
"rank": 1,
|
|
23
|
+
"schema_size": "small",
|
|
24
|
+
"availability": "exposed",
|
|
25
|
+
"callable": True,
|
|
26
|
+
"requires": [],
|
|
27
|
+
"side_effects": [],
|
|
28
|
+
"artifact_kinds": ["pdf", "idml"],
|
|
29
|
+
"destructive": False,
|
|
30
|
+
"target_scope": "filesystem",
|
|
31
|
+
"needs_indesign": False,
|
|
32
|
+
"produces_artifacts": False,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "server.health",
|
|
36
|
+
"domain": "server",
|
|
37
|
+
"name": "health",
|
|
38
|
+
"one_line_purpose": "检查 CLI、Node 入口和可选 InDesign 后端状态",
|
|
39
|
+
"arg_names": ["deep"],
|
|
40
|
+
"source": "cli",
|
|
41
|
+
"rank": 1,
|
|
42
|
+
"schema_size": "small",
|
|
43
|
+
"availability": "exposed",
|
|
44
|
+
"callable": True,
|
|
45
|
+
"requires": [],
|
|
46
|
+
"side_effects": [],
|
|
47
|
+
"artifact_kinds": [],
|
|
48
|
+
"destructive": False,
|
|
49
|
+
"target_scope": "project",
|
|
50
|
+
"needs_indesign": False,
|
|
51
|
+
"produces_artifacts": False,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "server.setup",
|
|
55
|
+
"domain": "server",
|
|
56
|
+
"name": "setup",
|
|
57
|
+
"one_line_purpose": "在 CLI 内置 server 目录执行 npm install",
|
|
58
|
+
"arg_names": [],
|
|
59
|
+
"source": "cli",
|
|
60
|
+
"rank": 2,
|
|
61
|
+
"schema_size": "small",
|
|
62
|
+
"availability": "exposed",
|
|
63
|
+
"callable": True,
|
|
64
|
+
"requires": ["node", "npm"],
|
|
65
|
+
"side_effects": ["filesystem_write"],
|
|
66
|
+
"artifact_kinds": [],
|
|
67
|
+
"destructive": False,
|
|
68
|
+
"target_scope": "project",
|
|
69
|
+
"needs_indesign": False,
|
|
70
|
+
"produces_artifacts": False,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "session.show",
|
|
74
|
+
"domain": "session",
|
|
75
|
+
"name": "show",
|
|
76
|
+
"one_line_purpose": "读取当前工作目录下的精简 CLI session",
|
|
77
|
+
"arg_names": ["verbose"],
|
|
78
|
+
"source": "cli",
|
|
79
|
+
"rank": 1,
|
|
80
|
+
"schema_size": "small",
|
|
81
|
+
"availability": "exposed",
|
|
82
|
+
"callable": True,
|
|
83
|
+
"requires": [],
|
|
84
|
+
"side_effects": [],
|
|
85
|
+
"artifact_kinds": [],
|
|
86
|
+
"destructive": False,
|
|
87
|
+
"target_scope": "workspace",
|
|
88
|
+
"needs_indesign": False,
|
|
89
|
+
"produces_artifacts": False,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"id": "session.clear",
|
|
93
|
+
"domain": "session",
|
|
94
|
+
"name": "clear",
|
|
95
|
+
"one_line_purpose": "清空当前工作目录下的 CLI session",
|
|
96
|
+
"arg_names": [],
|
|
97
|
+
"source": "cli",
|
|
98
|
+
"rank": 2,
|
|
99
|
+
"schema_size": "small",
|
|
100
|
+
"availability": "exposed",
|
|
101
|
+
"callable": True,
|
|
102
|
+
"requires": [],
|
|
103
|
+
"side_effects": ["session_write"],
|
|
104
|
+
"artifact_kinds": [],
|
|
105
|
+
"destructive": True,
|
|
106
|
+
"target_scope": "workspace",
|
|
107
|
+
"needs_indesign": False,
|
|
108
|
+
"produces_artifacts": False,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "script.run",
|
|
112
|
+
"domain": "script",
|
|
113
|
+
"name": "run",
|
|
114
|
+
"one_line_purpose": "执行 JSX 文件或 stdin 临时脚本",
|
|
115
|
+
"arg_names": ["file", "stdin", "timeout"],
|
|
116
|
+
"source": "script",
|
|
117
|
+
"rank": 1,
|
|
118
|
+
"schema_size": "small",
|
|
119
|
+
"availability": "exposed",
|
|
120
|
+
"callable": True,
|
|
121
|
+
"requires": ["indesign_com"],
|
|
122
|
+
"side_effects": ["indesign_mutation"],
|
|
123
|
+
"artifact_kinds": [],
|
|
124
|
+
"destructive": False,
|
|
125
|
+
"target_scope": "indesign",
|
|
126
|
+
"needs_indesign": True,
|
|
127
|
+
"produces_artifacts": False,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"id": "skill.install",
|
|
131
|
+
"domain": "skill",
|
|
132
|
+
"name": "install",
|
|
133
|
+
"one_line_purpose": "把内置 indesign-cli skill 安装到目标项目",
|
|
134
|
+
"arg_names": ["target"],
|
|
135
|
+
"source": "cli",
|
|
136
|
+
"rank": 1,
|
|
137
|
+
"schema_size": "small",
|
|
138
|
+
"availability": "exposed",
|
|
139
|
+
"callable": True,
|
|
140
|
+
"requires": [],
|
|
141
|
+
"side_effects": ["filesystem_write"],
|
|
142
|
+
"artifact_kinds": [],
|
|
143
|
+
"destructive": False,
|
|
144
|
+
"target_scope": "workspace",
|
|
145
|
+
"needs_indesign": False,
|
|
146
|
+
"produces_artifacts": True,
|
|
147
|
+
},
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
HIDDEN_HANDLER_FILES = {
|
|
152
|
+
"book": "src/handlers/bookHandlers.js",
|
|
153
|
+
"presentation": "src/handlers/presentationHandlers.js",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
VALID_SOURCES = {"cli", "script", "advanced", "classic", "hidden_handler", "plugin"}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _camel_to_snake(value: str) -> str:
|
|
160
|
+
value = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", value)
|
|
161
|
+
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", value).lower()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _schema_size(schema: dict[str, Any]) -> str:
|
|
165
|
+
count = len(schema.get("properties", {}))
|
|
166
|
+
if count <= 3:
|
|
167
|
+
return "small"
|
|
168
|
+
if count <= 8:
|
|
169
|
+
return "medium"
|
|
170
|
+
return "large"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _side_effects(tool_name: str, domain: str) -> list[str]:
|
|
174
|
+
if tool_name.startswith(("get_", "list_", "inspect_", "find_", "search_")):
|
|
175
|
+
return []
|
|
176
|
+
if domain == "export" or "export" in tool_name or "package" in tool_name:
|
|
177
|
+
return ["filesystem_write"]
|
|
178
|
+
return ["indesign_mutation"]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _artifact_kinds(tool_name: str) -> list[str]:
|
|
182
|
+
kinds: list[str] = []
|
|
183
|
+
lowered = tool_name.lower()
|
|
184
|
+
if "pdf" in lowered:
|
|
185
|
+
kinds.append("pdf")
|
|
186
|
+
if "idml" in lowered:
|
|
187
|
+
kinds.append("idml")
|
|
188
|
+
if "image" in lowered or "png" in lowered or "jpg" in lowered:
|
|
189
|
+
kinds.append("image")
|
|
190
|
+
if "epub" in lowered:
|
|
191
|
+
kinds.append("epub")
|
|
192
|
+
return kinds
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _target_scope(domain: str, tool_name: str) -> str:
|
|
196
|
+
if domain == "export":
|
|
197
|
+
return "filesystem"
|
|
198
|
+
if "document" in tool_name or domain == "document":
|
|
199
|
+
return "active_document"
|
|
200
|
+
if domain in {"server", "session"}:
|
|
201
|
+
return "workspace"
|
|
202
|
+
return "indesign"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def exposed_tool_entries(tools: list[dict[str, Any]], source: str) -> list[dict[str, Any]]:
|
|
206
|
+
entries: list[dict[str, Any]] = []
|
|
207
|
+
for index, tool in enumerate(tools):
|
|
208
|
+
name = tool["name"]
|
|
209
|
+
description = tool.get("description", "")
|
|
210
|
+
domain = infer_domain(name, description)
|
|
211
|
+
schema = tool.get("inputSchema", {})
|
|
212
|
+
arg_names = list(schema.get("properties", {}).keys())
|
|
213
|
+
artifact_kinds = _artifact_kinds(name)
|
|
214
|
+
entries.append(
|
|
215
|
+
{
|
|
216
|
+
"id": f"{domain}.{name}",
|
|
217
|
+
"domain": domain,
|
|
218
|
+
"name": name,
|
|
219
|
+
"one_line_purpose": description.splitlines()[0] if description else name,
|
|
220
|
+
"arg_names": arg_names,
|
|
221
|
+
"source": source,
|
|
222
|
+
"rank": (10 if source == "advanced" else 20) + index,
|
|
223
|
+
"schema_size": _schema_size(schema),
|
|
224
|
+
"availability": "exposed",
|
|
225
|
+
"callable": True,
|
|
226
|
+
"requires": ["indesign_com"],
|
|
227
|
+
"side_effects": _side_effects(name, domain),
|
|
228
|
+
"artifact_kinds": artifact_kinds,
|
|
229
|
+
"destructive": any(part in name for part in ("delete", "clear", "close")),
|
|
230
|
+
"target_scope": _target_scope(domain, name),
|
|
231
|
+
"needs_indesign": True,
|
|
232
|
+
"produces_artifacts": bool(artifact_kinds),
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
return entries
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def plugin_tool_entries(record: PluginRecord, tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
239
|
+
entries: list[dict[str, Any]] = []
|
|
240
|
+
for index, tool in enumerate(tools):
|
|
241
|
+
tool_id = str(tool.get("id") or "")
|
|
242
|
+
name = str(tool.get("name") or tool_id.split(".", 1)[-1])
|
|
243
|
+
entries.append(
|
|
244
|
+
{
|
|
245
|
+
"id": tool_id,
|
|
246
|
+
"domain": str(tool.get("domain") or record.domain),
|
|
247
|
+
"name": name,
|
|
248
|
+
"one_line_purpose": str(tool.get("one_line_purpose") or tool.get("description") or name),
|
|
249
|
+
"arg_names": list(tool.get("arg_names") or []),
|
|
250
|
+
"source": "plugin",
|
|
251
|
+
"plugin": record.id,
|
|
252
|
+
"rank": int(tool.get("rank") or (70 + index)),
|
|
253
|
+
"schema_size": str(tool.get("schema_size") or "medium"),
|
|
254
|
+
"availability": "exposed",
|
|
255
|
+
"callable": bool(tool.get("callable", True)),
|
|
256
|
+
"requires": list(tool.get("requires") or []),
|
|
257
|
+
"side_effects": list(tool.get("side_effects") or []),
|
|
258
|
+
"artifact_kinds": list(tool.get("artifact_kinds") or []),
|
|
259
|
+
"destructive": bool(tool.get("destructive", False)),
|
|
260
|
+
"target_scope": str(tool.get("target_scope") or "workspace"),
|
|
261
|
+
"needs_indesign": bool(tool.get("needs_indesign", False)),
|
|
262
|
+
"produces_artifacts": bool(tool.get("produces_artifacts", False)),
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
return entries
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class Catalog:
|
|
269
|
+
def __init__(
|
|
270
|
+
self,
|
|
271
|
+
repo_root: Path,
|
|
272
|
+
tools: list[dict[str, Any]] | None = None,
|
|
273
|
+
domains: dict[str, str] | None = None,
|
|
274
|
+
plugin_records: dict[str, PluginRecord] | None = None,
|
|
275
|
+
) -> None:
|
|
276
|
+
self.repo_root = repo_root
|
|
277
|
+
self._tools = tools or [*CLI_PRIMITIVES, *self._hidden_handler_entries()]
|
|
278
|
+
self._domains = domains or dict(DOMAINS)
|
|
279
|
+
self._plugin_records = plugin_records or {}
|
|
280
|
+
|
|
281
|
+
def with_exposed_tools(
|
|
282
|
+
self,
|
|
283
|
+
*,
|
|
284
|
+
advanced_tools: list[dict[str, Any]] | None = None,
|
|
285
|
+
classic_tools: list[dict[str, Any]] | None = None,
|
|
286
|
+
plugin_tools: list[dict[str, Any]] | None = None,
|
|
287
|
+
plugin_domain_summaries: dict[str, str] | None = None,
|
|
288
|
+
plugin_records: dict[str, PluginRecord] | None = None,
|
|
289
|
+
) -> "Catalog":
|
|
290
|
+
tools = [*CLI_PRIMITIVES]
|
|
291
|
+
tools.extend(exposed_tool_entries(advanced_tools or [], "advanced"))
|
|
292
|
+
tools.extend(exposed_tool_entries(classic_tools or [], "classic"))
|
|
293
|
+
exposed_ids = {tool["id"] for tool in tools}
|
|
294
|
+
tools.extend(tool for tool in self._hidden_handler_entries() if tool["id"] not in exposed_ids)
|
|
295
|
+
existing_ids = {tool["id"] for tool in tools}
|
|
296
|
+
for plugin_tool in plugin_tools or []:
|
|
297
|
+
if plugin_tool["id"] in existing_ids:
|
|
298
|
+
raise CliError("Plugin tool id conflicts with an existing tool", code="PLUGIN_TOOL_CONFLICT", details={"tool_id": plugin_tool["id"]})
|
|
299
|
+
existing_ids.add(plugin_tool["id"])
|
|
300
|
+
tools.append(plugin_tool)
|
|
301
|
+
domains = dict(DOMAINS)
|
|
302
|
+
domains.update(plugin_domain_summaries or {})
|
|
303
|
+
return Catalog(repo_root=self.repo_root, tools=tools, domains=domains, plugin_records=plugin_records or {})
|
|
304
|
+
|
|
305
|
+
def domains(self) -> list[dict[str, Any]]:
|
|
306
|
+
source_counts: dict[str, Counter[str]] = {domain: Counter() for domain in self._domains}
|
|
307
|
+
top_tools: dict[str, list[dict[str, Any]]] = {domain: [] for domain in self._domains}
|
|
308
|
+
for tool in self._tools:
|
|
309
|
+
domain = tool["domain"]
|
|
310
|
+
source_counts.setdefault(domain, Counter())[tool["source"]] += 1
|
|
311
|
+
top_tools.setdefault(domain, []).append(tool)
|
|
312
|
+
|
|
313
|
+
result = []
|
|
314
|
+
for domain, summary in self._domains.items():
|
|
315
|
+
ranked = sorted(top_tools.get(domain, []), key=lambda item: (item["rank"], item["id"]))
|
|
316
|
+
callable_ranked = [item for item in ranked if item["callable"]]
|
|
317
|
+
result.append(
|
|
318
|
+
{
|
|
319
|
+
"domain": domain,
|
|
320
|
+
"summary": summary,
|
|
321
|
+
"count_by_source": dict(source_counts.get(domain, Counter())),
|
|
322
|
+
"top_tools": [item["id"] for item in callable_ranked[:5]],
|
|
323
|
+
}
|
|
324
|
+
)
|
|
325
|
+
return result
|
|
326
|
+
|
|
327
|
+
def list_tools(
|
|
328
|
+
self,
|
|
329
|
+
*,
|
|
330
|
+
domain: str | None = None,
|
|
331
|
+
source: str | None = None,
|
|
332
|
+
callable_only: bool = False,
|
|
333
|
+
query: str | None = None,
|
|
334
|
+
) -> list[dict[str, Any]]:
|
|
335
|
+
tools = self._tools
|
|
336
|
+
if domain:
|
|
337
|
+
if domain not in self._domains:
|
|
338
|
+
raise CliError(
|
|
339
|
+
f"Unknown domain: {domain}",
|
|
340
|
+
code="DOMAIN_NOT_FOUND",
|
|
341
|
+
details={"domain": domain, "available": list(self._domains)},
|
|
342
|
+
)
|
|
343
|
+
tools = [tool for tool in tools if tool["domain"] == domain]
|
|
344
|
+
if source:
|
|
345
|
+
if source not in VALID_SOURCES:
|
|
346
|
+
raise CliError(
|
|
347
|
+
f"Unknown source: {source}",
|
|
348
|
+
code="SOURCE_NOT_FOUND",
|
|
349
|
+
details={"source": source, "available": sorted(VALID_SOURCES)},
|
|
350
|
+
)
|
|
351
|
+
tools = [tool for tool in tools if tool["source"] == source]
|
|
352
|
+
if callable_only:
|
|
353
|
+
tools = [tool for tool in tools if tool["callable"]]
|
|
354
|
+
if query:
|
|
355
|
+
needle = query.lower()
|
|
356
|
+
tools = [
|
|
357
|
+
tool
|
|
358
|
+
for tool in tools
|
|
359
|
+
if needle in tool["id"].lower()
|
|
360
|
+
or needle in tool["name"].lower()
|
|
361
|
+
or needle in tool["one_line_purpose"].lower()
|
|
362
|
+
]
|
|
363
|
+
return sorted(tools, key=lambda item: (item["rank"], item["id"]))
|
|
364
|
+
|
|
365
|
+
def plugin_record(self, plugin_id: str) -> PluginRecord:
|
|
366
|
+
try:
|
|
367
|
+
return self._plugin_records[plugin_id]
|
|
368
|
+
except KeyError as exc:
|
|
369
|
+
raise CliError("Plugin record missing from catalog", code="PLUGIN_RECORD_NOT_FOUND", details={"plugin": plugin_id}) from exc
|
|
370
|
+
|
|
371
|
+
def _hidden_handler_entries(self) -> list[dict[str, Any]]:
|
|
372
|
+
entries: list[dict[str, Any]] = []
|
|
373
|
+
for domain, relative_path in HIDDEN_HANDLER_FILES.items():
|
|
374
|
+
path = self.repo_root / relative_path
|
|
375
|
+
if not path.exists():
|
|
376
|
+
continue
|
|
377
|
+
content = path.read_text(encoding="utf-8")
|
|
378
|
+
for method in re.findall(r"static\s+async\s+([A-Za-z0-9_]+)\s*\(", content):
|
|
379
|
+
name = _camel_to_snake(method)
|
|
380
|
+
tool_id = f"{domain}.{name}"
|
|
381
|
+
schema = HIDDEN_HANDLER_SCHEMAS.get(tool_id, {"type": "object", "properties": {}})
|
|
382
|
+
arg_names = list(schema.get("properties", {}).keys())
|
|
383
|
+
callable_handler = tool_id in HIDDEN_HANDLER_SCHEMAS
|
|
384
|
+
entries.append(
|
|
385
|
+
{
|
|
386
|
+
"id": tool_id,
|
|
387
|
+
"domain": domain,
|
|
388
|
+
"name": name,
|
|
389
|
+
"one_line_purpose": f"调用已有 {domain} handler 能力",
|
|
390
|
+
"arg_names": arg_names,
|
|
391
|
+
"source": "hidden_handler",
|
|
392
|
+
"rank": 90,
|
|
393
|
+
"schema_size": _schema_size(schema) if callable_handler else "unknown",
|
|
394
|
+
"availability": "exposed" if callable_handler else "hidden_handler",
|
|
395
|
+
"callable": callable_handler,
|
|
396
|
+
"requires": ["indesign_com"],
|
|
397
|
+
"side_effects": ["indesign_mutation"],
|
|
398
|
+
"artifact_kinds": _artifact_kinds(name),
|
|
399
|
+
"destructive": any(part in name for part in ("delete", "clear", "close")),
|
|
400
|
+
"target_scope": _target_scope(domain, name),
|
|
401
|
+
"needs_indesign": True,
|
|
402
|
+
"produces_artifacts": bool(_artifact_kinds(name)),
|
|
403
|
+
}
|
|
404
|
+
)
|
|
405
|
+
return entries
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
DOMAINS = {
|
|
5
|
+
"template": "模板槽位、脚本标签、母版占位和模板填充",
|
|
6
|
+
"document": "打开、保存、关闭、文档信息",
|
|
7
|
+
"page": "页面、页面尺寸和页面基础操作",
|
|
8
|
+
"spread": "跨页、跨页布局和跨页范围操作",
|
|
9
|
+
"master": "母版、母版跨页和母版对象",
|
|
10
|
+
"layer": "图层创建、查询、锁定、显示和删除",
|
|
11
|
+
"object": "页面对象、对象组、几何位置、脚本标签",
|
|
12
|
+
"text": "文本框、文本内容、段落和字符操作",
|
|
13
|
+
"graphics": "图片、图形框、适配和基础绘制",
|
|
14
|
+
"style": "段落样式、字符样式、对象样式",
|
|
15
|
+
"export": "PDF、IDML、图片等导出和产物验证",
|
|
16
|
+
"book": "InDesign Book 文件、章节和书籍级同步",
|
|
17
|
+
"presentation": "演示型版面、页面序列和 presentation handler 能力",
|
|
18
|
+
"script": "JSX 文件执行和 stdin 临时脚本",
|
|
19
|
+
"session": "CLI 本地状态、最近文档和最近输出",
|
|
20
|
+
"server": "依赖、后端、InDesign COM 健康检查",
|
|
21
|
+
"skill": "把内置 Agent skill 安装到目标项目",
|
|
22
|
+
"utility": "难以归入以上域的辅助能力",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
EXACT_TOOL_DOMAINS = {
|
|
27
|
+
"run_jsx_file": "template",
|
|
28
|
+
"inspect_template_blueprint": "template",
|
|
29
|
+
"list_template_blueprints": "template",
|
|
30
|
+
"create_page_with_template": "template",
|
|
31
|
+
"populate_template_slots": "template",
|
|
32
|
+
"execute_indesign_code": "script",
|
|
33
|
+
"help": "utility",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
NAME_DOMAIN_RULES = [
|
|
38
|
+
("export_", "export"),
|
|
39
|
+
("package_", "export"),
|
|
40
|
+
("create_presentation_", "presentation"),
|
|
41
|
+
("add_cover_page", "presentation"),
|
|
42
|
+
("add_section_page", "presentation"),
|
|
43
|
+
("add_full_bleed_image", "presentation"),
|
|
44
|
+
("add_image_grid", "presentation"),
|
|
45
|
+
("master_", "master"),
|
|
46
|
+
("create_master_", "master"),
|
|
47
|
+
("delete_master_", "master"),
|
|
48
|
+
("duplicate_master_", "master"),
|
|
49
|
+
("apply_master_", "master"),
|
|
50
|
+
("get_master_", "master"),
|
|
51
|
+
("detach_master_", "master"),
|
|
52
|
+
("remove_master_", "master"),
|
|
53
|
+
("list_spreads", "spread"),
|
|
54
|
+
("get_spread_", "spread"),
|
|
55
|
+
("duplicate_spread", "spread"),
|
|
56
|
+
("move_spread", "spread"),
|
|
57
|
+
("delete_spread", "spread"),
|
|
58
|
+
("set_spread_", "spread"),
|
|
59
|
+
("create_spread_", "spread"),
|
|
60
|
+
("place_file_on_spread", "spread"),
|
|
61
|
+
("place_xml_on_spread", "spread"),
|
|
62
|
+
("select_spread", "spread"),
|
|
63
|
+
("add_page", "page"),
|
|
64
|
+
("delete_page", "page"),
|
|
65
|
+
("duplicate_page", "page"),
|
|
66
|
+
("navigate_to_page", "page"),
|
|
67
|
+
("get_page_", "page"),
|
|
68
|
+
("move_page", "page"),
|
|
69
|
+
("set_page_", "page"),
|
|
70
|
+
("adjust_page_", "page"),
|
|
71
|
+
("resize_page", "page"),
|
|
72
|
+
("create_page_", "page"),
|
|
73
|
+
("place_file_on_page", "page"),
|
|
74
|
+
("place_xml_on_page", "page"),
|
|
75
|
+
("snapshot_page_", "page"),
|
|
76
|
+
("delete_page_layout_", "page"),
|
|
77
|
+
("reframe_page", "page"),
|
|
78
|
+
("select_page", "page"),
|
|
79
|
+
("create_layer", "layer"),
|
|
80
|
+
("set_active_layer", "layer"),
|
|
81
|
+
("list_layers", "layer"),
|
|
82
|
+
("create_text_", "text"),
|
|
83
|
+
("edit_text_", "text"),
|
|
84
|
+
("create_table", "text"),
|
|
85
|
+
("populate_table", "text"),
|
|
86
|
+
("find_replace_text", "text"),
|
|
87
|
+
("find_text_", "text"),
|
|
88
|
+
("create_paragraph_style", "style"),
|
|
89
|
+
("create_character_style", "style"),
|
|
90
|
+
("apply_paragraph_style", "style"),
|
|
91
|
+
("apply_character_style", "style"),
|
|
92
|
+
("create_object_style", "style"),
|
|
93
|
+
("list_object_styles", "style"),
|
|
94
|
+
("apply_object_style", "style"),
|
|
95
|
+
("apply_color", "style"),
|
|
96
|
+
("create_color_swatch", "style"),
|
|
97
|
+
("list_color_swatches", "style"),
|
|
98
|
+
("create_rectangle", "graphics"),
|
|
99
|
+
("create_ellipse", "graphics"),
|
|
100
|
+
("create_polygon", "graphics"),
|
|
101
|
+
("place_image", "graphics"),
|
|
102
|
+
("get_image_", "graphics"),
|
|
103
|
+
("create_group", "object"),
|
|
104
|
+
("ungroup", "object"),
|
|
105
|
+
("get_group_", "object"),
|
|
106
|
+
("add_items_to_group", "object"),
|
|
107
|
+
("remove_items_from_group", "object"),
|
|
108
|
+
("get_all_page_items", "object"),
|
|
109
|
+
("get_page_item_", "object"),
|
|
110
|
+
("move_page_item", "object"),
|
|
111
|
+
("resize_page_item", "object"),
|
|
112
|
+
("rotate_page_item", "object"),
|
|
113
|
+
("delete_page_item", "object"),
|
|
114
|
+
("set_page_item_", "object"),
|
|
115
|
+
("get_page_items_by_label", "object"),
|
|
116
|
+
("set_page_item_label", "object"),
|
|
117
|
+
("get_document_info", "document"),
|
|
118
|
+
("create_document", "document"),
|
|
119
|
+
("open_document", "document"),
|
|
120
|
+
("save_document", "document"),
|
|
121
|
+
("close_document", "document"),
|
|
122
|
+
("preflight_document", "document"),
|
|
123
|
+
("data_merge", "document"),
|
|
124
|
+
("get_document_", "document"),
|
|
125
|
+
("set_document_", "document"),
|
|
126
|
+
("organize_document_", "document"),
|
|
127
|
+
("create_document_", "document"),
|
|
128
|
+
("export_document_", "document"),
|
|
129
|
+
("save_document_", "document"),
|
|
130
|
+
("open_cloud_document", "document"),
|
|
131
|
+
("validate_document", "document"),
|
|
132
|
+
("cleanup_document", "document"),
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
KEYWORD_DOMAINS = {
|
|
137
|
+
"template": "template",
|
|
138
|
+
"blueprint": "template",
|
|
139
|
+
"slot": "template",
|
|
140
|
+
"document": "document",
|
|
141
|
+
"page": "page",
|
|
142
|
+
"spread": "spread",
|
|
143
|
+
"master": "master",
|
|
144
|
+
"layer": "layer",
|
|
145
|
+
"item": "object",
|
|
146
|
+
"object": "object",
|
|
147
|
+
"group": "object",
|
|
148
|
+
"label": "object",
|
|
149
|
+
"text": "text",
|
|
150
|
+
"paragraph": "style",
|
|
151
|
+
"character": "style",
|
|
152
|
+
"style": "style",
|
|
153
|
+
"graphic": "graphics",
|
|
154
|
+
"image": "graphics",
|
|
155
|
+
"export": "export",
|
|
156
|
+
"pdf": "export",
|
|
157
|
+
"idml": "export",
|
|
158
|
+
"book": "book",
|
|
159
|
+
"presentation": "presentation",
|
|
160
|
+
"session": "session",
|
|
161
|
+
"health": "server",
|
|
162
|
+
"skill": "skill",
|
|
163
|
+
"help": "utility",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def infer_domain(tool_name: str, description: str = "") -> str:
|
|
168
|
+
exact = EXACT_TOOL_DOMAINS.get(tool_name)
|
|
169
|
+
if exact:
|
|
170
|
+
return exact
|
|
171
|
+
for prefix, domain in NAME_DOMAIN_RULES:
|
|
172
|
+
if tool_name.startswith(prefix):
|
|
173
|
+
return domain
|
|
174
|
+
haystack = f"{tool_name} {description}".lower()
|
|
175
|
+
for keyword, domain in KEYWORD_DOMAINS.items():
|
|
176
|
+
if keyword in haystack:
|
|
177
|
+
return domain
|
|
178
|
+
return "utility"
|