@cteno/sdk-linux-x64 0.0.0-stage → 2.10.14

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 (116) hide show
  1. package/LICENSE.daemon +31 -0
  2. package/README.md +4 -2
  3. package/agents/flow-analyzer/AGENT.md +71 -0
  4. package/bin/cteno-host-cli +4 -0
  5. package/bin/cteno-wiki +0 -0
  6. package/bin/rg +0 -0
  7. package/bin/secret-exec +0 -0
  8. package/manifest.json +12 -0
  9. package/package.json +33 -4
  10. package/plugins/agent-secrets/.cteno-plugin/plugin.json +133 -0
  11. package/plugins/agent-secrets/bin/linux-x64/fnox +4 -0
  12. package/plugins/agent-secrets/bin/linux-x64/secret-exec +0 -0
  13. package/plugins/agent-secrets/skills/secret/SKILL.md +201 -0
  14. package/plugins/cteno.browser/.cteno-plugin/plugin.json +65 -0
  15. package/plugins/cteno.browser/skills/browser/SKILL.md +28 -0
  16. package/plugins/cteno.inbox/.cteno-plugin/plugin.json +132 -0
  17. package/plugins/cteno.inbox/bin/all/cteno-connector-credential +31 -0
  18. package/plugins/cteno.inbox/bin/all/cteno-telegram-connector +33 -0
  19. package/plugins/cteno.inbox/bin/linux-x64/cteno-inbox +0 -0
  20. package/plugins/cteno.inbox/skills/cteno-inbox/SKILL.md +374 -0
  21. package/plugins/cteno.inbox/tools/inbox_focus/TOOL.md +44 -0
  22. package/plugins/cteno.memory/.cteno-plugin/plugin.json +176 -0
  23. package/plugins/cteno.memory/agents/llm-wiki/AGENT.md +96 -0
  24. package/plugins/cteno.memory/bin/linux-x64/cteno-wiki +0 -0
  25. package/plugins/cteno.memory/skills/codebase-wiki/SKILL.md +52 -0
  26. package/plugins/cteno.memory/skills/cteno.memory.native/SKILL.md +110 -0
  27. package/plugins/cteno.memory/tools/memory/TOOL.md +492 -0
  28. package/plugins/cteno.memory/ui/index.html +822 -0
  29. package/plugins/cteno.models/.cteno-plugin/plugin.json +191 -0
  30. package/plugins/cteno.websearch/.cteno-plugin/plugin.json +45 -0
  31. package/plugins/jev/.cteno-plugin/plugin.json +309 -0
  32. package/plugins/jev/skills/jev-browser/SKILL.md +66 -0
  33. package/plugins/jev/skills/jev-browser/jev_browser.py +335 -0
  34. package/skills/agent-create/SKILL.md +86 -0
  35. package/skills/cteno-host-cli/SKILL.md +256 -0
  36. package/skills/docx/LICENSE +23 -0
  37. package/skills/docx/NOTICE.md +8 -0
  38. package/skills/docx/SKILL.md +69 -0
  39. package/skills/docx/references/cjk_typography.md +357 -0
  40. package/skills/docx/references/design_principles.md +819 -0
  41. package/skills/docx/references/revisions-and-comments.md +88 -0
  42. package/skills/docx/references/track_changes_guide.md +200 -0
  43. package/skills/docx/references/typography_guide.md +294 -0
  44. package/skills/docx/scripts/cjk_fonts.py +244 -0
  45. package/skills/docx/scripts/docx_comments.py +289 -0
  46. package/skills/docx/scripts/docx_common.py +92 -0
  47. package/skills/docx/scripts/docx_create.py +259 -0
  48. package/skills/docx/scripts/docx_edit.py +250 -0
  49. package/skills/docx/scripts/docx_read.py +149 -0
  50. package/skills/docx/scripts/docx_revisions.py +146 -0
  51. package/skills/docx/scripts/docx_template.py +70 -0
  52. package/skills/docx/scripts/docx_validate.py +156 -0
  53. package/skills/docx/scripts/render.py +250 -0
  54. package/skills/docx/tests/test_docx_skill.py +525 -0
  55. package/skills/home-devices/SKILL.md +121 -0
  56. package/skills/mcp/SKILL.md +72 -0
  57. package/skills/mcp/scripts/mcp_setup.py +439 -0
  58. package/skills/pdf/LICENSE +23 -0
  59. package/skills/pdf/NOTICE.md +8 -0
  60. package/skills/pdf/SKILL.md +63 -0
  61. package/skills/pdf/references/design-tokens.md +381 -0
  62. package/skills/pdf/references/forms.md +99 -0
  63. package/skills/pdf/references/nano-pdf-editing.md +40 -0
  64. package/skills/pdf/references/ocr-extraction.md +165 -0
  65. package/skills/pdf/scripts/_raster.py +76 -0
  66. package/skills/pdf/scripts/extract_marker.py +88 -0
  67. package/skills/pdf/scripts/extract_pymupdf.py +101 -0
  68. package/skills/pdf/scripts/pdf_create.py +191 -0
  69. package/skills/pdf/scripts/pdf_fill_form.py +97 -0
  70. package/skills/pdf/scripts/pdf_form_layout.py +168 -0
  71. package/skills/pdf/scripts/pdf_make_form.py +145 -0
  72. package/skills/pdf/scripts/pdf_merge.py +50 -0
  73. package/skills/pdf/scripts/pdf_meta.py +115 -0
  74. package/skills/pdf/scripts/pdf_page_image.py +99 -0
  75. package/skills/pdf/scripts/pdf_read.py +153 -0
  76. package/skills/pdf/scripts/pdf_secure.py +71 -0
  77. package/skills/pdf/scripts/pdf_split.py +84 -0
  78. package/skills/pdf/scripts/pdf_stamp.py +143 -0
  79. package/skills/pdf/scripts/pdf_watermark.py +51 -0
  80. package/skills/pdf/scripts/render.py +250 -0
  81. package/skills/pdf/tests/test_pdf_skill.py +415 -0
  82. package/skills/pptx/LICENSE +23 -0
  83. package/skills/pptx/NOTICE.md +8 -0
  84. package/skills/pptx/SKILL.md +62 -0
  85. package/skills/pptx/references/design-system.md +392 -0
  86. package/skills/pptx/references/pptxgenjs.md +420 -0
  87. package/skills/pptx/references/slide-types.md +413 -0
  88. package/skills/pptx/scripts/cjk_fonts.py +244 -0
  89. package/skills/pptx/scripts/pptx_create.py +240 -0
  90. package/skills/pptx/scripts/pptx_edit.py +436 -0
  91. package/skills/pptx/scripts/pptx_from_template.py +88 -0
  92. package/skills/pptx/scripts/pptx_read.py +131 -0
  93. package/skills/pptx/scripts/pptx_render.py +96 -0
  94. package/skills/pptx/scripts/render.py +250 -0
  95. package/skills/pptx/tests/test_powerpoint_skill.py +477 -0
  96. package/skills/skill-create/SKILL.md +114 -0
  97. package/skills/video-generation/SKILL.md +27 -0
  98. package/skills/xlsx/LICENSE +23 -0
  99. package/skills/xlsx/NOTICE.md +8 -0
  100. package/skills/xlsx/SKILL.md +63 -0
  101. package/skills/xlsx/references/format.md +71 -0
  102. package/skills/xlsx/references/restructuring.md +71 -0
  103. package/skills/xlsx/scripts/csv_to_xlsx.py +110 -0
  104. package/skills/xlsx/scripts/formula_check.py +428 -0
  105. package/skills/xlsx/scripts/render.py +250 -0
  106. package/skills/xlsx/scripts/xlsx_create.py +265 -0
  107. package/skills/xlsx/scripts/xlsx_edit.py +269 -0
  108. package/skills/xlsx/scripts/xlsx_read.py +166 -0
  109. package/skills/xlsx/scripts/xlsx_recalc.py +119 -0
  110. package/skills/xlsx/scripts/xlsx_restructure.py +343 -0
  111. package/skills/xlsx/scripts/xlsx_to_csv.py +75 -0
  112. package/skills/xlsx/tests/test_xlsx_skill.py +544 -0
  113. package/tools/create_goal/TOOL.md +36 -0
  114. package/tools/image_generation/TOOL.md +81 -0
  115. package/tools/skill/TOOL.md +117 -0
  116. package/tools/update_profile_notes/TOOL.md +39 -0
