crewly 1.20.130 → 1.20.132

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 (136) hide show
  1. package/config/skills/agent/core/find-skill/SKILL.md +58 -0
  2. package/config/skills/agent/core/find-skill/execute.sh +67 -0
  3. package/config/skills/agent/core/find-skill/execute.test.sh +69 -0
  4. package/config/skills/agent/core/generate-pdf/SKILL.md +24 -29
  5. package/config/skills/agent/core/generate-pdf/execute.sh +22 -78
  6. package/config/skills/agent/core/generate-pdf/execute.test.sh +45 -0
  7. package/config/skills/agent/core/install-skill/SKILL.md +67 -0
  8. package/config/skills/agent/core/install-skill/execute.sh +80 -0
  9. package/config/skills/agent/core/install-skill/execute.test.sh +74 -0
  10. package/config/skills/agent/pdf-tools/SKILL.md +103 -0
  11. package/config/skills/agent/pdf-tools/execute.sh +301 -0
  12. package/config/skills/agent/pdf-tools/execute.test.sh +218 -0
  13. package/config/skills/agent/pdf-tools/instructions.md +14 -0
  14. package/config/skills/agent/pdf-tools/pdf_tools.py +268 -0
  15. package/config/skills/agent/pdf-tools/skill.json +103 -0
  16. package/config/skills/agent/pdf-tools/style.css +143 -0
  17. package/config/skills/agent/transcribe-audio/SKILL.md +42 -5
  18. package/config/skills/agent/transcribe-audio/execute.sh +44 -11
  19. package/config/skills/agent/transcribe-audio/execute.test.sh +90 -0
  20. package/config/skills/agent/transcribe-audio/install-whisper-cpp.sh +126 -0
  21. package/config/skills/agent/transcribe-audio/install-whisper-cpp.test.sh +69 -0
  22. package/config/skills/agent/transcribe-audio/skill.json +67 -6
  23. package/config/skills/marketplace-registry.ts +8 -1
  24. package/config/skills/registry.json +337 -3
  25. package/dist/backend/backend/src/constants.d.ts +71 -0
  26. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  27. package/dist/backend/backend/src/constants.js +96 -0
  28. package/dist/backend/backend/src/constants.js.map +1 -1
  29. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.d.ts +49 -0
  30. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.d.ts.map +1 -0
  31. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.js +185 -0
  32. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.js.map +1 -0
  33. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.d.ts +21 -0
  34. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.d.ts.map +1 -0
  35. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.js +29 -0
  36. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.js.map +1 -0
  37. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  38. package/dist/backend/backend/src/routes/api.routes.js +5 -0
  39. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  40. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +14 -0
  41. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  42. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +33 -1
  43. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  44. package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts.map +1 -1
  45. package/dist/backend/backend/src/services/event-bus/event-bus.service.js +7 -0
  46. package/dist/backend/backend/src/services/event-bus/event-bus.service.js.map +1 -1
  47. package/dist/backend/backend/src/services/marketplace/marketplace.service.d.ts.map +1 -1
  48. package/dist/backend/backend/src/services/marketplace/marketplace.service.js +11 -3
  49. package/dist/backend/backend/src/services/marketplace/marketplace.service.js.map +1 -1
  50. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.d.ts +281 -0
  51. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.d.ts.map +1 -0
  52. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.js +559 -0
  53. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.js.map +1 -0
  54. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.d.ts +243 -0
  55. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.d.ts.map +1 -0
  56. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.js +439 -0
  57. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.js.map +1 -0
  58. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.d.ts +138 -0
  59. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.d.ts.map +1 -0
  60. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.js +281 -0
  61. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.js.map +1 -0
  62. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.d.ts +338 -0
  63. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.d.ts.map +1 -0
  64. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.js +761 -0
  65. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.js.map +1 -0
  66. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  67. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +6 -0
  68. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  69. package/dist/backend/backend/src/types/marketplace.types.d.ts +16 -1
  70. package/dist/backend/backend/src/types/marketplace.types.d.ts.map +1 -1
  71. package/dist/backend/backend/src/utils/inbound-file-hint.utils.d.ts +55 -0
  72. package/dist/backend/backend/src/utils/inbound-file-hint.utils.d.ts.map +1 -0
  73. package/dist/backend/backend/src/utils/inbound-file-hint.utils.js +78 -0
  74. package/dist/backend/backend/src/utils/inbound-file-hint.utils.js.map +1 -0
  75. package/dist/backend/build-info.json +2 -2
  76. package/dist/cli/backend/src/constants.d.ts +71 -0
  77. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  78. package/dist/cli/backend/src/constants.js +96 -0
  79. package/dist/cli/backend/src/constants.js.map +1 -1
  80. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +14 -0
  81. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  82. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +33 -1
  83. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  84. package/dist/cli/backend/src/services/event-bus/event-bus.service.d.ts.map +1 -1
  85. package/dist/cli/backend/src/services/event-bus/event-bus.service.js +7 -0
  86. package/dist/cli/backend/src/services/event-bus/event-bus.service.js.map +1 -1
  87. package/dist/cli/backend/src/services/marketplace/marketplace.service.d.ts +101 -0
  88. package/dist/cli/backend/src/services/marketplace/marketplace.service.d.ts.map +1 -0
  89. package/dist/cli/backend/src/services/marketplace/marketplace.service.js +392 -0
  90. package/dist/cli/backend/src/services/marketplace/marketplace.service.js.map +1 -0
  91. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.d.ts +281 -0
  92. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.d.ts.map +1 -0
  93. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.js +559 -0
  94. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.js.map +1 -0
  95. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.d.ts +138 -0
  96. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.d.ts.map +1 -0
  97. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.js +281 -0
  98. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.js.map +1 -0
  99. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.d.ts +338 -0
  100. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.d.ts.map +1 -0
  101. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.js +761 -0
  102. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.js.map +1 -0
  103. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  104. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +6 -0
  105. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  106. package/dist/cli/backend/src/types/marketplace.types.d.ts +219 -0
  107. package/dist/cli/backend/src/types/marketplace.types.d.ts.map +1 -0
  108. package/dist/cli/backend/src/types/marketplace.types.js +6 -0
  109. package/dist/cli/backend/src/types/marketplace.types.js.map +1 -0
  110. package/dist/cli/backend/src/utils/inbound-file-hint.utils.d.ts +55 -0
  111. package/dist/cli/backend/src/utils/inbound-file-hint.utils.d.ts.map +1 -0
  112. package/dist/cli/backend/src/utils/inbound-file-hint.utils.js +78 -0
  113. package/dist/cli/backend/src/utils/inbound-file-hint.utils.js.map +1 -0
  114. package/dist/cli/backend/src/utils/package-root.d.ts +15 -0
  115. package/dist/cli/backend/src/utils/package-root.d.ts.map +1 -0
  116. package/dist/cli/backend/src/utils/package-root.js +38 -0
  117. package/dist/cli/backend/src/utils/package-root.js.map +1 -0
  118. package/dist/cli/cli/src/commands/install.d.ts +13 -1
  119. package/dist/cli/cli/src/commands/install.d.ts.map +1 -1
  120. package/dist/cli/cli/src/commands/install.js +40 -3
  121. package/dist/cli/cli/src/commands/install.js.map +1 -1
  122. package/dist/cli/cli/src/commands/skills.d.ts +58 -0
  123. package/dist/cli/cli/src/commands/skills.d.ts.map +1 -0
  124. package/dist/cli/cli/src/commands/skills.js +132 -0
  125. package/dist/cli/cli/src/commands/skills.js.map +1 -0
  126. package/dist/cli/cli/src/index.js +12 -3
  127. package/dist/cli/cli/src/index.js.map +1 -1
  128. package/dist/cli/cli/src/utils/archive-creator.d.ts +14 -0
  129. package/dist/cli/cli/src/utils/archive-creator.d.ts.map +1 -1
  130. package/dist/cli/cli/src/utils/archive-creator.js +24 -3
  131. package/dist/cli/cli/src/utils/archive-creator.js.map +1 -1
  132. package/dist/cli/cli/src/utils/package-validator.d.ts +2 -0
  133. package/dist/cli/cli/src/utils/package-validator.d.ts.map +1 -1
  134. package/dist/cli/cli/src/utils/package-validator.js +16 -0
  135. package/dist/cli/cli/src/utils/package-validator.js.map +1 -1
  136. package/package.json +1 -1
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: Find Skill
3
+ description: Find a skill that gives you a capability you lack (a file type you cannot read, a missing tool). Searches bundled Crewly skills, installed skills and the marketplace; says which are official, installed and ready. Run it BEFORE telling the user you cannot do something.
4
+ version: 1.0.0
5
+ category: automation
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - "*"
9
+ triggers:
10
+ - find skill
11
+ - missing capability
12
+ - cannot read this file
13
+ - not installed
14
+ - need a tool
15
+ tags:
16
+ - skills
17
+ - marketplace
18
+ - install
19
+ - capability
20
+ - auto-install
21
+ execution:
22
+ type: script
23
+ script:
24
+ file: execute.sh
25
+ interpreter: bash
26
+ timeoutMs: 60000
27
+ ---
28
+
29
+ # Find Skill
30
+
31
+ Before you tell the user "I can't do that" or "X is not installed", look for a
32
+ skill that does it.
33
+
34
+ ```bash
35
+ bash {{AGENT_SKILLS_PATH}}/core/find-skill/execute.sh --query "transcribe a voice message"
36
+ bash {{AGENT_SKILLS_PATH}}/core/find-skill/execute.sh --query "read a pdf" --limit 3
37
+ ```
38
+
39
+ ## Output
40
+
41
+ ```json
42
+ {"success":true,"query":"transcribe a voice message",
43
+ "next":"transcribe-audio is an official skill (bundled with Crewly) that is not ready yet. Tell the user in one line that you are installing it (about 6 min), then run install-skill --id transcribe-audio and continue when the completion message arrives.",
44
+ "candidates":[{"id":"transcribe-audio","official":true,"officialReason":"bundled with Crewly",
45
+ "installed":true,"ready":false,"setup":{"declared":true,"estimatedMinutes":6,"satisfied":false,"missing":["whisper-cli","whisper-model"]},
46
+ "executePath":"/…/transcribe-audio/execute.sh","source":"bundled"}]}
47
+ ```
48
+
49
+ Follow `next`:
50
+
51
+ - **ready** — use the skill now (`executePath`).
52
+ - **official, not ready** — tell the user in one line that you are installing it
53
+ and roughly how long it takes, then `install-skill --id <id>`.
54
+ - **third-party** (`official:false`) — ask the owner in chat first; install only
55
+ after they say yes (`install-skill --id <id> --approved-by-owner`).
56
+ - **no candidates** — say plainly what you cannot do.
57
+
58
+ Official = bundled with Crewly, or listed in the official registry by the Crewly Team.
@@ -0,0 +1,67 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # find-skill — find a skill that gives you a capability you lack.
4
+ #
5
+ # Searches the skills bundled with Crewly, installed marketplace skills and
6
+ # the marketplace registry by capability, name, tags and description.
7
+ # Backed by GET /api/skill-setup/find.
8
+ #
9
+ # Usage:
10
+ # bash execute.sh --query "transcribe a voice message"
11
+ # bash execute.sh --query "read a pdf" --limit 3
12
+ # bash execute.sh '{"query":"…"}'
13
+ #
14
+ # Output: {success, query, candidates:[{id, name, official, officialReason,
15
+ # installed, ready, setup:{declared, estimatedMinutes, satisfied, missing},
16
+ # executePath, …}], registryAvailable, next}
17
+ # `next` says what to do: use it, install it (official), or ask the owner.
18
+ # =============================================================================
19
+ set -euo pipefail
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
22
+
23
+ print_usage() {
24
+ cat <<'EOF_USAGE'
25
+ Usage:
26
+ bash execute.sh --query "transcribe a voice message" [--limit 5]
27
+ bash execute.sh '{"query":"…","limit":5}'
28
+
29
+ Options:
30
+ --query | -q What you need, in plain words (required)
31
+ --limit Maximum candidates (default 8)
32
+ --help | -h Show this help
33
+ EOF_USAGE
34
+ }
35
+
36
+ INPUT_JSON=""; QUERY=""; LIMIT=""
37
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then INPUT_JSON="$1"; shift || true; fi
38
+ while [[ $# -gt 0 ]]; do
39
+ case "$1" in
40
+ --query|-q) [ $# -ge 2 ] || error_exit "--query requires a value"; QUERY="$2"; shift 2 ;;
41
+ --limit) [ $# -ge 2 ] || error_exit "--limit requires a value"; LIMIT="$2"; shift 2 ;;
42
+ --help|-h) print_usage; exit 0 ;;
43
+ *) error_exit "Unknown option: $1" ;;
44
+ esac
45
+ done
46
+ if [ -n "$INPUT_JSON" ]; then
47
+ INPUT=$(read_json_input "$INPUT_JSON")
48
+ [ -z "$QUERY" ] && QUERY=$(printf '%s' "$INPUT" | jq -r '.query // empty')
49
+ [ -z "$LIMIT" ] && LIMIT=$(printf '%s' "$INPUT" | jq -r '.limit // empty')
50
+ fi
51
+ require_param "query (--query)" "$QUERY"
52
+ [ -z "$LIMIT" ] || [[ "$LIMIT" =~ ^[0-9]+$ ]] || error_exit "--limit must be a number"
53
+
54
+ ENDPOINT="/skill-setup/find?query=$(printf '%s' "$QUERY" | jq -sRr @uri)"
55
+ [ -n "$LIMIT" ] && ENDPOINT="${ENDPOINT}&limit=${LIMIT}"
56
+
57
+ ERR_FILE=$(mktemp); trap 'rm -f "$ERR_FILE"' EXIT
58
+ RESPONSE=$(api_call GET "$ENDPOINT" 2>"$ERR_FILE") || {
59
+ ERR=$(tail -n 1 "$ERR_FILE")
60
+ printf '%s' "$ERR" | jq -c '{success: false, error: (.details.error // .error // "find failed")}' 2>/dev/null \
61
+ || jq -cn --arg e "$ERR" '{success: false, error: $e}'
62
+ exit 1
63
+ }
64
+
65
+ printf '%s' "$RESPONSE" | jq -c '.data | {success: true, query, next, registryAvailable,
66
+ candidates: [.candidates[] | {id, name, description: (.description | .[0:200]), official, officialReason,
67
+ installed, ready, setup, executePath, source}]}'
@@ -0,0 +1,69 @@
1
+ #!/bin/bash
2
+ # Tests for find-skill — run with: bash execute.test.sh </dev/null
3
+ # A python HTTP stub plays the backend; asserts the request and the output shape.
4
+ set -u
5
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ EXEC="$HERE/execute.sh"
7
+ PASS=0; FAIL=0
8
+ check() {
9
+ local name="$1"; local got="$2"; local want="$3"
10
+ if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
11
+ }
12
+
13
+ PORT=18851
14
+ STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
15
+ python3 -u <<'PY' >/dev/null 2>&1 &
16
+ from http.server import BaseHTTPRequestHandler, HTTPServer
17
+ from urllib.parse import urlparse, parse_qs
18
+ import json, os
19
+ LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
20
+ class H(BaseHTTPRequestHandler):
21
+ def reply(self, code, body):
22
+ self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers()
23
+ self.wfile.write(json.dumps(body).encode())
24
+ def do_GET(self):
25
+ u = urlparse(self.path); q = parse_qs(u.query)
26
+ open(LOG, 'w').write(json.dumps({'path': u.path, 'query': q, 'agent': self.headers.get('X-Agent-Session')}))
27
+ if q.get('query', [''])[0] == 'boom':
28
+ return self.reply(500, {'success': False, 'error': 'registry exploded'})
29
+ self.reply(200, {'success': True, 'data': {'query': q['query'][0], 'registryAvailable': True,
30
+ 'next': 'transcribe-audio is an official skill (bundled with Crewly) that is not ready yet. Tell the user…',
31
+ 'candidates': [{'id': 'transcribe-audio', 'name': 'transcribe-audio', 'description': 'x' * 500, 'official': True,
32
+ 'officialReason': 'bundled with Crewly', 'installed': True, 'ready': False, 'tags': ['audio'], 'triggers': [],
33
+ 'setup': {'declared': True, 'estimatedMinutes': 6, 'satisfied': False, 'missing': ['whisper-cli']},
34
+ 'executePath': '/pkg/transcribe-audio/execute.sh', 'source': 'bundled', 'score': 12}]}})
35
+ def log_message(self, *a, **k): pass
36
+ HTTPServer(('127.0.0.1', PORT), H).serve_forever()
37
+ PY
38
+ STUB_PID=$!
39
+ disown "$STUB_PID" 2>/dev/null || true
40
+ trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
41
+ for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
42
+
43
+ run() { CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
44
+ run_err() { CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null | jq -rs 'last.error' 2>/dev/null; }
45
+
46
+ OUT=$(run --query "transcribe a voice message (m4a)" --limit 3)
47
+ check "path" "$(jq -r .path "$STUB_LOG")" "/api/skill-setup/find"
48
+ check "query encoded + sent" "$(jq -r '.query.query[0]' "$STUB_LOG")" "transcribe a voice message (m4a)"
49
+ check "limit sent" "$(jq -r '.query.limit[0]' "$STUB_LOG")" "3"
50
+ check "agent header" "$(jq -r .agent "$STUB_LOG")" "test-agent"
51
+ check "success" "$(printf '%s' "$OUT" | jq -r .success)" "true"
52
+ check "next passed through" "$(printf '%s' "$OUT" | jq -r '.next | startswith("transcribe-audio is an official skill")')" "true"
53
+ check "candidate fields" "$(printf '%s' "$OUT" | jq -c '.candidates[0] | {id, official, ready, source, missing: .setup.missing}')" '{"id":"transcribe-audio","official":true,"ready":false,"source":"bundled","missing":["whisper-cli"]}'
54
+ check "description trimmed" "$(printf '%s' "$OUT" | jq -r '.candidates[0].description | length')" "200"
55
+ check "internal score dropped" "$(printf '%s' "$OUT" | jq -r '.candidates[0] | has("score")')" "false"
56
+
57
+ run '{"query":"语音 转文字"}' >/dev/null
58
+ check "json input" "$(jq -r '.query.query[0]' "$STUB_LOG")" "语音 转文字"
59
+
60
+ OUT=$(run --query boom); RC=$?
61
+ check "backend error: exit 1" "$RC" "1"
62
+ check "backend error: message" "$(printf '%s' "$OUT" | jq -r .error)" "registry exploded"
63
+
64
+ check "missing query" "$(run_err)" "Missing required parameter: query (--query)"
65
+ check "bad limit" "$(run_err --query x --limit lots)" "--limit must be a number"
66
+ check "unknown option" "$(run_err --install)" "Unknown option: --install"
67
+
68
+ echo "=== Results: $PASS passed, $FAIL failed ==="
69
+ [ "$FAIL" -eq 0 ]
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: Generate PDF
3
- description: Convert a Markdown file to a styled PDF with full CJK (Chinese/Japanese/Korean) font support. Uses pandoc for Markdown→HTML and weasyprint for HTML→PDF rendering.
4
- version: 1.0.0
3
+ description: Convert a Markdown (or HTML) file to a styled PDF with full CJK (Chinese/Japanese/Korean) font support. Thin wrapper over the pdf-tools skill — headless Chrome preferred, WeasyPrint fallback. For multi-part documents, merging or reading PDFs use pdf-tools directly.
4
+ version: 2.0.0
5
5
  category: document
6
6
  skillType: claude-skill
7
7
  assignableRoles:
@@ -29,7 +29,7 @@ tags:
29
29
  - pdf
30
30
  - document
31
31
  - markdown
32
- - pandoc
32
+ - chrome
33
33
  - weasyprint
34
34
  - cjk
35
35
  execution:
@@ -42,47 +42,42 @@ execution:
42
42
 
43
43
  # Generate PDF
44
44
 
45
- Convert a Markdown file to a professionally styled PDF with full CJK (Chinese/Japanese/Korean) font support.
45
+ Convert a Markdown (or HTML) file to a professionally styled PDF with full CJK
46
+ (Chinese/Japanese/Korean) font support.
47
+
48
+ This skill is kept for compatibility; it calls **`pdf-tools`**
49
+ (`{{AGENT_SKILLS_PATH}}/pdf-tools/execute.sh`), which also merges several parts
50
+ into one PDF and reads PDFs. Prefer `pdf-tools` for anything beyond one file.
46
51
 
47
52
  ## Pipeline
48
53
 
49
- 1. **Markdown → HTML**: Uses `pandoc` with `--standalone` and an inline CJK-compatible CSS header
50
- 2. **HTML → PDF**: Uses `weasyprint` (NOT Chrome headless, which cannot render CJK characters)
54
+ 1. **Markdown → HTML** with a clean A4 stylesheet (CJK font stacks: PingFang /
55
+ Hiragino on macOS, Noto CJK on Linux). HTML input is printed as-is.
56
+ 2. **HTML → PDF** with headless Chrome/Chromium when installed, else WeasyPrint.
57
+ Chrome renders CJK correctly whenever CJK fonts are installed (macOS has them;
58
+ on Linux the pdf-tools setup installs `fonts-noto-cjk`).
51
59
 
52
60
  ## Parameters
53
61
 
54
62
  | Parameter | Required | Description |
55
63
  |-----------|----------|-------------|
56
- | `input` | Yes | Path to the input Markdown file |
57
- | `output` | No | Path for the output PDF file (defaults to input path with `.pdf` extension) |
58
- | `title` | No | Document title for the HTML metadata |
64
+ | `input` | Yes | Path to the input Markdown (or HTML) file |
65
+ | `output` | No | Path for the output PDF (defaults to the input path with `.pdf`) |
66
+ | `title` | No | Document title |
67
+ | `css` | No | Extra stylesheet appended to the default one |
68
+ | `engine` | No | `auto` (default), `chrome`, `weasyprint` |
59
69
 
60
70
  ## Example
61
71
 
62
72
  ```bash
63
- bash config/skills/agent/core/generate-pdf/execute.sh '{"input":"/tmp/report.md","output":"/tmp/report.pdf","title":"My Report"}'
64
- ```
65
-
66
- ### Minimal usage (auto-generates output path)
67
-
68
- ```bash
69
- bash config/skills/agent/core/generate-pdf/execute.sh '{"input":"/tmp/report.md"}'
73
+ bash {{AGENT_SKILLS_PATH}}/core/generate-pdf/execute.sh '{"input":"/tmp/report.md","output":"/tmp/report.pdf","title":"My Report"}'
70
74
  ```
71
75
 
72
- This produces `/tmp/report.pdf`.
73
-
74
- ## Important Notes
75
-
76
- - **Always use this skill** instead of manually running pandoc + Chrome headless
77
- - **Chrome headless cannot render Chinese/CJK characters** — they appear blank in the PDF
78
- - **weasyprint** properly embeds CJK fonts (PingFang SC, Hiragino Sans GB, STHeiti) on macOS
79
- - The skill auto-generates a CSS header with CJK font declarations if one doesn't exist
80
- - Output is a clean, styled PDF suitable for sharing via Slack or email
81
-
82
76
  ## Output
83
77
 
84
- JSON with the path to the generated PDF file:
85
-
86
78
  ```json
87
- {"success": true, "pdf": "/tmp/report.pdf", "size": 364024}
79
+ {"success": true, "pdf": "/tmp/report.pdf", "size": 364024, "engine": "chrome"}
88
80
  ```
81
+
82
+ If a dependency is missing the result carries `"needsSetup": true, "skill": "pdf-tools"`:
83
+ tell the user you are setting up the PDF tools and run `install-skill --id pdf-tools`.
@@ -1,88 +1,32 @@
1
1
  #!/bin/bash
2
- # Generate PDF from Markdown with CJK font support
3
- # Uses pandoc (md→html) + weasyprint (html→pdf)
2
+ # =============================================================================
3
+ # generate-pdf — Markdown (or HTML) → styled PDF with CJK support.
4
+ #
5
+ # Kept for compatibility: the work is done by the `pdf-tools` skill
6
+ # (headless Chrome preferred, WeasyPrint fallback, CJK-ready stylesheet).
7
+ # Input and output shapes are unchanged:
8
+ # bash execute.sh '{"input":"/tmp/report.md","output":"/tmp/report.pdf","title":"My Report"}'
9
+ # → {"success":true,"pdf":"/tmp/report.pdf","size":364024,"engine":"chrome"}
10
+ # A missing dependency returns pdf-tools' `needsSetup` JSON
11
+ # (run install-skill --id pdf-tools).
12
+ # =============================================================================
4
13
  set -euo pipefail
5
14
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
15
  source "${SCRIPT_DIR}/../../_common/lib.sh"
16
+ PDF_TOOLS="${SCRIPT_DIR}/../../pdf-tools/execute.sh"
7
17
 
8
18
  INPUT=$(read_json_input "${1:-}")
9
19
  [ -z "$INPUT" ] && error_exit "Usage: execute.sh '{\"input\":\"/tmp/report.md\",\"output\":\"/tmp/report.pdf\",\"title\":\"My Report\"}'"
20
+ [ -f "$PDF_TOOLS" ] || error_exit "pdf-tools skill not found at ${PDF_TOOLS}"
10
21
 
11
- MD_PATH=$(printf '%s' "$INPUT" | jq -r '.input // empty')
12
- PDF_PATH=$(printf '%s' "$INPUT" | jq -r '.output // empty')
13
- TITLE=$(printf '%s' "$INPUT" | jq -r '.title // empty')
22
+ SRC=$(printf '%s' "$INPUT" | jq -r '.input // empty')
23
+ require_param "input" "$SRC"
14
24
 
15
- require_param "input" "$MD_PATH"
25
+ REQUEST=$(printf '%s' "$INPUT" | jq -c '{action: "render", input: .input}
26
+ + (if .output then {output: .output} else {} end)
27
+ + (if .title then {title: .title} else {} end)
28
+ + (if .css then {css: .css} else {} end)
29
+ + (if .engine then {engine: .engine} else {} end)')
16
30
 
17
- # Default output: same path with .pdf extension
18
- if [ -z "$PDF_PATH" ]; then
19
- PDF_PATH="${MD_PATH%.md}.pdf"
20
- fi
21
-
22
- # Intermediate HTML path
23
- HTML_PATH="${PDF_PATH%.pdf}.html"
24
-
25
- # Default title from filename
26
- if [ -z "$TITLE" ]; then
27
- TITLE=$(basename "$MD_PATH" .md | tr '-' ' ' | tr '_' ' ')
28
- fi
29
-
30
- # Ensure the CJK CSS header exists
31
- CJK_HEADER="/tmp/cjk-header.html"
32
- if [ ! -f "$CJK_HEADER" ]; then
33
- cat > "$CJK_HEADER" << 'CSSEOF'
34
- <style>
35
- body {
36
- font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
37
- max-width: 900px;
38
- margin: 0 auto;
39
- padding: 2rem;
40
- line-height: 1.8;
41
- color: #1a1a1a;
42
- font-size: 14px;
43
- }
44
- h1, h2, h3, h4 {
45
- font-family: -apple-system, "PingFang SC", "Hiragino Sans GB", sans-serif;
46
- margin-top: 1.5em;
47
- color: #111;
48
- }
49
- h1 { font-size: 1.8em; border-bottom: 2px solid #333; padding-bottom: 0.3em; }
50
- h2 { font-size: 1.4em; border-bottom: 1px solid #ccc; padding-bottom: 0.2em; }
51
- h3 { font-size: 1.15em; }
52
- table { border-collapse: collapse; width: 100%; margin: 1em 0; font-size: 13px; }
53
- th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
54
- th { background: #f5f5f5; font-weight: 600; }
55
- tr:nth-child(even) { background: #fafafa; }
56
- blockquote { border-left: 4px solid #4a90d9; margin: 1em 0; padding: 0.5em 1em; background: #f0f6ff; color: #333; }
57
- code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-size: 0.9em; }
58
- hr { border: none; border-top: 1px solid #ddd; margin: 2em 0; }
59
- strong { color: #111; }
60
- </style>
61
- CSSEOF
62
- fi
63
-
64
- # Check dependencies
65
- if ! command -v pandoc &>/dev/null; then
66
- error_exit "pandoc is not installed. Run: brew install pandoc"
67
- fi
68
- if ! command -v weasyprint &>/dev/null; then
69
- error_exit "weasyprint is not installed. Run: brew install weasyprint"
70
- fi
71
-
72
- # Step 1: Markdown → HTML (with CJK CSS header)
73
- pandoc "$MD_PATH" -o "$HTML_PATH" \
74
- --standalone \
75
- --metadata title="$TITLE" \
76
- --include-in-header="$CJK_HEADER" 2>/dev/null
77
-
78
- # Step 2: HTML → PDF (weasyprint handles CJK fonts correctly)
79
- weasyprint "$HTML_PATH" "$PDF_PATH" 2>/dev/null
80
-
81
- # Get file size
82
- PDF_SIZE=$(wc -c < "$PDF_PATH" | tr -d ' ')
83
-
84
- # Clean up intermediate HTML
85
- rm -f "$HTML_PATH"
86
-
87
- # Output result
88
- echo "{\"success\":true,\"pdf\":\"${PDF_PATH}\",\"size\":${PDF_SIZE}}"
31
+ RESULT=$(bash "$PDF_TOOLS" "$REQUEST" </dev/null) || { printf '%s\n' "$RESULT"; exit 1; }
32
+ printf '%s' "$RESULT" | jq -c '{success, pdf, size, engine}'
@@ -0,0 +1,45 @@
1
+ #!/bin/bash
2
+ # Tests for generate-pdf (wrapper over pdf-tools) — run with: bash execute.test.sh </dev/null
3
+ # Uses a fake Chrome and a stub python-markdown on an isolated PATH. Exit 0 on pass.
4
+ set -u
5
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ EXEC="$HERE/execute.sh"
7
+ PASS=0; FAIL=0
8
+ check() {
9
+ local name="$1"; local got="$2"; local want="$3"
10
+ if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
11
+ }
12
+ JQ="$(command -v jq)"; BASH_BIN="${TEST_BASH:-$(command -v bash)}"
13
+ T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
14
+ mkdir -p "$T/bin" "$T/stub/markdown"
15
+ for tool in bash jq python3 mktemp cat rm tail head dirname mkdir sed tr wc sleep kill id awk grep basename cp uname date; do
16
+ src="$(command -v "$tool" 2>/dev/null)" && ln -sf "$src" "$T/bin/$tool"
17
+ done
18
+ printf '%%PDF-1.4\n%% fake\n%%%%EOF\n' > "$T/fixture.pdf"
19
+ cat > "$T/chrome" <<EOT
20
+ #!/bin/bash
21
+ for a in "\$@"; do case "\$a" in --print-to-pdf=*) cp "$T/fixture.pdf" "\${a#--print-to-pdf=}" ;; esac; done
22
+ EOT
23
+ chmod 755 "$T/chrome"
24
+ printf 'def markdown(text, **kw):\n return "<p>" + text + "</p>"\n' > "$T/stub/markdown/__init__.py"
25
+ printf '# 报告\n' > "$T/report.md"
26
+ run() { env -i HOME="$T" PATH="$T/bin" CREWLY_HOME="$T/.crewly" PYTHONPATH="$T/stub" PDF_TOOLS_CHROME_PATHS="" "$@" "$BASH_BIN" "$EXEC" "$JSON" 2>/dev/null </dev/null; }
27
+
28
+ JSON="{\"input\":\"$T/report.md\",\"title\":\"报告\"}"
29
+ OUT=$(run CHROME_BIN="$T/chrome")
30
+ check "same output shape" "$(printf '%s' "$OUT" | "$JQ" -c 'keys')" '["engine","pdf","size","success"]'
31
+ check "success" "$(printf '%s' "$OUT" | "$JQ" -r .success)" "true"
32
+ check "default output path" "$(printf '%s' "$OUT" | "$JQ" -r .pdf)" "$T/report.pdf"
33
+ check "engine" "$(printf '%s' "$OUT" | "$JQ" -r .engine)" "chrome"
34
+
35
+ JSON="{\"input\":\"$T/report.md\",\"output\":\"$T/x/out.pdf\"}"
36
+ OUT=$(run); RC=$?
37
+ check "no engine: exit 1" "$RC" "1"
38
+ check "no engine: needsSetup from pdf-tools" "$(printf '%s' "$OUT" | "$JQ" -r '.needsSetup, .skill' | tr '\n' ' ')" "true pdf-tools "
39
+
40
+ JSON='{"title":"x"}'
41
+ ERR=$(env -i HOME="$T" PATH="$T/bin" "$BASH_BIN" "$EXEC" "$JSON" 2>&1 >/dev/null </dev/null)
42
+ check "missing input" "$(printf '%s' "$ERR" | "$JQ" -rs 'last.error')" "Missing required parameter: input"
43
+
44
+ echo "=== Results: $PASS passed, $FAIL failed ==="
45
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: Install Skill
3
+ description: Install a skill (and the system tools, models and Python packages it declares) as a background job. Returns a job id at once; you get a [SKILL INSTALLED] / [SKILL INSTALL FAILED] message when it ends. Official skills install without asking; third-party ones need the owner's yes in chat first.
4
+ version: 1.0.0
5
+ category: automation
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - "*"
9
+ triggers:
10
+ - install skill
11
+ - set up skill
12
+ - needsSetup
13
+ - missing dependency
14
+ tags:
15
+ - skills
16
+ - marketplace
17
+ - install
18
+ - setup
19
+ - auto-install
20
+ execution:
21
+ type: script
22
+ script:
23
+ file: execute.sh
24
+ interpreter: bash
25
+ timeoutMs: 60000
26
+ ---
27
+
28
+ # Install Skill
29
+
30
+ Use after `find-skill`, or when a skill answered `"needsSetup": true, "skill": "<id>"`.
31
+
32
+ ```bash
33
+ bash {{AGENT_SKILLS_PATH}}/core/install-skill/execute.sh --id transcribe-audio --resume "transcribe the voice message Steve sent in #general"
34
+ ```
35
+
36
+ It returns immediately:
37
+
38
+ ```json
39
+ {"success":true,"jobId":"3f9c1a2b","skillId":"transcribe-audio","state":"running","official":true,
40
+ "estimatedMinutes":6,"willNotify":["crewly-dev-1"],
41
+ "next":"Installing transcribe-audio in the background (about 6 min). Tell the user now…"}
42
+ ```
43
+
44
+ or `{"success":true,"state":"already-ready","executePath":"…"}` — use the skill now.
45
+
46
+ ## The flow
47
+
48
+ 1. Tell the user in **one line** that you are installing it and roughly how long it
49
+ takes ("I can't read audio yet — installing the transcription skill, about 6 min.").
50
+ 2. Run `install-skill`. Pass `--resume` with what you were doing; it comes back to you.
51
+ 3. When `[SKILL INSTALLED] …` arrives, tell the user it's ready and **do the original
52
+ task right away**. On `[SKILL INSTALL FAILED] …`, tell the user plainly what is
53
+ missing and the fix the message gives (e.g. a command the owner must run).
54
+
55
+ Never answer "X is not installed" and stop.
56
+
57
+ ## Trust
58
+
59
+ - **Official** (bundled with Crewly, or published by the Crewly Team in the official
60
+ registry): installs without asking, including the system dependencies it declares.
61
+ - **Third-party**: refused with `reason: owner_approval_required`. Ask the owner in chat
62
+ (say what it is and who publishes it). Only after they say yes:
63
+ `install-skill --id <id> --approved-by-owner --owner-said "<their words>"`.
64
+ The backend checks the owner's recent chat for that yes; claiming it without one fails
65
+ (`owner_approval_not_found`).
66
+
67
+ Progress: `GET /api/skill-setup/jobs/<jobId>`. Setup log: `~/.crewly/logs/skill-setup/<id>.log`.
@@ -0,0 +1,80 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # install-skill — install a skill and its dependencies as a background job.
4
+ #
5
+ # Returns at once with a job id (or "already-ready"). When the job ends you get
6
+ # a message: [SKILL INSTALLED] … or [SKILL INSTALL FAILED] … with the reason.
7
+ # Tell the user in one line that you are installing it, then continue with
8
+ # other work or wait; resume the paused task when the message arrives.
9
+ # Backed by POST /api/skill-setup/install.
10
+ #
11
+ # Only official skills (bundled with Crewly, or published by the Crewly Team in
12
+ # the official registry) install without asking. A third-party skill needs the
13
+ # owner's yes in chat first; then pass --approved-by-owner (it is checked
14
+ # against the owner's chat history — claiming it without their yes fails).
15
+ #
16
+ # Usage:
17
+ # bash execute.sh --id transcribe-audio --resume "transcribe clip.m4a for Steve"
18
+ # bash execute.sh --id some-skill --approved-by-owner --owner-said "好的,装 some-skill"
19
+ # bash execute.sh '{"id":"pdf-tools","resume":"…"}'
20
+ # =============================================================================
21
+ set -euo pipefail
22
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+ source "${SCRIPT_DIR}/../../_common/lib.sh"
24
+
25
+ print_usage() {
26
+ cat <<'EOF_USAGE'
27
+ Usage:
28
+ bash execute.sh --id <skill-id> [--resume "what you were doing"] [--approved-by-owner --owner-said "…"] [--force]
29
+ bash execute.sh '{"id":"…","resume":"…","approvedByOwner":false}'
30
+
31
+ Options:
32
+ --id Skill id from find-skill (required)
33
+ --resume What you were doing; echoed back in the completion message
34
+ --approved-by-owner The owner said yes in chat (third-party skills only; verified)
35
+ --owner-said Quote the owner's yes (recorded next to the verified message)
36
+ --force Re-run setup even if everything looks installed
37
+ --help | -h Show this help
38
+ EOF_USAGE
39
+ }
40
+
41
+ INPUT_JSON=""; ID=""; RESUME=""; APPROVED=false; FORCE=false; OWNER_SAID=""
42
+ if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then INPUT_JSON="$1"; shift || true; fi
43
+ while [[ $# -gt 0 ]]; do
44
+ case "$1" in
45
+ --id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
46
+ --resume) [ $# -ge 2 ] || error_exit "--resume requires a value"; RESUME="$2"; shift 2 ;;
47
+ --approved-by-owner) APPROVED=true; shift ;;
48
+ --owner-said) [ $# -ge 2 ] || error_exit "--owner-said requires a value"; OWNER_SAID="$2"; shift 2 ;;
49
+ --force) FORCE=true; shift ;;
50
+ --help|-h) print_usage; exit 0 ;;
51
+ *) error_exit "Unknown option: $1" ;;
52
+ esac
53
+ done
54
+ if [ -n "$INPUT_JSON" ]; then
55
+ INPUT=$(read_json_input "$INPUT_JSON")
56
+ [ -z "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // empty')
57
+ [ -z "$RESUME" ] && RESUME=$(printf '%s' "$INPUT" | jq -r '.resume // empty')
58
+ [ "$(printf '%s' "$INPUT" | jq -r '.approvedByOwner // false')" = "true" ] && APPROVED=true
59
+ [ "$(printf '%s' "$INPUT" | jq -r '.force // false')" = "true" ] && FORCE=true
60
+ fi
61
+ require_param "id (--id)" "$ID"
62
+ # The quote travels as X-Agent-Authorization (lib.sh encodes it): recorded, never trusted.
63
+ [ -n "$OWNER_SAID" ] && export CREWLY_AGENT_AUTHORIZATION="$OWNER_SAID"
64
+
65
+ BODY=$(jq -cn --arg id "$ID" --arg resume "$RESUME" --argjson approved "$APPROVED" --argjson force "$FORCE" \
66
+ '{id: $id} + (if $resume != "" then {resume: $resume} else {} end)
67
+ + (if $approved then {approvedByOwner: true} else {} end) + (if $force then {force: true} else {} end)')
68
+
69
+ ERR_FILE=$(mktemp); trap 'rm -f "$ERR_FILE"' EXIT
70
+ RESPONSE=$(api_call POST "/skill-setup/install" "$BODY" 2>"$ERR_FILE") || {
71
+ ERR=$(tail -n 1 "$ERR_FILE")
72
+ printf '%s' "$ERR" | jq -c '{success: false,
73
+ reason: (.details.code // "install_failed"),
74
+ error: (.details.error // .error // "install failed")}
75
+ + (if (.details.code // "") | startswith("owner_approval") then {official: false, next: "Ask the owner in chat before installing this third-party skill."} else {} end)' 2>/dev/null \
76
+ || jq -cn --arg e "$ERR" '{success: false, reason: "install_failed", error: $e}'
77
+ exit 1
78
+ }
79
+
80
+ printf '%s' "$RESPONSE" | jq -c '.data | {success: true} + .'