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.
Files changed (72) hide show
  1. cli_anything/indesign/README.md +32 -0
  2. cli_anything/indesign/__init__.py +1 -0
  3. cli_anything/indesign/__main__.py +5 -0
  4. cli_anything/indesign/core/artifacts.py +57 -0
  5. cli_anything/indesign/core/catalog.py +405 -0
  6. cli_anything/indesign/core/domains.py +178 -0
  7. cli_anything/indesign/core/envelope.py +65 -0
  8. cli_anything/indesign/core/errors.py +30 -0
  9. cli_anything/indesign/core/health.py +46 -0
  10. cli_anything/indesign/core/hidden_backend.py +116 -0
  11. cli_anything/indesign/core/hidden_handler_schemas.py +223 -0
  12. cli_anything/indesign/core/mcp_backend.py +152 -0
  13. cli_anything/indesign/core/node_setup.py +35 -0
  14. cli_anything/indesign/core/paths.py +41 -0
  15. cli_anything/indesign/core/plugins/__init__.py +2 -0
  16. cli_anything/indesign/core/plugins/backend.py +90 -0
  17. cli_anything/indesign/core/plugins/discovery.py +69 -0
  18. cli_anything/indesign/core/plugins/host_actions.py +76 -0
  19. cli_anything/indesign/core/plugins/install.py +38 -0
  20. cli_anything/indesign/core/plugins/manifest.py +279 -0
  21. cli_anything/indesign/core/plugins/validate.py +181 -0
  22. cli_anything/indesign/core/router.py +217 -0
  23. cli_anything/indesign/core/runtime.py +59 -0
  24. cli_anything/indesign/core/scripts.py +44 -0
  25. cli_anything/indesign/core/session.py +68 -0
  26. cli_anything/indesign/indesign_cli.py +320 -0
  27. cli_anything/indesign/node/hidden_handler_bridge.mjs +111 -0
  28. cli_anything/indesign/server/package-lock.json +168 -0
  29. cli_anything/indesign/server/package.json +45 -0
  30. cli_anything/indesign/server/src/advanced/index.js +76 -0
  31. cli_anything/indesign/server/src/core/InDesignMCPServer.js +273 -0
  32. cli_anything/indesign/server/src/core/scriptExecutor.js +271 -0
  33. cli_anything/indesign/server/src/core/sessionManager.js +545 -0
  34. cli_anything/indesign/server/src/handlers/advancedTemplateHandlers.js +1072 -0
  35. cli_anything/indesign/server/src/handlers/bookHandlers.js +490 -0
  36. cli_anything/indesign/server/src/handlers/documentHandlers.js +1472 -0
  37. cli_anything/indesign/server/src/handlers/exportHandlers.js +208 -0
  38. cli_anything/indesign/server/src/handlers/graphicsHandlers.js +605 -0
  39. cli_anything/indesign/server/src/handlers/groupHandlers.js +358 -0
  40. cli_anything/indesign/server/src/handlers/helpHandlers.js +347 -0
  41. cli_anything/indesign/server/src/handlers/index.js +77 -0
  42. cli_anything/indesign/server/src/handlers/layerHandlers.js +75 -0
  43. cli_anything/indesign/server/src/handlers/masterSpreadHandlers.js +451 -0
  44. cli_anything/indesign/server/src/handlers/pageHandlers.js +698 -0
  45. cli_anything/indesign/server/src/handlers/pageItemHandlers.js +704 -0
  46. cli_anything/indesign/server/src/handlers/presentationHandlers.js +220 -0
  47. cli_anything/indesign/server/src/handlers/spreadHandlers.js +348 -0
  48. cli_anything/indesign/server/src/handlers/styleHandlers.js +458 -0
  49. cli_anything/indesign/server/src/handlers/textHandlers.js +431 -0
  50. cli_anything/indesign/server/src/handlers/utilityHandlers.js +83 -0
  51. cli_anything/indesign/server/src/index.js +17 -0
  52. cli_anything/indesign/server/src/types/index.js +106 -0
  53. cli_anything/indesign/server/src/types/toolDefinitionsAdvancedTemplates.js +144 -0
  54. cli_anything/indesign/server/src/types/toolDefinitionsBook.js +224 -0
  55. cli_anything/indesign/server/src/types/toolDefinitionsContent.js +353 -0
  56. cli_anything/indesign/server/src/types/toolDefinitionsDocument.js +409 -0
  57. cli_anything/indesign/server/src/types/toolDefinitionsExport.js +65 -0
  58. cli_anything/indesign/server/src/types/toolDefinitionsLayer.js +40 -0
  59. cli_anything/indesign/server/src/types/toolDefinitionsMasterSpread.js +160 -0
  60. cli_anything/indesign/server/src/types/toolDefinitionsPage.js +271 -0
  61. cli_anything/indesign/server/src/types/toolDefinitionsPageItemGroup.js +437 -0
  62. cli_anything/indesign/server/src/types/toolDefinitionsPresentation.js +83 -0
  63. cli_anything/indesign/server/src/types/toolDefinitionsSpread.js +158 -0
  64. cli_anything/indesign/server/src/types/toolDefinitionsUtility.js +40 -0
  65. cli_anything/indesign/server/src/utils/stringUtils.js +107 -0
  66. cli_anything/indesign/skills/SKILL.md +198 -0
  67. indesign_cli-0.2.0.dist-info/METADATA +267 -0
  68. indesign_cli-0.2.0.dist-info/RECORD +72 -0
  69. indesign_cli-0.2.0.dist-info/WHEEL +5 -0
  70. indesign_cli-0.2.0.dist-info/entry_points.txt +3 -0
  71. indesign_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
  72. 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,5 @@
1
+ from .indesign_cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -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"