@@ -0,0 +1,72 @@
1
+ ---
2
+ id: mcp
3
+ name: mcp
4
+ description: "根据用户提供的 MCP 文档(README/配置片段/链接),自动解析、下载安装并注册 MCP Server"
5
+ when_to_use: "用户要求安装/接入/注册 MCP,或给出 MCP 文档、mcpServers JSON、MCP 启动命令时使用"
6
+ version: "1.0.0"
7
+ tags:
8
+ - mcp
9
+ - integration
10
+ - install
11
+ - automation
12
+ user_invocable: true
13
+ disable_model_invocation: false
14
+ argument_hint: "<mcp_doc_path_or_url_or_text> [server_name]"
15
+ metadata:
16
+ display-name: "MCP Skill"
17
+ ---
18
+
19
+ # MCP Skill
20
+
21
+ 把用户提供的 MCP 文档自动落地为可用 MCP Server:**解析文档 -> 安装依赖 -> 注册到本地 MCPRegistry -> 验证可见**。
22
+
23
+ ## 输入
24
+
25
+ - `$ARGS`: MCP 文档来源,可以是:
26
+ - 本地文件路径(README/Markdown/JSON)
27
+ - URL
28
+ - 一段文本(含 `mcpServers` JSON 或启动命令)
29
+
30
+ ## 执行规则
31
+
32
+ 1. 如果用户已经给了文档路径/URL/文本,直接执行,不要反复追问。
33
+ 2. 优先使用本 Skill 自带脚本:
34
+
35
+ ```bash
36
+ python3 "${SKILL_DIR}/scripts/mcp_setup.py" --input "$ARGS"
37
+ ```
38
+
39
+ 3. 若文档中有多个 server,优先匹配用户提到的名称;否则使用脚本自动选中的第一个。
40
+ 4. 如果安装失败:
41
+ - 先输出失败命令与 stderr 摘要
42
+ - 尝试脚本给出的回退路径(例如 npx/uvx 直启)
43
+ - 仍失败再向用户报告缺失前置依赖(Node/Python/uv 等)
44
+ 5. 注册成功后,必须给出:
45
+ - server id
46
+ - transport(stdio/streamable_http;旧 http_sse 配置仍兼容)
47
+ - protocol(legacy 或 auto_2026_07_28;仅在服务端明确支持/需要最新版无状态协议时启用后者)
48
+ - 安装执行结果
49
+ - 注册验证结果(list-mcp-servers 中可见)
50
+
51
+ ## 多 server 文档处理
52
+
53
+ 当文档里有多个 `mcpServers` 条目时,使用:
54
+
55
+ ```bash
56
+ python3 "${SKILL_DIR}/scripts/mcp_setup.py" --input "$ARGS" --server "<server_key>"
57
+ ```
58
+
59
+ ## 仅预览(不执行安装/注册)
60
+
61
+ ```bash
62
+ python3 "${SKILL_DIR}/scripts/mcp_setup.py" --input "$ARGS" --dry-run
63
+ ```
64
+
65
+ ## 结果验收标准
66
+
67
+ 完成时满足以下全部条件:
68
+
69
+ 1. 已解析出结构化 `config`(含 id/name/transport)。
70
+ 2. 已执行安装步骤,或明确说明为何可跳过安装。
71
+ 3. `add-mcp-server` RPC 返回成功(或幂等更新成功)。
72
+ 4. `list-mcp-servers` 中能看到目标 server。
@@ -0,0 +1,439 @@
1
+ #!/usr/bin/env python3
2
+ """Parse MCP docs, auto-install dependencies, and register MCP servers via local RPC."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import re
10
+ import shlex
11
+ import socket
12
+ import subprocess
13
+ import sys
14
+ import urllib.request
15
+ import uuid
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+
20
+ def sanitize_id(raw: str) -> str:
21
+ cleaned = re.sub(r"[^a-zA-Z0-9_-]+", "-", raw.strip().lower()).strip("-_")
22
+ if not cleaned:
23
+ cleaned = "mcp-server"
24
+ if cleaned[0].isdigit():
25
+ cleaned = f"mcp-{cleaned}"
26
+ return cleaned
27
+
28
+
29
+ def socket_path() -> Path:
30
+ override = os.environ.get("CTENO_RPC_SOCKET")
31
+ if override:
32
+ return Path(override)
33
+
34
+ home = Path.home()
35
+ env_tag = os.environ.get("CTENO_ENV", "").strip()
36
+ if env_tag and env_tag != "release":
37
+ return home / ".agents" / f"daemon.{env_tag}.sock"
38
+
39
+ dev_sock = home / ".agents" / "daemon.dev.sock"
40
+ if dev_sock.exists():
41
+ return dev_sock
42
+ return home / ".agents" / "daemon.sock"
43
+
44
+
45
+ def rpc_call(method: str, params: dict[str, Any]) -> Any:
46
+ sock = socket_path()
47
+ if not sock.exists():
48
+ raise RuntimeError(f"RPC socket not found: {sock}")
49
+
50
+ req_id = str(uuid.uuid4())
51
+ payload = {
52
+ "id": req_id,
53
+ "method": method,
54
+ "params": params,
55
+ }
56
+
57
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
58
+ client.settimeout(20)
59
+ client.connect(str(sock))
60
+ client.sendall((json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8"))
61
+
62
+ chunks: list[bytes] = []
63
+ while True:
64
+ b = client.recv(4096)
65
+ if not b:
66
+ break
67
+ chunks.append(b)
68
+ if b"\n" in b:
69
+ break
70
+
71
+ if not chunks:
72
+ raise RuntimeError(f"No RPC response for method {method}")
73
+
74
+ line = b"".join(chunks).split(b"\n", 1)[0].decode("utf-8", errors="replace")
75
+ response = json.loads(line)
76
+
77
+ if response.get("id") != req_id:
78
+ raise RuntimeError(f"RPC id mismatch for method {method}")
79
+ if "error" in response and response["error"]:
80
+ raise RuntimeError(f"RPC error ({method}): {response['error']}")
81
+
82
+ return response.get("result")
83
+
84
+
85
+ def strip_json_comments(text: str) -> str:
86
+ text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
87
+ text = re.sub(r"(^|\s)//.*$", "", text, flags=re.MULTILINE)
88
+ text = re.sub(r",\s*([}\]])", r"\1", text)
89
+ return text
90
+
91
+
92
+ def iter_fenced_blocks(text: str) -> list[tuple[str, str]]:
93
+ out: list[tuple[str, str]] = []
94
+ for m in re.finditer(r"```([a-zA-Z0-9_-]*)\n(.*?)```", text, flags=re.DOTALL):
95
+ lang = (m.group(1) or "").strip().lower()
96
+ body = m.group(2)
97
+ out.append((lang, body))
98
+ return out
99
+
100
+
101
+ def try_parse_json(text: str) -> Any | None:
102
+ text = text.strip()
103
+ if not text:
104
+ return None
105
+
106
+ for candidate in (text, strip_json_comments(text)):
107
+ try:
108
+ return json.loads(candidate)
109
+ except Exception:
110
+ pass
111
+
112
+ return None
113
+
114
+
115
+ def is_url(value: str) -> bool:
116
+ return value.startswith("http://") or value.startswith("https://")
117
+
118
+
119
+ def read_input_source(source: str) -> tuple[str, str]:
120
+ p = Path(source)
121
+ if p.exists() and p.is_file():
122
+ return p.read_text(encoding="utf-8", errors="replace"), "file"
123
+
124
+ if is_url(source):
125
+ with urllib.request.urlopen(source, timeout=20) as resp:
126
+ data = resp.read().decode("utf-8", errors="replace")
127
+ return data, "url"
128
+
129
+ return source, "text"
130
+
131
+
132
+ def first_non_flag(args: list[str]) -> str | None:
133
+ skip_next = False
134
+ for token in args:
135
+ if skip_next:
136
+ skip_next = False
137
+ continue
138
+ if token in {"-p", "--port", "-c", "--config", "--env-file"}:
139
+ skip_next = True
140
+ continue
141
+ if token.startswith("-"):
142
+ continue
143
+ return token
144
+ return None
145
+
146
+
147
+ def derive_install_commands(command: str, args: list[str]) -> list[list[str]]:
148
+ cmd = Path(command).name.lower()
149
+ pkg = first_non_flag(args)
150
+ if not pkg:
151
+ return []
152
+
153
+ if cmd == "npx":
154
+ return [["npm", "install", "-g", pkg]]
155
+ if cmd == "bunx":
156
+ return [["bun", "add", "-g", pkg]]
157
+ if cmd == "uvx":
158
+ return [["uv", "tool", "install", pkg]]
159
+ if cmd in {"python", "python3"} and len(args) >= 2 and args[0] == "-m":
160
+ return [["python3", "-m", "pip", "install", "-U", args[1]]]
161
+ return []
162
+
163
+
164
+ def build_config_from_entry(server_key: str, entry: dict[str, Any]) -> dict[str, Any]:
165
+ server_id = sanitize_id(server_key)
166
+ name = str(entry.get("name") or server_key)
167
+ enabled = bool(entry.get("enabled", True))
168
+ protocol = str(entry.get("protocol") or "legacy")
169
+ transport = entry.get("transport") if isinstance(entry.get("transport"), dict) else entry
170
+ transport_type = transport.get("type")
171
+
172
+ if isinstance(transport.get("url"), str) and transport["url"].strip():
173
+ if transport_type not in {None, "streamable_http", "http_sse"}:
174
+ raise RuntimeError(f"Unsupported HTTP transport type: {transport_type}")
175
+ return {
176
+ "id": server_id,
177
+ "name": name,
178
+ "enabled": enabled,
179
+ "protocol": protocol,
180
+ "transport": {
181
+ "type": "streamable_http",
182
+ "url": transport["url"].strip(),
183
+ "headers": transport.get("headers", {}) if isinstance(transport.get("headers"), dict) else {},
184
+ },
185
+ }
186
+
187
+ if isinstance(transport.get("command"), str) and transport["command"].strip():
188
+ if transport_type not in {None, "stdio"}:
189
+ raise RuntimeError(f"Unsupported stdio transport type: {transport_type}")
190
+ args = transport.get("args", []) if isinstance(transport.get("args"), list) else []
191
+ env = transport.get("env", {}) if isinstance(transport.get("env"), dict) else {}
192
+ return {
193
+ "id": server_id,
194
+ "name": name,
195
+ "enabled": enabled,
196
+ "protocol": protocol,
197
+ "transport": {
198
+ "type": "stdio",
199
+ "command": transport["command"].strip(),
200
+ "args": [str(x) for x in args],
201
+ "env": {str(k): str(v) for k, v in env.items()},
202
+ },
203
+ }
204
+
205
+ raise RuntimeError(f"Server entry '{server_key}' has neither 'command' nor 'url'")
206
+
207
+
208
+ def parse_from_json_obj(obj: Any, server_hint: str | None) -> tuple[dict[str, Any], str] | None:
209
+ if not isinstance(obj, dict):
210
+ return None
211
+
212
+ mcp_servers = obj.get("mcpServers")
213
+ if isinstance(mcp_servers, dict) and mcp_servers:
214
+ if server_hint and server_hint in mcp_servers:
215
+ key = server_hint
216
+ else:
217
+ key = next(iter(mcp_servers.keys()))
218
+
219
+ entry = mcp_servers.get(key)
220
+ if isinstance(entry, dict):
221
+ return build_config_from_entry(str(key), entry), f"json:mcpServers:{key}"
222
+
223
+ if isinstance(obj.get("command"), str) or isinstance(obj.get("url"), str):
224
+ key = server_hint or str(obj.get("name") or "mcp-server")
225
+ return build_config_from_entry(key, obj), "json:single"
226
+
227
+ transport = obj.get("transport")
228
+ if isinstance(transport, dict) and isinstance(obj.get("id"), str) and isinstance(obj.get("name"), str):
229
+ return build_config_from_entry(str(obj["id"]), obj), "json:transport"
230
+
231
+ return None
232
+
233
+
234
+ def parse_from_command_lines(text: str) -> tuple[dict[str, Any], str] | None:
235
+ candidates: list[str] = []
236
+ for lang, body in iter_fenced_blocks(text):
237
+ if lang in {"bash", "sh", "zsh", "shell", ""}:
238
+ candidates.extend(body.splitlines())
239
+
240
+ if not candidates:
241
+ candidates = text.splitlines()
242
+
243
+ preferred = []
244
+ fallback = []
245
+ for raw in candidates:
246
+ line = raw.strip()
247
+ if not line or line.startswith("#"):
248
+ continue
249
+ if re.match(r"^(npx|bunx|uvx|python3?|node)\b", line):
250
+ if "mcp" in line.lower():
251
+ preferred.append(line)
252
+ else:
253
+ fallback.append(line)
254
+
255
+ line = preferred[0] if preferred else (fallback[0] if fallback else None)
256
+ if not line:
257
+ inline = re.search(
258
+ r"(npx|bunx|uvx|python3?|node)\s+[^\n`]+",
259
+ text,
260
+ flags=re.IGNORECASE,
261
+ )
262
+ if inline:
263
+ line = inline.group(0).strip()
264
+ if not line:
265
+ return None
266
+
267
+ parts = shlex.split(line)
268
+ if not parts:
269
+ return None
270
+
271
+ command = parts[0]
272
+ args = parts[1:]
273
+ key = first_non_flag(args) or Path(command).name
274
+ key = sanitize_id(key.replace("@", "").replace("/", "-"))
275
+
276
+ config = {
277
+ "id": key,
278
+ "name": key,
279
+ "enabled": True,
280
+ "transport": {
281
+ "type": "stdio",
282
+ "command": command,
283
+ "args": args,
284
+ "env": {},
285
+ },
286
+ }
287
+ return config, f"command:{line}"
288
+
289
+
290
+ def parse_mcp_doc(text: str, server_hint: str | None) -> tuple[dict[str, Any], str]:
291
+ parsed_full = try_parse_json(text)
292
+ if parsed_full is not None:
293
+ out = parse_from_json_obj(parsed_full, server_hint)
294
+ if out:
295
+ return out
296
+
297
+ for lang, body in iter_fenced_blocks(text):
298
+ if lang in {"json", "jsonc", "javascript", "js", "ts", ""}:
299
+ obj = try_parse_json(body)
300
+ if obj is None:
301
+ continue
302
+ out = parse_from_json_obj(obj, server_hint)
303
+ if out:
304
+ return out
305
+
306
+ cmd_out = parse_from_command_lines(text)
307
+ if cmd_out:
308
+ return cmd_out
309
+
310
+ raise RuntimeError(
311
+ "Could not extract MCP config from input. Provide mcpServers JSON or a runnable command."
312
+ )
313
+
314
+
315
+ def maybe_install(config: dict[str, Any], dry_run: bool, continue_on_error: bool) -> list[dict[str, Any]]:
316
+ transport = config.get("transport", {})
317
+ if not isinstance(transport, dict):
318
+ return []
319
+ if transport.get("type") != "stdio":
320
+ return []
321
+
322
+ command = str(transport.get("command") or "")
323
+ args = transport.get("args", []) if isinstance(transport.get("args"), list) else []
324
+ install_cmds = derive_install_commands(command, [str(x) for x in args])
325
+
326
+ results: list[dict[str, Any]] = []
327
+ for cmd in install_cmds:
328
+ item: dict[str, Any] = {"command": " ".join(shlex.quote(x) for x in cmd)}
329
+ if dry_run:
330
+ item["status"] = "skipped(dry-run)"
331
+ results.append(item)
332
+ continue
333
+
334
+ proc = subprocess.run(cmd, text=True, capture_output=True)
335
+ item["status"] = "ok" if proc.returncode == 0 else "failed"
336
+ item["exit_code"] = proc.returncode
337
+ item["stdout_tail"] = "\n".join(proc.stdout.splitlines()[-10:])
338
+ item["stderr_tail"] = "\n".join(proc.stderr.splitlines()[-10:])
339
+ results.append(item)
340
+
341
+ if proc.returncode != 0 and not continue_on_error:
342
+ raise RuntimeError(
343
+ f"Install command failed: {item['command']}\n{item.get('stderr_tail', '')}".strip()
344
+ )
345
+
346
+ return results
347
+
348
+
349
+ def register_server(config: dict[str, Any], dry_run: bool) -> dict[str, Any]:
350
+ server_id = config["id"]
351
+
352
+ if dry_run:
353
+ return {
354
+ "status": "skipped(dry-run)",
355
+ "server_id": server_id,
356
+ }
357
+
358
+ listed = rpc_call("list-mcp-servers", {})
359
+ servers = listed.get("servers", []) if isinstance(listed, dict) else []
360
+ exists = any(isinstance(s, dict) and s.get("id") == server_id for s in servers)
361
+
362
+ if exists:
363
+ remove_res = rpc_call("remove-mcp-server", {"serverId": server_id})
364
+ if isinstance(remove_res, dict) and remove_res.get("success") is False:
365
+ raise RuntimeError(remove_res.get("error") or f"Failed to remove existing server '{server_id}'")
366
+
367
+ add_res = rpc_call("add-mcp-server", config)
368
+ if isinstance(add_res, dict) and add_res.get("success") is False:
369
+ raise RuntimeError(add_res.get("error") or f"Failed to add server '{server_id}'")
370
+
371
+ listed_after = rpc_call("list-mcp-servers", {})
372
+ servers_after = listed_after.get("servers", []) if isinstance(listed_after, dict) else []
373
+ visible = any(isinstance(s, dict) and s.get("id") == server_id for s in servers_after)
374
+
375
+ return {
376
+ "status": "ok" if visible else "warning:not-visible",
377
+ "server_id": server_id,
378
+ "already_existed": exists,
379
+ "add_result": add_res,
380
+ "visible_in_list": visible,
381
+ }
382
+
383
+
384
+ def main() -> int:
385
+ parser = argparse.ArgumentParser(description="Auto setup MCP server from docs.")
386
+ parser.add_argument("--input", required=True, help="Path/URL/raw text containing MCP docs")
387
+ parser.add_argument("--server", help="Server key when mcpServers has multiple entries")
388
+ parser.add_argument("--id", dest="override_id", help="Override server id")
389
+ parser.add_argument("--name", dest="override_name", help="Override server display name")
390
+ parser.add_argument("--skip-install", action="store_true", help="Skip dependency installation")
391
+ parser.add_argument("--skip-register", action="store_true", help="Skip RPC registration")
392
+ parser.add_argument("--continue-on-install-error", action="store_true", help="Continue even when install fails")
393
+ parser.add_argument("--dry-run", action="store_true", help="Parse only; do not install/register")
394
+ args = parser.parse_args()
395
+
396
+ result: dict[str, Any] = {
397
+ "ok": False,
398
+ "input": args.input,
399
+ }
400
+
401
+ try:
402
+ text, input_kind = read_input_source(args.input)
403
+ config, parsed_from = parse_mcp_doc(text, args.server)
404
+
405
+ if args.override_id:
406
+ config["id"] = sanitize_id(args.override_id)
407
+ if args.override_name:
408
+ config["name"] = args.override_name
409
+
410
+ result["input_kind"] = input_kind
411
+ result["parsed_from"] = parsed_from
412
+ result["config"] = config
413
+
414
+ if args.skip_install:
415
+ result["install"] = {"status": "skipped"}
416
+ else:
417
+ installs = maybe_install(config, dry_run=args.dry_run, continue_on_error=args.continue_on_install_error)
418
+ result["install"] = {
419
+ "status": "ok",
420
+ "steps": installs,
421
+ }
422
+
423
+ if args.skip_register:
424
+ result["register"] = {"status": "skipped"}
425
+ else:
426
+ result["register"] = register_server(config, dry_run=args.dry_run)
427
+
428
+ result["ok"] = True
429
+ print(json.dumps(result, ensure_ascii=False, indent=2))
430
+ return 0
431
+
432
+ except Exception as exc:
433
+ result["error"] = str(exc)
434
+ print(json.dumps(result, ensure_ascii=False, indent=2), file=sys.stderr)
435
+ return 1
436
+
437
+
438
+ if __name__ == "__main__":
439
+ raise SystemExit(main())
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cteno
4
+ Portions Copyright (c) 2026 Nous Research (scripts/ and tests/ adapted from the Hermes Agent productivity skills)
5
+ Portions Copyright (c) 2026 MiniMaxAI (references adapted from MiniMax-AI/skills)
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ # Sources for the office skills (docx, pptx, xlsx, pdf)
2
+
3
+ - **Scripts and tests**: adapted from Nous Research Hermes Agent `skills/productivity/{docx,powerpoint,xlsx,pdf}` (MIT), upstream commit `bf0203a` (2026-10-22).
4
+ - **Chinese typesetting, design and formatting references**: adapted from MiniMax-AI/skills `minimax-docx`, `pptx-generator`, `minimax-xlsx`, `minimax-pdf` (MIT); `xlsx/scripts/formula_check.py` from minimax-xlsx.
5
+ - **Cteno additions**: `scripts/render.py` (isolated LibreOffice profile, macOS CJK font exposure and substitution), `scripts/cjk_fonts.py` (East Asian font slots), CJK handling in `docx_create.py` / `pptx_create.py` / `pdf_create.py`, 16:9 layout stretch in `pptx_create.py`, all `SKILL.md` texts.
6
+ - Anthropic's document skills (proprietary licence) informed which pitfalls to cover; no text or code was copied from them.
7
+
8
+ Not tracked live: to update, diff the upstream folders above against these copies and re-run each skill's `tests/` plus a render check.
@@ -0,0 +1,63 @@
1
+ ---
2
+ id: pdf
3
+ name: pdf
4
+ version: "2.0.0"
5
+ description: "Create, read and manipulate PDFs: designed reports from HTML or a JSON spec (Chinese supported), text/table extraction, OCR of scans, merge/split/rotate, watermark/stamp, fillable forms (build, fill, flatten), metadata/attachments, encryption, and office-to-PDF conversion."
6
+ when_to_use: "Use whenever a .pdf is input or output: make a PDF report/resume/proposal/invoice, export Word/PPT/Excel to PDF, extract text or tables, OCR a scanned PDF, merge/split/rotate pages, add a watermark or signature image, fill or create a PDF form, encrypt/decrypt. For an editable Word deliverable use docx first, then convert."
7
+ license: MIT (see LICENSE; scripts adapted from Nous Research and MiniMax, MIT)
8
+ tags: [pdf, report, forms, ocr, merge, watermark]
9
+ ---
10
+
11
+ # PDF
12
+
13
+ ## Choose the creation route by what the PDF must look like
14
+
15
+ | Need | Route |
16
+ |---|---|
17
+ | Already have a .docx/.pptx/.xlsx | `python3 ${SKILL_DIR}/scripts/render.py FILE --pdf-only` (LibreOffice; same as the office skills' preview) |
18
+ | Designed report, resume, brochure, invoice with real layout | Write **HTML + CSS** (print styles, `@page` size and margins, page breaks) and print it with the `browser` tool / headless Chrome (`page.pdf()`), or `weasyprint` if installed. Best typography, CJK just works with system fonts. See `${SKILL_DIR}/references/design-tokens.md` for palette/type choices |
19
+ | Simple structured document (headings, paragraphs, tables, images) | JSON spec → `${SKILL_DIR}/scripts/pdf_create.py spec.json -o out.pdf` (reportlab; Chinese is detected and a CJK font is embedded automatically, lines break between characters) |
20
+ | Fillable form | `${SKILL_DIR}/scripts/pdf_form_layout.py spec.json` (lint) → `${SKILL_DIR}/scripts/pdf_make_form.py spec.json -o form.pdf` — read `${SKILL_DIR}/references/forms.md` |
21
+
22
+ ## Read and manipulate
23
+
24
+ | Task | Command |
25
+ |---|---|
26
+ | Inspect first (pages, encrypted, scanned?) | `${SKILL_DIR}/scripts/pdf_read.py f.pdf --meta` |
27
+ | Text per page / tables → JSON+CSV | `${SKILL_DIR}/scripts/pdf_read.py f.pdf --text` / `--tables --csv-dir t/` |
28
+ | Scanned pages / OCR | read `${SKILL_DIR}/references/ocr-extraction.md` (`${SKILL_DIR}/scripts/extract_pymupdf.py`, `${SKILL_DIR}/scripts/extract_marker.py`) — never report a scan as "no content" |
29
+ | Merge (with bookmarks) | `${SKILL_DIR}/scripts/pdf_merge.py a.pdf b.pdf -o m.pdf [--bookmarks]` |
30
+ | Split / extract / rotate / compress | `${SKILL_DIR}/scripts/pdf_split.py f.pdf --pages 1-3,7 -o part.pdf [--rotate 90] [--compress]` |
31
+ | Watermark with a PDF page | `${SKILL_DIR}/scripts/pdf_watermark.py f.pdf --stamp mark.pdf -o out.pdf [--under]` |
32
+ | Stamp text or image (DRAFT, signature, seal) | `${SKILL_DIR}/scripts/pdf_stamp.py f.pdf -o out.pdf --text "草稿" --x 150 --y 400 --rotation 45 --opacity 0.3` / `--image sig.png --x 400 --y 60 --width 120` |
33
+ | List / fill / flatten form fields | `${SKILL_DIR}/scripts/pdf_read.py f.pdf --fields`, `${SKILL_DIR}/scripts/pdf_fill_form.py f.pdf --fields-json v.json -o out.pdf [--flatten]` |
34
+ | Metadata, attachments | `${SKILL_DIR}/scripts/pdf_meta.py` |
35
+ | Encrypt / decrypt (AES-256) | `${SKILL_DIR}/scripts/pdf_secure.py` |
36
+ | Page images | `${SKILL_DIR}/scripts/pdf_page_image.py f.pdf --pages 1-3 --dpi 150 --out-dir imgs/` or `${SKILL_DIR}/scripts/render.py f.pdf` |
37
+ | Edit text inside an existing PDF | read `${SKILL_DIR}/references/nano-pdf-editing.md`; often better: regenerate from source |
38
+
39
+ Scripts take `--help`, print JSON, exit non-zero on failure; pages are 1-based. Always call scripts by the absolute paths shown here (not a copy found elsewhere on disk).
40
+ Dependencies: `python3 -m pip install pypdf reportlab pdfplumber pypdfium2` (OCR extras per reference file).
41
+
42
+ ## Design (documents you create)
43
+
44
+ - Choose palette and type from the content, not "safe" defaults (`${SKILL_DIR}/references/design-tokens.md`): one accent color, one heading face, one body face; A4 for China/Europe, Letter for the US; margins ≥ 20 mm.
45
+ - Chinese text: body 10.5–12 pt with 1.6–1.8 line height; headings 黑体/思源黑体, body 宋体/思源宋体 or 苹方/雅黑 for screen reading; full-width punctuation.
46
+ - Cover pages and TOCs only for long documents (> 6 pages). Tables: header row shaded, numbers right-aligned, repeat header on page breaks.
47
+ - Page numbers on every page except the cover; "第 X 页 / 共 Y 页" for Chinese.
48
+
49
+ ## Pitfalls
50
+
51
+ - **Fonts**: reportlab's built-in Helvetica has no Chinese glyphs — text becomes black boxes. `pdf_create.py` handles this; in your own reportlab code register a TrueType CJK font (`TTFont`) and set `wordWrap="CJK"`. Hiragino/PingFang on macOS are CFF and can't be embedded by reportlab — use STHeiti/Songti, Windows 微软雅黑/宋体, or Noto Sans/Serif CJK.
52
+ - **Scanned PDFs** have no text layer: empty `--text` plus page images means OCR, not "empty document".
53
+ - **Encrypted inputs** must be decrypted first (`pdf_secure.py --decrypt`).
54
+ - **Owner-password permissions** (no-print/no-copy) are advisory; only a user password protects content. Don't present them as security.
55
+ - **Form values in CJK** are stored correctly but a field's default font may not show them; verify with `--fields` and flatten with an embedded CJK font if appearance matters.
56
+ - **Table extraction is heuristic** — borderless or merged-cell tables need checking against the page image.
57
+ - **PDF/A** is not produced by these libraries; use Ghostscript + veraPDF if archival conformance is required.
58
+
59
+ ## Verify before delivery (required)
60
+
61
+ 1. `python3 ${SKILL_DIR}/scripts/pdf_read.py out.pdf --meta` — page count, sizes, encryption as intended.
62
+ 2. `python3 ${SKILL_DIR}/scripts/render.py out.pdf` and **open every page image** (or the pages you changed): black boxes or missing CJK glyphs, text past margins, tables split badly, blank trailing page, wrong page numbers, watermark covering content.
63
+ 3. After extraction/OCR: spot-check a known string or table cell against the page image. After form fill: re-read `--fields` and compare values exactly.