loongclaw-devkit 0.1.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.
@@ -0,0 +1,5 @@
1
+ """LoongClaw DevKit — MCP 开发者工具包"""
2
+
3
+ from loongclaw_devkit.server import main
4
+
5
+ __all__ = ["main"]
@@ -0,0 +1,5 @@
1
+ """python -m loongclaw_devkit 入口"""
2
+
3
+ from loongclaw_devkit.server import main
4
+
5
+ main()
@@ -0,0 +1,660 @@
1
+ #!/usr/bin/env python3
2
+ """LoongClaw MCP 一键发布工具
3
+
4
+ 交互模式(人类用):
5
+ python publish.py
6
+
7
+ 自动模式(AI 用):
8
+ python publish.py --auto --id my-mcp --version 1.0.0 --access public --json-output
9
+
10
+ 首次运行后配置保存到 .publish.json,后续 --auto 自动读取。
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import os
18
+ import re
19
+ import shutil
20
+ import subprocess
21
+ import sys
22
+ import zipfile
23
+ from pathlib import Path
24
+
25
+ # ── 常量 ──────────────────────────────────────────────────
26
+ CLOUD_API_BASE = "https://yun.loongclaw.com"
27
+ UPLOAD_ENDPOINT = "/v1/store/upload"
28
+ ENTRYPOINT = "server.py" # 入口文件,始终明文
29
+ CONFIG_FILE = ".publish.json"
30
+ IGNORE_PATTERNS = {
31
+ "__pycache__", ".pytest_cache", ".ruff_cache", ".git", ".venv",
32
+ "logs", "cache", ".coverage", "*.pyc", "*.pyo", ".publish.json",
33
+ "publish.py", "setup.py", "build", "dist", "*.egg-info",
34
+ }
35
+
36
+
37
+ # ── 工具函数 ──────────────────────────────────────────────
38
+
39
+ def should_skip(name: str) -> bool:
40
+ if name in IGNORE_PATTERNS:
41
+ return True
42
+ return any(
43
+ name.endswith(p[1:]) for p in IGNORE_PATTERNS if p.startswith("*")
44
+ )
45
+
46
+
47
+ def print_step(n: int, total: int, msg: str) -> None:
48
+ print(f"\n[{n}/{total}] {msg}")
49
+
50
+
51
+ def detect_project_info(project_dir: Path) -> dict:
52
+ """从 server.py 的 FastMCP 实例中提取插件信息。"""
53
+ entry = project_dir / ENTRYPOINT
54
+ if not entry.exists():
55
+ return {}
56
+
57
+ text = entry.read_text(encoding="utf-8", errors="replace")
58
+ info: dict = {}
59
+
60
+ # FastMCP("name", ...) 或 FastMCP(name="...")
61
+ m = re.search(r'FastMCP\(\s*["\']([^"\']+)["\']', text)
62
+ if m:
63
+ info["id"] = m.group(1)
64
+
65
+ # instructions=("...") 或 description="..."
66
+ m = re.search(r'(?:instructions|description)\s*=\s*["\'\(]+(.*?)[\)"\']', text, re.DOTALL)
67
+ if m:
68
+ info["description"] = " ".join(m.group(1).split())[:200]
69
+
70
+ return info
71
+
72
+
73
+ def find_py_files(project_dir: Path) -> list[Path]:
74
+ """递归查找所有 .py 文件,排除忽略项。"""
75
+ results = []
76
+ for p in sorted(project_dir.rglob("*.py")):
77
+ if any(should_skip(part) for part in p.relative_to(project_dir).parts):
78
+ continue
79
+ results.append(p)
80
+ return results
81
+
82
+
83
+ def validate_project(project_dir: Path) -> list[str]:
84
+ """校验项目结构,返回错误列表。"""
85
+ errors = []
86
+ entry = project_dir / ENTRYPOINT
87
+ if not entry.exists():
88
+ errors.append(f"缺少入口文件 {ENTRYPOINT}")
89
+ else:
90
+ text = entry.read_text(encoding="utf-8", errors="replace")
91
+ if "FastMCP" not in text:
92
+ errors.append(f"{ENTRYPOINT} 中未找到 FastMCP 实例")
93
+ if "mcp.run(" not in text and ".run(" not in text:
94
+ errors.append(f"{ENTRYPOINT} 中未找到 mcp.run() 调用")
95
+
96
+ req = project_dir / "requirements.txt"
97
+ if not req.exists():
98
+ errors.append("缺少 requirements.txt")
99
+
100
+ return errors
101
+
102
+
103
+ # ── 加密(Cython 编译)──────────────────────────────────
104
+
105
+ def encrypt_files(
106
+ project_dir: Path,
107
+ py_files: list[Path],
108
+ skip_encrypt: list[str],
109
+ staging_dir: Path,
110
+ ) -> tuple[list[str], list[str]]:
111
+ """编译 .py → .so/.pyd,返回 (加密文件列表, 明文文件列表)。"""
112
+ always_plain = {ENTRYPOINT, "publish.py", "setup.py", "__init__.py"}
113
+ skip_set = set(skip_encrypt) | always_plain
114
+
115
+ to_encrypt = []
116
+ plain_files = []
117
+
118
+ for f in py_files:
119
+ rel = str(f.relative_to(project_dir))
120
+ name = f.name
121
+ if name in skip_set or rel in skip_set:
122
+ plain_files.append(rel)
123
+ else:
124
+ to_encrypt.append(f)
125
+
126
+ if not to_encrypt:
127
+ print(" ℹ 无需加密的文件")
128
+ return [], [str(f.relative_to(project_dir)) for f in py_files]
129
+
130
+ # 检查 Cython 是否安装
131
+ try:
132
+ import Cython # noqa: F401
133
+ except ImportError:
134
+ print(" ⚠ Cython 未安装,正在安装...")
135
+ subprocess.check_call(
136
+ [sys.executable, "-m", "pip", "install", "cython", "-q"],
137
+ )
138
+
139
+ encrypted_names = []
140
+ for f in to_encrypt:
141
+ rel = f.relative_to(project_dir)
142
+ module_name = f.stem
143
+ work_dir = staging_dir / rel.parent
144
+ work_dir.mkdir(parents=True, exist_ok=True)
145
+
146
+ # 复制为 .pyx
147
+ pyx_path = work_dir / f"{module_name}.pyx"
148
+ shutil.copy2(f, pyx_path)
149
+
150
+ # 编译
151
+ print(f" 🔒 编译 {rel}...")
152
+ setup_code = (
153
+ "from setuptools import setup\n"
154
+ "from Cython.Build import cythonize\n"
155
+ f"setup(ext_modules=cythonize('{module_name}.pyx', "
156
+ "compiler_directives={'language_level': '3'}))\n"
157
+ )
158
+ setup_file = work_dir / "_setup_tmp.py"
159
+ setup_file.write_text(setup_code)
160
+
161
+ result = subprocess.run(
162
+ [sys.executable, str(setup_file), "build_ext", "--inplace"],
163
+ cwd=str(work_dir),
164
+ capture_output=True,
165
+ text=True,
166
+ )
167
+ if result.returncode != 0:
168
+ print(f" ❌ 编译失败: {rel}")
169
+ print(f" {result.stderr[:500]}")
170
+ # 回退:保留明文
171
+ plain_files.append(str(rel))
172
+ continue
173
+
174
+ # 收集编译产物
175
+ so_files = list(work_dir.glob(f"{module_name}.cpython-*.so")) + \
176
+ list(work_dir.glob(f"{module_name}.cp*-*.pyd"))
177
+ if so_files:
178
+ encrypted_names.append(str(rel))
179
+ else:
180
+ print(f" ⚠ 未找到编译产物: {rel},保留明文")
181
+ plain_files.append(str(rel))
182
+
183
+ # 清理中间文件
184
+ for tmp in [pyx_path, setup_file]:
185
+ tmp.unlink(missing_ok=True)
186
+ for c_file in work_dir.glob(f"{module_name}.c"):
187
+ c_file.unlink(missing_ok=True)
188
+ for build_dir in work_dir.glob("build"):
189
+ shutil.rmtree(build_dir, ignore_errors=True)
190
+
191
+ return encrypted_names, plain_files
192
+
193
+
194
+ # ── 打包 ─────────────────────────────────────────────────
195
+
196
+ def package_project(
197
+ project_dir: Path,
198
+ staging_dir: Path,
199
+ plain_files: list[str],
200
+ ) -> Path:
201
+ """收集明文文件 + 编译产物 + vendor wheels → project.zip"""
202
+ # 复制明文文件
203
+ for rel in plain_files:
204
+ src = project_dir / rel
205
+ dst = staging_dir / rel
206
+ dst.parent.mkdir(parents=True, exist_ok=True)
207
+ if src.exists():
208
+ shutil.copy2(src, dst)
209
+
210
+ # 复制非 .py 文件(配置、模板等)
211
+ for f in sorted(project_dir.rglob("*")):
212
+ if f.is_dir():
213
+ continue
214
+ rel = f.relative_to(project_dir)
215
+ if any(should_skip(part) for part in rel.parts):
216
+ continue
217
+ if f.suffix == ".py":
218
+ continue # .py 已处理
219
+ dst = staging_dir / rel
220
+ if not dst.exists():
221
+ dst.parent.mkdir(parents=True, exist_ok=True)
222
+ shutil.copy2(f, dst)
223
+
224
+ # requirements.txt → vendor wheels
225
+ req = staging_dir / "requirements.txt"
226
+ if req.exists():
227
+ vendor_dir = staging_dir / "_vendor"
228
+ vendor_dir.mkdir(exist_ok=True)
229
+ print(" 📦 下载离线 wheel 包...")
230
+ result = subprocess.run(
231
+ [
232
+ sys.executable, "-m", "pip", "download",
233
+ "-r", str(req), "-d", str(vendor_dir),
234
+ "--only-binary=:all:",
235
+ ],
236
+ capture_output=True, text=True,
237
+ )
238
+ if result.returncode != 0:
239
+ # 尝试清华镜像
240
+ subprocess.run(
241
+ [
242
+ sys.executable, "-m", "pip", "download",
243
+ "-r", str(req), "-d", str(vendor_dir),
244
+ "--only-binary=:all:",
245
+ "-i", "https://pypi.tuna.tsinghua.edu.cn/simple",
246
+ "--trusted-host", "pypi.tuna.tsinghua.edu.cn",
247
+ ],
248
+ capture_output=True, text=True,
249
+ )
250
+
251
+ # 打 zip
252
+ zip_path = project_dir / "project.zip"
253
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
254
+ for root, dirs, files in os.walk(staging_dir):
255
+ dirs[:] = [d for d in dirs if not should_skip(d)]
256
+ for fname in sorted(files):
257
+ if should_skip(fname):
258
+ continue
259
+ full = Path(root) / fname
260
+ arcname = full.relative_to(staging_dir)
261
+ zf.write(full, arcname)
262
+
263
+ return zip_path
264
+
265
+
266
+ # ── Manifest 生成 ────────────────────────────────────────
267
+
268
+ def generate_manifest(
269
+ project_dir: Path,
270
+ staging_dir: Path,
271
+ config: dict,
272
+ ) -> dict:
273
+ """生成 manifest.json。"""
274
+ # 收集 staging 中的所有文件
275
+ all_files = []
276
+ for f in sorted(staging_dir.rglob("*")):
277
+ if f.is_dir():
278
+ continue
279
+ rel = str(f.relative_to(staging_dir))
280
+ if not should_skip(f.name):
281
+ all_files.append(rel)
282
+
283
+ # 检测 configFields
284
+ config_fields = []
285
+ entry = project_dir / ENTRYPOINT
286
+ if entry.exists():
287
+ text = entry.read_text(encoding="utf-8", errors="replace")
288
+ for m in re.finditer(
289
+ r'os\.environ\.get\(["\'](\w+)["\'](?:,\s*["\']([^"\']*)["\'])?\)',
290
+ text,
291
+ ):
292
+ key = m.group(1)
293
+ default = m.group(2) or ""
294
+ if key not in ("PATH", "HOME", "USER", "LANG"):
295
+ config_fields.append({
296
+ "key": key,
297
+ "label": key.replace("_", " ").title(),
298
+ "default": default,
299
+ "required": default == "",
300
+ })
301
+
302
+ manifest = {
303
+ "id": config["id"],
304
+ "name": config.get("name", config["id"]),
305
+ "description": config.get("description", ""),
306
+ "version": config["version"],
307
+ "author": config.get("author", ""),
308
+ "icon": config.get("icon", "🔧"),
309
+ "runtime": "python",
310
+ "entrypoint": ENTRYPOINT,
311
+ "files": [],
312
+ "env": {},
313
+ "configFields": config_fields,
314
+ "sourceArchive": "project.zip",
315
+ }
316
+
317
+ # 写到 staging
318
+ manifest_path = staging_dir / "manifest.json"
319
+ manifest_path.write_text(
320
+ json.dumps(manifest, ensure_ascii=False, indent=2),
321
+ encoding="utf-8",
322
+ )
323
+
324
+ return manifest
325
+
326
+
327
+ # ── 上传 ──────────────────────────────────────────────────
328
+
329
+ def upload_to_cloud(
330
+ project_dir: Path,
331
+ config: dict,
332
+ manifest: dict,
333
+ ) -> dict:
334
+ """上传到 LoongClaw Cloud API。"""
335
+ import urllib.request
336
+ import urllib.error
337
+
338
+ token = config.get("token", "")
339
+ if not token:
340
+ token = os.environ.get("LOONGCLAW_TOKEN", "")
341
+ if not token:
342
+ return {"status": "error", "step": "upload",
343
+ "error": "缺少认证 token。设置环境变量 LOONGCLAW_TOKEN 或在 .publish.json 中配置 token",
344
+ "fix": "运行 python publish.py --reconfigure 设置 token"}
345
+
346
+ # 构造上传请求
347
+ zip_path = project_dir / "project.zip"
348
+ if not zip_path.exists():
349
+ return {"status": "error", "step": "upload",
350
+ "error": "project.zip 不存在",
351
+ "fix": "检查打包步骤是否成功"}
352
+
353
+ manifest_path = project_dir / "_staging" / "manifest.json"
354
+ if not manifest_path.exists():
355
+ manifest_path = project_dir / "manifest.json"
356
+
357
+ # multipart/form-data 上传
358
+ boundary = "----LoongClawUpload"
359
+ body = b""
360
+
361
+ # manifest.json 部分
362
+ body += f"--{boundary}\r\n".encode()
363
+ body += b'Content-Disposition: form-data; name="manifest"; filename="manifest.json"\r\n'
364
+ body += b"Content-Type: application/json\r\n\r\n"
365
+ body += json.dumps(manifest, ensure_ascii=False).encode("utf-8")
366
+ body += b"\r\n"
367
+
368
+ # project.zip 部分
369
+ body += f"--{boundary}\r\n".encode()
370
+ body += b'Content-Disposition: form-data; name="archive"; filename="project.zip"\r\n'
371
+ body += b"Content-Type: application/zip\r\n\r\n"
372
+ body += zip_path.read_bytes()
373
+ body += b"\r\n"
374
+
375
+ # accessType 部分
376
+ body += f"--{boundary}\r\n".encode()
377
+ body += b'Content-Disposition: form-data; name="accessType"\r\n\r\n'
378
+ body += config.get("access", "public").encode()
379
+ body += b"\r\n"
380
+
381
+ body += f"--{boundary}--\r\n".encode()
382
+
383
+ url = f"{CLOUD_API_BASE}{UPLOAD_ENDPOINT}"
384
+ req = urllib.request.Request(
385
+ url,
386
+ data=body,
387
+ headers={
388
+ "Authorization": f"Bearer {token}",
389
+ "Content-Type": f"multipart/form-data; boundary={boundary}",
390
+ },
391
+ method="POST",
392
+ )
393
+
394
+ try:
395
+ with urllib.request.urlopen(req) as resp:
396
+ result = json.loads(resp.read().decode("utf-8"))
397
+ return {"status": "success", **result}
398
+ except urllib.error.HTTPError as e:
399
+ err_body = e.read().decode("utf-8", errors="replace")
400
+ return {"status": "error", "step": "upload",
401
+ "error": f"HTTP {e.code}: {err_body[:300]}",
402
+ "fix": "检查 token 是否有效,或联系管理员"}
403
+ except urllib.error.URLError as e:
404
+ return {"status": "error", "step": "upload",
405
+ "error": f"网络错误: {e.reason}",
406
+ "fix": "检查网络连接和 CLOUD_API_BASE 地址"}
407
+
408
+
409
+ # ── 配置管理 ─────────────────────────────────────────────
410
+
411
+ def load_config(project_dir: Path) -> dict | None:
412
+ cfg_path = project_dir / CONFIG_FILE
413
+ if cfg_path.exists():
414
+ try:
415
+ return json.loads(cfg_path.read_text(encoding="utf-8"))
416
+ except (json.JSONDecodeError, OSError):
417
+ pass
418
+ return None
419
+
420
+
421
+ def save_config(project_dir: Path, config: dict) -> None:
422
+ cfg_path = project_dir / CONFIG_FILE
423
+ # 不保存 token 到配置文件(安全)
424
+ safe = {k: v for k, v in config.items() if k != "token"}
425
+ cfg_path.write_text(
426
+ json.dumps(safe, ensure_ascii=False, indent=2),
427
+ encoding="utf-8",
428
+ )
429
+
430
+
431
+ # ── 交互式配置 ───────────────────────────────────────────
432
+
433
+ def interactive_config(project_dir: Path, detected: dict) -> dict:
434
+ """交互式引导配置。"""
435
+ print("\n═══ LoongClaw MCP 发布工具 ═══\n")
436
+
437
+ config: dict = {}
438
+
439
+ # ID
440
+ default_id = detected.get("id", project_dir.name)
441
+ val = input(f"▶ 插件 ID [{default_id}]: ").strip()
442
+ config["id"] = val if val else default_id
443
+
444
+ # 名称
445
+ default_name = config["id"]
446
+ val = input(f"▶ 插件名称 [{default_name}]: ").strip()
447
+ config["name"] = val if val else default_name
448
+
449
+ # 描述
450
+ default_desc = detected.get("description", "")
451
+ if default_desc:
452
+ print(f" (自动检测到描述:{default_desc[:60]}...)")
453
+ val = input(f"▶ 插件描述 [{default_desc[:60] or '无'}]: ").strip()
454
+ config["description"] = val if val else default_desc
455
+
456
+ # 版本
457
+ val = input("▶ 版本号 [1.0.0]: ").strip()
458
+ config["version"] = val if val else "1.0.0"
459
+
460
+ # 作者
461
+ val = input("▶ 作者名: ").strip()
462
+ config["author"] = val
463
+
464
+ # 图标
465
+ val = input("▶ 图标 emoji [🔧]: ").strip()
466
+ config["icon"] = val if val else "🔧"
467
+
468
+ # 不加密文件
469
+ py_files = find_py_files(project_dir)
470
+ encryptable = [
471
+ str(f.relative_to(project_dir)) for f in py_files
472
+ if f.name not in {ENTRYPOINT, "publish.py", "setup.py", "__init__.py"}
473
+ ]
474
+ if encryptable:
475
+ print(f"\n▶ 以下 {len(encryptable)} 个 .py 文件将被加密编译:")
476
+ for name in encryptable:
477
+ print(f" {name}")
478
+ val = input(" 不需要加密的文件(逗号分隔,留空=全加密): ").strip()
479
+ config["skipEncrypt"] = [s.strip() for s in val.split(",") if s.strip()] if val else []
480
+ else:
481
+ config["skipEncrypt"] = []
482
+
483
+ # 访问类型
484
+ print("\n▶ 访问类型:")
485
+ print(" [1] 公开(所有用户免费使用)")
486
+ print(" [2] 付费(需联系商务开通)")
487
+ val = input(" 选择 [1]: ").strip()
488
+ config["access"] = "private" if val == "2" else "public"
489
+
490
+ # 上传
491
+ val = input("\n▶ 自动上传到 LoongClaw 服务器?[Y/n]: ").strip().lower()
492
+ config["upload"] = val != "n"
493
+
494
+ if config["upload"]:
495
+ val = input("▶ LoongClaw API Token(环境变量 LOONGCLAW_TOKEN 也可): ").strip()
496
+ if val:
497
+ config["token"] = val
498
+
499
+ return config
500
+
501
+
502
+ # ── 主流程 ────────────────────────────────────────────────
503
+
504
+ def run(config: dict, project_dir: Path, json_output: bool = False) -> dict:
505
+ """执行完整发布流程,返回结果字典。"""
506
+ total_steps = 6 if config.get("upload") else 5
507
+ result: dict = {"status": "success", "steps": {}}
508
+
509
+ # Step 1: 校验
510
+ print_step(1, total_steps, "校验项目结构...")
511
+ errors = validate_project(project_dir)
512
+ if errors:
513
+ result = {
514
+ "status": "error", "step": "validate",
515
+ "errors": errors,
516
+ "fix": "; ".join(errors),
517
+ }
518
+ if json_output:
519
+ print(json.dumps(result, ensure_ascii=False))
520
+ else:
521
+ for e in errors:
522
+ print(f" ❌ {e}")
523
+ return result
524
+ print(" ✅ 项目结构正确")
525
+ result["steps"]["validate"] = "ok"
526
+
527
+ # Step 2: 准备 staging
528
+ staging_dir = project_dir / "_staging"
529
+ if staging_dir.exists():
530
+ shutil.rmtree(staging_dir)
531
+ staging_dir.mkdir()
532
+
533
+ # Step 3: 加密编译
534
+ print_step(2, total_steps, "Cython 加密编译...")
535
+ py_files = find_py_files(project_dir)
536
+ encrypted, plain = encrypt_files(
537
+ project_dir, py_files,
538
+ config.get("skipEncrypt", []),
539
+ staging_dir,
540
+ )
541
+ print(f" ✅ 加密 {len(encrypted)} 个,明文 {len(plain)} 个")
542
+ result["steps"]["encrypt"] = {"encrypted": len(encrypted), "plain": len(plain)}
543
+
544
+ # Step 4: 打包
545
+ print_step(3, total_steps, "打包 project.zip...")
546
+ zip_path = package_project(project_dir, staging_dir, plain)
547
+ size_mb = zip_path.stat().st_size / 1024 / 1024
548
+ print(f" ✅ {zip_path.name} ({size_mb:.1f} MB)")
549
+ result["steps"]["package"] = {"size_mb": round(size_mb, 1)}
550
+
551
+ # Step 5: 生成 manifest
552
+ print_step(4, total_steps, "生成 manifest.json...")
553
+ manifest = generate_manifest(project_dir, staging_dir, config)
554
+ print(f" ✅ {manifest['id']} v{manifest['version']}")
555
+ result["steps"]["manifest"] = manifest
556
+
557
+ # Step 6: 上传(可选)
558
+ if config.get("upload"):
559
+ print_step(5, total_steps, "上传到 LoongClaw 服务器...")
560
+ upload_result = upload_to_cloud(project_dir, config, manifest)
561
+ if upload_result["status"] == "error":
562
+ result["status"] = "error"
563
+ result["step"] = "upload"
564
+ result["error"] = upload_result["error"]
565
+ result["fix"] = upload_result.get("fix", "")
566
+ if json_output:
567
+ print(json.dumps(result, ensure_ascii=False))
568
+ else:
569
+ print(f" ❌ {upload_result['error']}")
570
+ # 清理 staging 但保留 zip
571
+ shutil.rmtree(staging_dir, ignore_errors=True)
572
+ return result
573
+ print(" ✅ 上传成功")
574
+ result["steps"]["upload"] = "ok"
575
+
576
+ # 保存配置
577
+ save_config(project_dir, config)
578
+
579
+ # 清理
580
+ shutil.rmtree(staging_dir, ignore_errors=True)
581
+
582
+ result["version"] = config["version"]
583
+ result["files_encrypted"] = len(encrypted)
584
+ result["uploaded"] = config.get("upload", False)
585
+
586
+ if json_output:
587
+ print(json.dumps(result, ensure_ascii=False))
588
+ else:
589
+ print(f"\n{'═' * 50}")
590
+ print(f" 发布完成! {config['id']} v{config['version']}")
591
+ if config.get("access") == "private":
592
+ print(" 📌 访问类型: 付费(需商务开通)")
593
+ else:
594
+ print(" 📌 访问类型: 公开")
595
+ print(f"{'═' * 50}")
596
+
597
+ return result
598
+
599
+
600
+ def main() -> None:
601
+ parser = argparse.ArgumentParser(description="LoongClaw MCP 一键发布工具")
602
+ parser.add_argument("--auto", action="store_true",
603
+ help="自动模式,使用 .publish.json 或命令行参数")
604
+ parser.add_argument("--reconfigure", action="store_true",
605
+ help="重新配置(忽略 .publish.json)")
606
+ parser.add_argument("--json-output", action="store_true",
607
+ help="JSON 格式输出(适合 AI 解析)")
608
+ parser.add_argument("--id", type=str, help="插件 ID")
609
+ parser.add_argument("--name", type=str, help="插件名称")
610
+ parser.add_argument("--version", type=str, help="版本号")
611
+ parser.add_argument("--access", choices=["public", "private"], help="访问类型")
612
+ parser.add_argument("--skip-encrypt", type=str, default="",
613
+ help="不加密的文件(逗号分隔)")
614
+ parser.add_argument("--upload", action="store_true", help="上传到服务器")
615
+ parser.add_argument("--no-upload", action="store_true", help="不上传")
616
+ parser.add_argument("--token", type=str, help="LoongClaw API Token")
617
+ parser.add_argument("--dir", type=str, default=".",
618
+ help="项目目录(默认当前目录)")
619
+ args = parser.parse_args()
620
+
621
+ project_dir = Path(args.dir).resolve()
622
+ json_output = args.json_output
623
+
624
+ if args.auto:
625
+ # 自动模式:优先命令行参数 > .publish.json > 自动检测
626
+ saved = load_config(project_dir) or {}
627
+ detected = detect_project_info(project_dir)
628
+
629
+ config = {
630
+ "id": args.id or saved.get("id") or detected.get("id") or project_dir.name,
631
+ "name": args.name or saved.get("name") or saved.get("id", project_dir.name),
632
+ "version": args.version or saved.get("version", "1.0.0"),
633
+ "description": saved.get("description") or detected.get("description", ""),
634
+ "author": saved.get("author", ""),
635
+ "icon": saved.get("icon", "🔧"),
636
+ "access": args.access or saved.get("access", "public"),
637
+ "skipEncrypt": (
638
+ [s.strip() for s in args.skip_encrypt.split(",") if s.strip()]
639
+ if args.skip_encrypt
640
+ else saved.get("skipEncrypt", [])
641
+ ),
642
+ "upload": args.upload or (not args.no_upload and saved.get("upload", False)),
643
+ "token": args.token or os.environ.get("LOONGCLAW_TOKEN", ""),
644
+ }
645
+ elif args.reconfigure or not load_config(project_dir):
646
+ # 交互模式
647
+ detected = detect_project_info(project_dir)
648
+ config = interactive_config(project_dir, detected)
649
+ else:
650
+ # 有配置文件 → 直接用
651
+ config = load_config(project_dir) or {}
652
+ if not config.get("id"):
653
+ detected = detect_project_info(project_dir)
654
+ config = interactive_config(project_dir, detected)
655
+
656
+ run(config, project_dir, json_output)
657
+
658
+
659
+ if __name__ == "__main__":
660
+ main()
@@ -0,0 +1,288 @@
1
+ """LoongClaw DevKit MCP Server
2
+
3
+ 提供 3 个工具,让 AI 助手直接完成 MCP 插件的创建、发布、状态查看。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ from mcp.server.fastmcp import FastMCP
16
+
17
+ # 包目录(pip install 后 publish.py 和 templates/ 在同一目录下)
18
+ PACKAGE_DIR = Path(__file__).resolve().parent
19
+ PUBLISH_SCRIPT = PACKAGE_DIR / "publish.py"
20
+ TEMPLATES_DIR = PACKAGE_DIR / "templates"
21
+
22
+ mcp = FastMCP(
23
+ "loongclaw-devkit",
24
+ instructions=(
25
+ "LoongClaw MCP 开发者工具包。"
26
+ "帮助开发者创建、加密打包、发布 MCP 插件到 LoongClaw 商店。"
27
+ "一条命令完成整个流程,无需手动操作。"
28
+ ),
29
+ )
30
+
31
+
32
+ @mcp.tool()
33
+ def create_mcp_project(
34
+ project_dir: str,
35
+ plugin_id: str,
36
+ plugin_name: str = "",
37
+ description: str = "",
38
+ ) -> str:
39
+ """在指定目录创建一个新的 LoongClaw MCP 插件项目。
40
+
41
+ 自动生成 server.py、requirements.txt、AGENTS.md、publish.py 等文件。
42
+ 创建完成后可直接开始编写工具函数。
43
+
44
+ Args:
45
+ project_dir: 项目目录的绝对路径(会自动创建)
46
+ plugin_id: 插件 ID(字母数字和连字符,如 my-plugin)
47
+ plugin_name: 插件显示名称(留空则用 plugin_id)
48
+ description: 插件描述
49
+ """
50
+ target = Path(project_dir).resolve()
51
+
52
+ if target.exists() and any(target.iterdir()):
53
+ return json.dumps({
54
+ "status": "error",
55
+ "error": f"目录 {target} 已存在且非空",
56
+ "fix": "选择一个空目录或不存在的目录",
57
+ }, ensure_ascii=False)
58
+
59
+ target.mkdir(parents=True, exist_ok=True)
60
+
61
+ name = plugin_name or plugin_id
62
+
63
+ # 复制模板文件
64
+ server_template = TEMPLATES_DIR / "server_template.py"
65
+ if server_template.exists():
66
+ content = server_template.read_text(encoding="utf-8")
67
+ content = content.replace("{{PLUGIN_ID}}", plugin_id)
68
+ content = content.replace("{{PLUGIN_NAME}}", name)
69
+ content = content.replace("{{PLUGIN_DESCRIPTION}}", description)
70
+ (target / "server.py").write_text(content, encoding="utf-8")
71
+
72
+ agents_template = TEMPLATES_DIR / "AGENTS_template.md"
73
+ if agents_template.exists():
74
+ shutil.copy2(agents_template, target / "AGENTS.md")
75
+
76
+ req_template = TEMPLATES_DIR / "requirements_template.txt"
77
+ if req_template.exists():
78
+ shutil.copy2(req_template, target / "requirements.txt")
79
+
80
+ # 复制 publish.py
81
+ if PUBLISH_SCRIPT.exists():
82
+ shutil.copy2(PUBLISH_SCRIPT, target / "publish.py")
83
+
84
+ # 生成初始 .publish.json
85
+ config = {
86
+ "id": plugin_id,
87
+ "name": name,
88
+ "description": description,
89
+ "version": "1.0.0",
90
+ "author": "",
91
+ "icon": "🔧",
92
+ "access": "public",
93
+ "skipEncrypt": [],
94
+ "upload": False,
95
+ }
96
+ (target / ".publish.json").write_text(
97
+ json.dumps(config, ensure_ascii=False, indent=2),
98
+ encoding="utf-8",
99
+ )
100
+
101
+ return json.dumps({
102
+ "status": "success",
103
+ "project_dir": str(target),
104
+ "files_created": ["server.py", "requirements.txt", "AGENTS.md", "publish.py", ".publish.json"],
105
+ "next_steps": [
106
+ f"编辑 {target}/server.py 添加你的工具函数",
107
+ "核心业务逻辑放在 core.py 等独立文件中(自动加密)",
108
+ f"测试: cd {target} && python server.py",
109
+ f"发布: cd {target} && python publish.py",
110
+ ],
111
+ }, ensure_ascii=False)
112
+
113
+
114
+ @mcp.tool()
115
+ def publish_mcp(
116
+ project_dir: str,
117
+ version: str = "",
118
+ access: str = "",
119
+ upload: bool = False,
120
+ skip_encrypt: str = "",
121
+ token: str = "",
122
+ ) -> str:
123
+ """一键加密打包并发布 MCP 插件到 LoongClaw 商店。
124
+
125
+ 自动完成:校验项目 → Cython 加密编译 → 打包 zip → 生成 manifest → 上传服务器。
126
+ 配置保存在 .publish.json 中,后续更新只需传 version。
127
+
128
+ Args:
129
+ project_dir: 项目目录的绝对路径
130
+ version: 版本号(如 1.0.0),留空则读取 .publish.json 中的版本
131
+ access: 访问类型 public 或 private,留空则读取配置
132
+ upload: 是否上传到 LoongClaw 服务器
133
+ skip_encrypt: 不加密的文件列表(逗号分隔),留空则全部加密
134
+ token: LoongClaw API Token(也可用 LOONGCLAW_TOKEN 环境变量)
135
+ """
136
+ target = Path(project_dir).resolve()
137
+ if not target.exists():
138
+ return json.dumps({
139
+ "status": "error",
140
+ "error": f"目录不存在: {target}",
141
+ "fix": "检查路径是否正确",
142
+ }, ensure_ascii=False)
143
+
144
+ if not PUBLISH_SCRIPT.exists():
145
+ return json.dumps({
146
+ "status": "error",
147
+ "error": "publish.py 未找到",
148
+ "fix": f"确认 loongclaw-devkit 安装完整,预期位置: {PUBLISH_SCRIPT}",
149
+ }, ensure_ascii=False)
150
+
151
+ # 构造命令
152
+ cmd = [
153
+ sys.executable, str(PUBLISH_SCRIPT),
154
+ "--auto", "--json-output",
155
+ "--dir", str(target),
156
+ ]
157
+ if version:
158
+ cmd.extend(["--version", version])
159
+ if access:
160
+ cmd.extend(["--access", access])
161
+ if upload:
162
+ cmd.append("--upload")
163
+ else:
164
+ cmd.append("--no-upload")
165
+ if skip_encrypt:
166
+ cmd.extend(["--skip-encrypt", skip_encrypt])
167
+ if token:
168
+ cmd.extend(["--token", token])
169
+
170
+ try:
171
+ result = subprocess.run(
172
+ cmd,
173
+ capture_output=True,
174
+ text=True,
175
+ cwd=str(target),
176
+ )
177
+
178
+ # 尝试从 stdout 提取 JSON(最后一行)
179
+ lines = result.stdout.strip().split("\n")
180
+ for line in reversed(lines):
181
+ line = line.strip()
182
+ if line.startswith("{"):
183
+ try:
184
+ parsed = json.loads(line)
185
+ return json.dumps(parsed, ensure_ascii=False)
186
+ except json.JSONDecodeError:
187
+ continue
188
+
189
+ # 没找到 JSON → 返回原始输出
190
+ return json.dumps({
191
+ "status": "error" if result.returncode != 0 else "unknown",
192
+ "stdout": result.stdout[-2000:] if result.stdout else "",
193
+ "stderr": result.stderr[-2000:] if result.stderr else "",
194
+ "fix": "检查 publish.py 输出",
195
+ }, ensure_ascii=False)
196
+
197
+ except Exception as e:
198
+ return json.dumps({
199
+ "status": "error",
200
+ "error": str(e),
201
+ "fix": "检查 Python 环境和 publish.py 是否可执行",
202
+ }, ensure_ascii=False)
203
+
204
+
205
+ @mcp.tool()
206
+ def check_mcp_status(plugin_id: str = "", project_dir: str = "") -> str:
207
+ """查看 MCP 插件的当前状态。
208
+
209
+ 传入 plugin_id 查询已发布的状态,或传入 project_dir 查看本地项目状态。
210
+
211
+ Args:
212
+ plugin_id: 插件 ID(查询已发布状态时使用)
213
+ project_dir: 项目目录路径(查看本地项目状态时使用)
214
+ """
215
+ result: dict = {}
216
+
217
+ if project_dir:
218
+ target = Path(project_dir).resolve()
219
+ if not target.exists():
220
+ return json.dumps({
221
+ "status": "error",
222
+ "error": f"目录不存在: {target}",
223
+ }, ensure_ascii=False)
224
+
225
+ # 读取 .publish.json
226
+ config_path = target / ".publish.json"
227
+ if config_path.exists():
228
+ try:
229
+ config = json.loads(config_path.read_text(encoding="utf-8"))
230
+ result["config"] = config
231
+ except Exception:
232
+ result["config"] = None
233
+
234
+ # 检查项目文件
235
+ result["files"] = {
236
+ "server.py": (target / "server.py").exists(),
237
+ "requirements.txt": (target / "requirements.txt").exists(),
238
+ "publish.py": (target / "publish.py").exists(),
239
+ "AGENTS.md": (target / "AGENTS.md").exists(),
240
+ "project.zip": (target / "project.zip").exists(),
241
+ ".publish.json": config_path.exists(),
242
+ }
243
+
244
+ # 统计 .py 文件数量
245
+ py_files = list(target.rglob("*.py"))
246
+ result["py_file_count"] = len(py_files)
247
+ result["status"] = "ok"
248
+
249
+ if plugin_id:
250
+ # 尝试查询 Cloud API(需要 token)
251
+ token = os.environ.get("LOONGCLAW_TOKEN", "")
252
+ if token:
253
+ import urllib.request
254
+ import urllib.error
255
+ url = "https://yun.loongclaw.com/v1/store/mcp/registry.json"
256
+ req = urllib.request.Request(
257
+ url, headers={"Authorization": f"Bearer {token}"},
258
+ )
259
+ try:
260
+ with urllib.request.urlopen(req) as resp:
261
+ registry = json.loads(resp.read().decode("utf-8"))
262
+ servers = registry.get("servers", [])
263
+ for s in servers:
264
+ if s.get("id") == plugin_id:
265
+ result["published"] = s
266
+ break
267
+ if "published" not in result:
268
+ result["published"] = None
269
+ result["message"] = f"'{plugin_id}' 未在商店中找到"
270
+ except Exception as e:
271
+ result["registry_error"] = str(e)
272
+ else:
273
+ result["registry_note"] = "设置 LOONGCLAW_TOKEN 环境变量可查询已发布状态"
274
+
275
+ result["status"] = result.get("status", "ok")
276
+
277
+ if not plugin_id and not project_dir:
278
+ return json.dumps({
279
+ "status": "error",
280
+ "error": "请至少提供 plugin_id 或 project_dir",
281
+ "fix": "传入要查询的插件 ID 或项目目录路径",
282
+ }, ensure_ascii=False)
283
+
284
+ return json.dumps(result, ensure_ascii=False, indent=2)
285
+
286
+
287
+ def main():
288
+ mcp.run(transport="stdio")
@@ -0,0 +1,112 @@
1
+ # LoongClaw MCP 插件开发指南
2
+
3
+ ## 你正在开发什么
4
+
5
+ 这是一个 LoongClaw MCP 插件项目。MCP(Model Context Protocol)是让 AI 助手调用外部工具的标准协议。
6
+ 你写的每个 `@mcp.tool()` 函数都会变成 AI 可以调用的工具。
7
+
8
+ ## 项目结构
9
+
10
+ ```
11
+ server.py ← MCP 入口文件(必须保持明文,不要在这里写核心逻辑)
12
+ core.py ← 核心业务逻辑(发布时自动加密编译成 .so/.pyd)
13
+ requirements.txt ← Python 依赖
14
+ publish.py ← 一键发布脚本(不要修改)
15
+ .publish.json ← 发布配置(自动生成,不要手动编辑)
16
+ ```
17
+
18
+ ## 开发规范
19
+
20
+ ### 入口文件 server.py
21
+ - 必须叫 `server.py`,不能改名
22
+ - 必须使用 `from mcp.server.fastmcp import FastMCP`
23
+ - 必须以 `mcp.run(transport="stdio")` 结尾
24
+ - 只做 import + 注册工具 + 启动,不写业务逻辑
25
+ - server.py 发布时**不会被加密**(Python 需要它来启动)
26
+
27
+ ### 工具函数
28
+ - 每个 `@mcp.tool()` 的 **docstring 是 AI 看到的唯一说明**,必须写清楚:
29
+ - 这个工具做什么
30
+ - 每个参数的含义
31
+ - 返回什么
32
+ - 参数用 Python 类型注解(`str`, `int`, `bool`, `list[str]`)
33
+ - 返回值必须是 `str`(MCP 协议要求)
34
+ - 复杂返回值用 `json.dumps()` 序列化
35
+
36
+ ### 核心逻辑
37
+ - 放在独立的 .py 文件中(如 `core.py`、`utils.py`)
38
+ - 在 server.py 中 `from core import xxx` 引用
39
+ - 这些文件发布时**自动加密编译**为 .so/.pyd,用户看不到源码
40
+
41
+ ### 用户配置
42
+ - 通过环境变量传递:`os.environ.get("MY_CONFIG", "默认值")`
43
+ - publish.py 会自动扫描 `os.environ.get()` 调用,生成配置界面
44
+ - 用户在 LoongClaw 安装时会看到配置表单
45
+
46
+ ## 发布
47
+
48
+ ### 交互模式(你来操作)
49
+ ```bash
50
+ python publish.py
51
+ ```
52
+ 按提示回答问题即可。
53
+
54
+ ### 自动模式(AI 操作)
55
+ ```bash
56
+ # 首次发布
57
+ python publish.py --auto --id my-plugin --version 1.0.0 --access public --upload --json-output
58
+
59
+ # 后续更新(读取 .publish.json 配置)
60
+ python publish.py --auto --version 1.1.0 --json-output
61
+
62
+ # 纯本地打包,不上传
63
+ python publish.py --auto --no-upload --json-output
64
+ ```
65
+
66
+ ### 命令行参数
67
+ | 参数 | 说明 | 示例 |
68
+ |------|------|------|
69
+ | `--auto` | 自动模式(不交互) | |
70
+ | `--id` | 插件 ID | `--id my-plugin` |
71
+ | `--name` | 显示名称 | `--name "我的插件"` |
72
+ | `--version` | 版本号 | `--version 1.0.0` |
73
+ | `--access` | public 或 private | `--access private` |
74
+ | `--skip-encrypt` | 不加密的文件 | `--skip-encrypt "config.py,utils.py"` |
75
+ | `--upload` | 上传到服务器 | |
76
+ | `--no-upload` | 不上传 | |
77
+ | `--token` | API Token | `--token lc-xxx` |
78
+ | `--json-output` | JSON 输出 | |
79
+ | `--reconfigure` | 重新配置 | |
80
+
81
+ ### JSON 输出格式
82
+ 成功:
83
+ ```json
84
+ {"status": "success", "version": "1.0.0", "files_encrypted": 3, "uploaded": true}
85
+ ```
86
+
87
+ 失败:
88
+ ```json
89
+ {"status": "error", "step": "encrypt", "error": "SyntaxError at line 42", "fix": "检查 core.py 第 42 行的语法错误"}
90
+ ```
91
+
92
+ **`fix` 字段包含修复建议**——直接按建议修改代码,然后重新运行 `python publish.py --auto --json-output`。
93
+
94
+ ## 常见错误
95
+
96
+ | 错误 | 原因 | 修复 |
97
+ |------|------|------|
98
+ | `Cython not found` | 未安装编译器 | `pip install cython` |
99
+ | `编译失败` | macOS 缺 Xcode 工具 | `xcode-select --install` |
100
+ | `缺少入口文件 server.py` | 文件名不对 | 确保入口叫 `server.py` |
101
+ | `未找到 FastMCP 实例` | server.py 没用 FastMCP | 检查 import 和实例化 |
102
+ | `HTTP 401` | Token 无效 | 重新获取 token 或设置 `LOONGCLAW_TOKEN` 环境变量 |
103
+
104
+ ## 本地测试
105
+
106
+ ```bash
107
+ # 直接运行测试
108
+ python server.py
109
+
110
+ # 或用 MCP inspector
111
+ mcp dev server.py
112
+ ```
@@ -0,0 +1 @@
1
+ mcp[cli]>=1.0.0
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env python3
2
+ """{{PLUGIN_NAME}} — LoongClaw MCP 插件
3
+
4
+ 入口文件,负责注册工具并启动 MCP 服务。
5
+ 核心业务逻辑请放在其他 .py 文件中(发布时会自动加密)。
6
+ """
7
+
8
+ from mcp.server.fastmcp import FastMCP
9
+
10
+ mcp = FastMCP(
11
+ "{{PLUGIN_ID}}",
12
+ instructions=(
13
+ "{{PLUGIN_DESCRIPTION}}"
14
+ ),
15
+ )
16
+
17
+
18
+ @mcp.tool()
19
+ def hello(name: str) -> str:
20
+ """向用户问好。这是一个示例工具,请替换为你的实际功能。
21
+
22
+ Args:
23
+ name: 用户的名字
24
+ """
25
+ return f"你好,{name}!这是 {{PLUGIN_NAME}} 插件。"
26
+
27
+
28
+ # 在下方注册更多工具...
29
+ # 核心逻辑建议放在单独的 .py 文件中(如 core.py),然后 import 使用:
30
+ #
31
+ # from core import process_data
32
+ #
33
+ # @mcp.tool()
34
+ # def my_tool(input_text: str) -> str:
35
+ # """工具描述(AI 只看这段 docstring,写清楚功能和参数)"""
36
+ # return process_data(input_text)
37
+
38
+
39
+ if __name__ == "__main__":
40
+ mcp.run(transport="stdio")
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: loongclaw-devkit
3
+ Version: 0.1.0
4
+ Summary: LoongClaw MCP 开发者工具包 — 一键创建、加密、打包、发布 MCP 插件
5
+ Project-URL: Homepage, https://git.zhigujiaoyu.com.cn/loongclaw/loongclaw-devkit
6
+ Project-URL: Repository, https://git.zhigujiaoyu.com.cn/loongclaw/loongclaw-devkit
7
+ Author: LoongClaw Team
8
+ License-Expression: MIT
9
+ Keywords: ai,devkit,loongclaw,mcp,plugin
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Requires-Python: >=3.9
15
+ Requires-Dist: mcp[cli]>=1.0.0
16
+ Description-Content-Type: text/markdown
17
+
18
+ # LoongClaw DevKit
19
+
20
+ MCP 插件开发者工具包。一键创建、加密打包、发布 MCP 插件到 LoongClaw 商店。
21
+
22
+ ## 安装
23
+
24
+ 推荐使用 [uvx](https://docs.astral.sh/uv/)(零安装直接运行):
25
+
26
+ ```bash
27
+ uvx loongclaw-devkit
28
+ ```
29
+
30
+ 或用 pip 安装:
31
+
32
+ ```bash
33
+ pip install loongclaw-devkit
34
+ ```
35
+
36
+ ## 在 AI 客户端中使用
37
+
38
+ ### LoongClaw 桌面客户端
39
+
40
+ 在 MCP 商店中搜索 `loongclaw-devkit` 一键安装。
41
+
42
+ ### Claude Desktop / Cursor / VS Code
43
+
44
+ 在 MCP 配置文件中添加:
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "loongclaw-devkit": {
50
+ "command": "uvx",
51
+ "args": ["loongclaw-devkit"]
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## 提供的工具
58
+
59
+ | 工具 | 功能 |
60
+ |------|------|
61
+ | `create_mcp_project` | 创建新的 MCP 插件项目(自动生成模板代码) |
62
+ | `publish_mcp` | 一键加密编译 + 打包 + 发布到 LoongClaw 商店 |
63
+ | `check_mcp_status` | 查看插件的本地和已发布状态 |
64
+
65
+ ## 快速开始
66
+
67
+ 让 AI 帮你完成整个流程:
68
+
69
+ > "帮我创建一个天气查询的 MCP 插件,插件 ID 叫 weather-mcp"
70
+
71
+ AI 会自动调用 `create_mcp_project` 生成项目骨架,你只需编写核心逻辑。
72
+
73
+ 完成后:
74
+
75
+ > "把这个插件发布到 LoongClaw 商店"
76
+
77
+ AI 调用 `publish_mcp` 完成加密编译、打包、上传。
78
+
79
+ ## 发布到商店
80
+
81
+ 上传到 LoongClaw 商店需要 API Token:
82
+
83
+ ```bash
84
+ export LOONGCLAW_TOKEN="your-token-here"
85
+ ```
86
+
87
+ Token 获取方式:在 LoongClaw 客户端「设置 → 开发者」中生成。
88
+
89
+ 没有 Token 也可以正常使用创建项目和本地打包功能。
90
+
91
+ ## 手动使用
92
+
93
+ 也可以不通过 AI,直接命令行运行:
94
+
95
+ ```bash
96
+ # 启动 MCP server(开发调试用)
97
+ loongclaw-devkit
98
+
99
+ # 或用 Python 模块方式
100
+ python -m loongclaw_devkit
101
+ ```
102
+
103
+ ## License
104
+
105
+ MIT
@@ -0,0 +1,11 @@
1
+ loongclaw_devkit/__init__.py,sha256=r_AyCIRhzjgTi_jJ-QKIrqssGoLiJlrNrzuRl5GbhX4,112
2
+ loongclaw_devkit/__main__.py,sha256=f5HJaAAU3rN_zKkxMf0TNpjb2HnuamEvbxr2_44G44M,90
3
+ loongclaw_devkit/publish.py,sha256=lP5sK_pO7LWCbNNJ4m3mZJvp4f_tcoIRVkEO6tOJT4c,23649
4
+ loongclaw_devkit/server.py,sha256=vTyWZjjzix5ffZuL6xXQsNp_BTHt_lNhgMsoW33S3Qk,9836
5
+ loongclaw_devkit/templates/AGENTS_template.md,sha256=2C9ok1Rx1STZCAaRPgHDKu78mGaaZXLmCMGSvM5aWFY,3777
6
+ loongclaw_devkit/templates/requirements_template.txt,sha256=HZOgIbNCU2k_Aak14zrp6cmD-W2gkbqENLiKHbLdPQI,16
7
+ loongclaw_devkit/templates/server_template.py,sha256=nwIB5_2XjhJUJlVBdhGisiaO6Sxq8e7phC-00TP-iJU,989
8
+ loongclaw_devkit-0.1.0.dist-info/METADATA,sha256=s1-0FuFHZ0BCjGB6PIXSWBOEy3XHZFUhT2CPMCkmMIw,2452
9
+ loongclaw_devkit-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ loongclaw_devkit-0.1.0.dist-info/entry_points.txt,sha256=nG_rW60rl-BBSBDlDBaFZMTWB-dGIbeeBsre85MILgY,59
11
+ loongclaw_devkit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ loongclaw-devkit = loongclaw_devkit:main