clew-code 0.2.28 → 0.2.30

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 (52) hide show
  1. package/README.md +38 -28
  2. package/bin/clew.cjs +2 -2
  3. package/dist/main.js +2318 -2331
  4. package/docs/architecture.html +90 -91
  5. package/docs/changelog.html +141 -150
  6. package/docs/cli-reference.html +89 -90
  7. package/docs/commands.html +10 -9
  8. package/docs/daemon.html +62 -62
  9. package/docs/generated/providers.html +625 -0
  10. package/docs/generated/tools.html +559 -0
  11. package/docs/index.html +10 -13
  12. package/docs/personal-profile.html +3 -3
  13. package/docs/providers.html +625 -87
  14. package/docs/quick-start.html +81 -81
  15. package/docs/tools.html +551 -175
  16. package/docs/troubleshooting.html +86 -86
  17. package/package.json +165 -165
  18. package/scripts/auto-close-duplicates.ts +277 -0
  19. package/scripts/backfill-duplicate-comments.ts +213 -0
  20. package/scripts/codegraph.ts +221 -0
  21. package/scripts/comment-on-duplicates.sh +95 -0
  22. package/scripts/edit-issue-labels.sh +84 -0
  23. package/scripts/final-peer-rename.mjs +46 -0
  24. package/scripts/fix-encoding.mjs +83 -0
  25. package/scripts/fix-inconsistencies.mjs +67 -0
  26. package/scripts/generate-docs.ts +307 -0
  27. package/scripts/gh.sh +96 -0
  28. package/scripts/install.ps1 +37 -0
  29. package/scripts/install.sh +49 -0
  30. package/scripts/issue-lifecycle.ts +38 -0
  31. package/scripts/lifecycle-comment.ts +53 -0
  32. package/scripts/normalize-html.mjs +37 -0
  33. package/scripts/preload.ts +159 -0
  34. package/scripts/rename-content-peer-to-swarm.mjs +67 -0
  35. package/scripts/rename-hooks-mesh.mjs +6 -0
  36. package/scripts/rename-peer-to-swarm.mjs +62 -0
  37. package/scripts/run_devcontainer_claude_code.ps1 +152 -0
  38. package/scripts/scrape.py +82 -0
  39. package/scripts/session.ts +195 -0
  40. package/scripts/sweep.ts +168 -0
  41. package/scripts/write-discovery-test.cjs +93 -0
  42. package/scripts/write-discovery-test2.cjs +65 -0
  43. package/scripts/write-server-final.cjs +108 -0
  44. package/scripts/write-server-test.cjs +69 -0
  45. package/scripts/write-server-test2.cjs +99 -0
  46. package/scripts/write-server-test3.cjs +59 -0
  47. package/scripts/write-server-test4.cjs +108 -0
  48. package/scripts/write-server-v2.cjs +142 -0
  49. package/scripts/write-server-v2b.cjs +129 -0
  50. package/scripts/write-server-v2c.cjs +136 -0
  51. package/scripts/write-store-test.cjs +12 -0
  52. package/scripts/write-store-test2.cjs +163 -0
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Comments on a GitHub issue with a list of potential duplicates.
4
+ # Usage: ./comment-on-duplicates.sh --potential-duplicates 456 789 101
5
+ #
6
+ # The base issue number is read from the workflow event payload.
7
+ #
8
+
9
+ set -euo pipefail
10
+
11
+ REPO="JonusNattapong/ClaudeCode"
12
+
13
+ # Read from event payload so the issue number is bound to the triggering event.
14
+ # Falls back to workflow_dispatch inputs for manual runs.
15
+ BASE_ISSUE=$(jq -r '.issue.number // .inputs.issue_number // empty' "${GITHUB_EVENT_PATH:?GITHUB_EVENT_PATH not set}")
16
+ if ! [[ "$BASE_ISSUE" =~ ^[0-9]+$ ]]; then
17
+ echo "Error: no issue number in event payload" >&2
18
+ exit 1
19
+ fi
20
+
21
+ DUPLICATES=()
22
+
23
+ # Parse arguments
24
+ while [[ $# -gt 0 ]]; do
25
+ case $1 in
26
+ --potential-duplicates)
27
+ shift
28
+ while [[ $# -gt 0 && ! "$1" =~ ^-- ]]; do
29
+ DUPLICATES+=("$1")
30
+ shift
31
+ done
32
+ ;;
33
+ *)
34
+ echo "Error: unknown argument (only --potential-duplicates is accepted)" >&2
35
+ exit 1
36
+ ;;
37
+ esac
38
+ done
39
+
40
+ # Validate duplicates
41
+ if [[ ${#DUPLICATES[@]} -eq 0 ]]; then
42
+ echo "Error: --potential-duplicates requires at least one issue number" >&2
43
+ exit 1
44
+ fi
45
+
46
+ if [[ ${#DUPLICATES[@]} -gt 3 ]]; then
47
+ echo "Error: --potential-duplicates accepts at most 3 issues" >&2
48
+ exit 1
49
+ fi
50
+
51
+ for dup in "${DUPLICATES[@]}"; do
52
+ if ! [[ "$dup" =~ ^[0-9]+$ ]]; then
53
+ echo "Error: duplicate issue must be a number, got: $dup" >&2
54
+ exit 1
55
+ fi
56
+ done
57
+
58
+ # Validate that base issue exists
59
+ if ! gh issue view "$BASE_ISSUE" --repo "$REPO" &>/dev/null; then
60
+ echo "Error: issue #$BASE_ISSUE does not exist in $REPO" >&2
61
+ exit 1
62
+ fi
63
+
64
+ # Validate that all duplicate issues exist
65
+ for dup in "${DUPLICATES[@]}"; do
66
+ if ! gh issue view "$dup" --repo "$REPO" &>/dev/null; then
67
+ echo "Error: issue #$dup does not exist in $REPO" >&2
68
+ exit 1
69
+ fi
70
+ done
71
+
72
+ # Build comment body
73
+ COUNT=${#DUPLICATES[@]}
74
+ if [[ $COUNT -eq 1 ]]; then
75
+ HEADER="Found 1 possible duplicate issue:"
76
+ else
77
+ HEADER="Found $COUNT possible duplicate issues:"
78
+ fi
79
+
80
+ BODY="$HEADER"$'\n\n'
81
+ INDEX=1
82
+ for dup in "${DUPLICATES[@]}"; do
83
+ BODY+="$INDEX. https://github.com/$REPO/issues/$dup"$'\n'
84
+ ((INDEX++))
85
+ done
86
+
87
+ BODY+=$'\n'"This issue will be automatically closed as a duplicate in 3 days."$'\n\n'
88
+ BODY+="- If your issue is a duplicate, please close it and 👍 the existing issue instead"$'\n'
89
+ BODY+="- To prevent auto-closure, add a comment or 👎 this comment"$'\n\n'
90
+ BODY+="🤖 Generated with [Claude Code](https://claude.ai/code)"
91
+
92
+ # Post the comment
93
+ gh issue comment "$BASE_ISSUE" --repo "$REPO" --body "$BODY"
94
+
95
+ echo "Posted duplicate comment on issue #$BASE_ISSUE"
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Edits labels on a GitHub issue.
4
+ # Usage: ./edit-issue-labels.sh --add-label bug --add-label needs-triage --remove-label untriaged
5
+ #
6
+ # The issue number is read from the workflow event payload.
7
+ #
8
+
9
+ set -euo pipefail
10
+
11
+ # Read from event payload so the issue number is bound to the triggering event.
12
+ # Falls back to workflow_dispatch inputs for manual runs.
13
+ ISSUE=$(jq -r '.issue.number // .inputs.issue_number // empty' "${GITHUB_EVENT_PATH:?GITHUB_EVENT_PATH not set}")
14
+ if ! [[ "$ISSUE" =~ ^[0-9]+$ ]]; then
15
+ echo "Error: no issue number in event payload" >&2
16
+ exit 1
17
+ fi
18
+
19
+ ADD_LABELS=()
20
+ REMOVE_LABELS=()
21
+
22
+ # Parse arguments
23
+ while [[ $# -gt 0 ]]; do
24
+ case $1 in
25
+ --add-label)
26
+ ADD_LABELS+=("$2")
27
+ shift 2
28
+ ;;
29
+ --remove-label)
30
+ REMOVE_LABELS+=("$2")
31
+ shift 2
32
+ ;;
33
+ *)
34
+ echo "Error: unknown argument (only --add-label and --remove-label are accepted)" >&2
35
+ exit 1
36
+ ;;
37
+ esac
38
+ done
39
+
40
+ if [[ ${#ADD_LABELS[@]} -eq 0 && ${#REMOVE_LABELS[@]} -eq 0 ]]; then
41
+ exit 1
42
+ fi
43
+
44
+ # Fetch valid labels from the repo
45
+ VALID_LABELS=$(gh label list --limit 500 --json name --jq '.[].name')
46
+
47
+ # Filter to only labels that exist in the repo
48
+ FILTERED_ADD=()
49
+ for label in "${ADD_LABELS[@]}"; do
50
+ if echo "$VALID_LABELS" | grep -qxF "$label"; then
51
+ FILTERED_ADD+=("$label")
52
+ fi
53
+ done
54
+
55
+ FILTERED_REMOVE=()
56
+ for label in "${REMOVE_LABELS[@]}"; do
57
+ if echo "$VALID_LABELS" | grep -qxF "$label"; then
58
+ FILTERED_REMOVE+=("$label")
59
+ fi
60
+ done
61
+
62
+ if [[ ${#FILTERED_ADD[@]} -eq 0 && ${#FILTERED_REMOVE[@]} -eq 0 ]]; then
63
+ exit 0
64
+ fi
65
+
66
+ # Build gh command arguments
67
+ GH_ARGS=("issue" "edit" "$ISSUE")
68
+
69
+ for label in "${FILTERED_ADD[@]}"; do
70
+ GH_ARGS+=("--add-label" "$label")
71
+ done
72
+
73
+ for label in "${FILTERED_REMOVE[@]}"; do
74
+ GH_ARGS+=("--remove-label" "$label")
75
+ done
76
+
77
+ gh "${GH_ARGS[@]}"
78
+
79
+ if [[ ${#FILTERED_ADD[@]} -gt 0 ]]; then
80
+ echo "Added: ${FILTERED_ADD[*]}"
81
+ fi
82
+ if [[ ${#FILTERED_REMOVE[@]} -gt 0 ]]; then
83
+ echo "Removed: ${FILTERED_REMOVE[*]}"
84
+ fi
@@ -0,0 +1,46 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function walk(dir) {
5
+ const r = [];
6
+ for (const e of fs.readdirSync(dir, {withFileTypes:true})) {
7
+ const f = path.join(dir, e.name);
8
+ if (e.isDirectory()) {
9
+ if (!e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'dist') r.push(...walk(f));
10
+ } else if (e.name.endsWith('.ts') || e.name.endsWith('.tsx')) r.push(f);
11
+ }
12
+ return r;
13
+ }
14
+
15
+ const files = walk('src');
16
+ let count = 0;
17
+ for (const f of files) {
18
+ let c = fs.readFileSync(f, 'utf-8');
19
+ const orig = c;
20
+
21
+ c = c.replace(/\/peer\//g, '/swarm/');
22
+ c = c.replace(/\bPeer(Broadcast|Disconnect|Discover|Join|ListMessages|ListRoles|Ping|Run|SendMessage|SetName|SetRole|Share|Spawn|Info|Help|Server|Store|Discovery|StatusLine|Type)Tool\b/g, 'Swarm$1Tool');
23
+ c = c.replace(/\bPeer(Broadcast|Disconnect|Discover|Join|ListMessages|ListRoles|Ping|Run|SendMessage|SetName|SetRole|Share|Spawn|Info|Help|Server|Store|Discovery|StatusLine|Type)\b/g, 'Swarm$1');
24
+ c = c.replace(/\bProcessPeer(Provider|Tool)/g, 'ProcessSwarm$1');
25
+ c = c.replace(/\bCodexPeer/g, 'CodexSwarm');
26
+ c = c.replace(/\bgetProcessPeerProvider\b/g, 'getProcessSwarmProvider');
27
+ c = c.replace(/\bgetProcessPeerProviderIds\b/g, 'getProcessSwarmProviderIds');
28
+ c = c.replace(/\bmyPeerId\b/g, 'mySwarmId');
29
+ c = c.replace(/\bpeerId\b/g, 'swarmId');
30
+ c = c.replace(/\bPEER_(\w+)_TOOL_NAME\b/g, 'SWARM_$1_TOOL_NAME');
31
+ c = c.replace(/\bpeer(Server|Store|Discovery|Port|Name|Info|List|Count|Connection|Sharing|Health)\b/g, 'swarm$1');
32
+ c = c.replace(/\bgetGlobalPeer(Server|Store)\b/g, 'getGlobalSwarm$1');
33
+ c = c.replace(/\bnotifyPeerFeedback\b/g, 'notifySwarmFeedback');
34
+ c = c.replace(/\bsetPeerFeedbackHandler\b/g, 'setSwarmFeedbackHandler');
35
+ c = c.replace(/\bPeerFeedback\b/g, 'SwarmFeedback');
36
+ c = c.replace(/\bpeer-feedback/g, 'swarm-feedback');
37
+ c = c.replace(/\bpeerFeedback/g, 'swarmFeedback');
38
+ c = c.replace(/\/peer\b/g, '/swarm');
39
+ c = c.replace(/['"]peer-/g, (m) => m.replace('peer-', 'swarm-'));
40
+ c = c.replace(/\bspawnPeerTerminal\b/g, 'spawnSwarmTerminal');
41
+ c = c.replace(/\bparseProcessPeerRunArgs\b/g, 'parseProcessSwarmRunArgs');
42
+ c = c.replace(/\bProcessPeerRunArgs\b/g, 'ProcessSwarmRunArgs');
43
+
44
+ if (c !== orig) { fs.writeFileSync(f, c, 'utf-8'); count++; }
45
+ }
46
+ console.log('Updated:', count, 'files');
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Fix mojibake encoding in all docs HTML files.
3
+ *
4
+ * Problem: Files were double-encoded — original UTF-8 bytes were
5
+ * misinterpreted as CP1252 then re-encoded as UTF-8, producing
6
+ * characters like — instead of — and ค instead of ค.
7
+ *
8
+ * This script reverses the damage by mapping each Unicode char
9
+ * back to its CP1252 byte and re-decoding as UTF-8.
10
+ * Legitimate Unicode characters (Thai, arrows, emoji) pass through.
11
+ */
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+
15
+ const cp1252 = new Map([
16
+ [0x20ac, 0x80], [0x201a, 0x82], [0x0192, 0x83], [0x201e, 0x84],
17
+ [0x2026, 0x85], [0x2020, 0x86], [0x2021, 0x87], [0x02c6, 0x88],
18
+ [0x2030, 0x89], [0x0160, 0x8a], [0x2039, 0x8b], [0x0152, 0x8c],
19
+ [0x017d, 0x8e], [0x2018, 0x91], [0x2019, 0x92], [0x201c, 0x93],
20
+ [0x201d, 0x94], [0x2022, 0x95], [0x2013, 0x96], [0x2014, 0x97],
21
+ [0x02dc, 0x98], [0x2122, 0x99], [0x0161, 0x9a], [0x203a, 0x9b],
22
+ [0x0153, 0x9c], [0x017e, 0x9e], [0x0178, 0x9f],
23
+ ]);
24
+
25
+ function fixMojibake(text) {
26
+ if (text.charCodeAt(0) === 0xfeff) text = text.slice(1); // strip BOM
27
+
28
+ const bytes = [];
29
+ for (let i = 0; i < text.length; i++) {
30
+ const code = text.charCodeAt(i);
31
+ if (code <= 0xff) {
32
+ bytes.push(code);
33
+ } else if (cp1252.has(code)) {
34
+ bytes.push(cp1252.get(code));
35
+ } else {
36
+ const buf = Buffer.from(text[i], 'utf-8');
37
+ for (const b of buf) bytes.push(b);
38
+ }
39
+ }
40
+ return Buffer.from(bytes).toString('utf-8');
41
+ }
42
+
43
+ // Walk docs/ recursively
44
+ function walk(dir) {
45
+ const results = [];
46
+ const list = fs.readdirSync(dir);
47
+ for (const entry of list) {
48
+ const full = path.join(dir, entry);
49
+ const stat = fs.statSync(full);
50
+ if (stat.isDirectory()) {
51
+ results.push(...walk(full));
52
+ } else if (entry.endsWith('.html')) {
53
+ results.push(full);
54
+ }
55
+ }
56
+ return results;
57
+ }
58
+
59
+ const files = walk('docs');
60
+ let changed = 0;
61
+ let errors = 0;
62
+
63
+ for (const f of files) {
64
+ try {
65
+ const content = fs.readFileSync(f, 'utf-8');
66
+ const fixed = fixMojibake(content);
67
+
68
+ // skip if no change
69
+ if (fixed === content) {
70
+ console.log(` ✓ ${f} — already clean`);
71
+ continue;
72
+ }
73
+
74
+ fs.writeFileSync(f, fixed, 'utf-8');
75
+ console.log(` ✔ ${f}`);
76
+ changed++;
77
+ } catch (err) {
78
+ console.error(` ✘ ${f}: ${err.message}`);
79
+ errors++;
80
+ }
81
+ }
82
+
83
+ console.log(`\nDone: ${changed} files fixed, ${errors} errors`);
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Normalize all HTML files to consistent structure:
3
+ * - Empty header (JS-injected)
4
+ * - main.js script at end of body
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const docsDir = path.resolve(__dirname, '../docs');
12
+
13
+ function walk(dir) {
14
+ const results = [];
15
+ const list = fs.readdirSync(dir);
16
+ for (const entry of list) {
17
+ const full = path.join(dir, entry);
18
+ const stat = fs.statSync(full);
19
+ if (stat.isDirectory()) results.push(...walk(full));
20
+ else if (entry.endsWith('.html')) results.push(full);
21
+ }
22
+ return results;
23
+ }
24
+
25
+ function relativePath(from, to) {
26
+ const rel = path.relative(path.dirname(from), to);
27
+ return rel.startsWith('.') ? rel : './' + rel;
28
+ }
29
+
30
+ const files = walk(docsDir);
31
+ let fixed = 0;
32
+
33
+ for (const filePath of files) {
34
+ let c = fs.readFileSync(filePath, 'utf-8');
35
+ const name = path.basename(filePath);
36
+
37
+ // Skip landing page (different structure by design)
38
+ if (name === 'index.html' || name === 'index.th.html') continue;
39
+
40
+ // Fix 1: Replace inline header with empty header
41
+ if (!c.includes('<header class="header"></header>')) {
42
+ c = c.replace(/<header class="header">[\s\S]*?<\/header>\s*/,
43
+ '<header class="header"></header>\n');
44
+ }
45
+
46
+ // Fix 2: Add main.js if missing
47
+ if (!c.includes('main.js')) {
48
+ const jsRelPath = relativePath(filePath, path.join(docsDir, 'js/main.js'));
49
+ c = c.replace('</body>', `<script src="${jsRelPath}"></script>\n</body>`);
50
+ }
51
+
52
+ // Fix 3: Ensure Google Fonts includes Noto Sans Thai for .th.html files
53
+ if (name.endsWith('.th.html') && !c.includes('Noto+Sans+Thai')) {
54
+ c = c.replace(
55
+ /family=JetBrains\+Mono:[^"'&]+/,
56
+ '$&, &family=Noto+Sans+Thai:wght@400;500;600;700'
57
+ );
58
+ }
59
+
60
+ // Fix 4: Clean up old lang-wrap styles in features/internals
61
+ c = c.replace(/<div class="lang-wrap">[\s\S]*?<\/div>\s*<\/div>\s*/, '');
62
+
63
+ fs.writeFileSync(filePath, c, 'utf-8');
64
+ fixed++;
65
+ }
66
+
67
+ console.log(`Normalized ${fixed} files`);
@@ -0,0 +1,307 @@
1
+ /**
2
+ * generate-docs.ts — Auto-generate HTML documentation from source code.
3
+ *
4
+ * Run: bun run scripts/generate-docs.ts
5
+ * Reads source data and writes to docs/generated/
6
+ *
7
+ * Currently generates:
8
+ * - providers.json → docs/generated/providers.html (provider reference table)
9
+ * - tool prompts → docs/generated/tools.html (tool reference with prompts)
10
+ */
11
+
12
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
13
+ import { join, dirname } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const ROOT = join(__dirname, '..');
18
+ const SRC = join(ROOT, 'src');
19
+ const DOCS = join(ROOT, 'docs');
20
+ const OUT = join(DOCS, 'generated');
21
+
22
+ if (!existsSync(OUT)) mkdirSync(OUT, { recursive: true });
23
+
24
+ // ── Helpers ──
25
+
26
+ function html( Title: string, body: string, description = ''): string {
27
+ return `<!DOCTYPE html>
28
+ <html lang="en">
29
+ <head>
30
+ <meta charset="UTF-8">
31
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
32
+ <title>${Title} — Clew</title>
33
+ <meta name="description" content="${description}">
34
+ <link rel="preconnect" href="https://fonts.googleapis.com">
35
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
36
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
37
+ <link rel="stylesheet" href="../css/styles.css">
38
+ <link rel="icon" type="image/svg+xml" href="../assets/clew.svg">
39
+ </head>
40
+ <body>
41
+ <header class="header">
42
+ <div class="header-inner">
43
+ <a href="../index.html" class="logo"><span class="logo-mark">C</span><span>Clew Code</span></a>
44
+ <nav class="header-nav">
45
+ <a href="../index.html">Home</a>
46
+ <a href="../commands.html">Commands</a>
47
+ <a href="../tools.html">Tools</a>
48
+ <a href="../providers.html">Providers</a>
49
+ <a href="../prompts-and-features.html">Reference</a>
50
+ <a href="../quick-start.html">Docs</a>
51
+ </nav>
52
+ </div>
53
+ </header>
54
+ <div class="app">
55
+ <aside class="sidebar" id="sidebar">
56
+ <div class="sidebar-section">
57
+ <div class="sidebar-label">Generated Docs</div>
58
+ <a href="providers.html" class="sidebar-link"><span class="link-icon"></span>Providers</a>
59
+ <a href="tools.html" class="sidebar-link"><span class="link-icon"></span>Tools</a>
60
+ </div>
61
+ <div class="sidebar-section">
62
+ <div class="sidebar-label">Static Docs</div>
63
+ <a href="../commands.html" class="sidebar-link"><span class="link-icon"></span>Commands</a>
64
+ <a href="../tools.html" class="sidebar-link"><span class="link-icon"></span>Tools</a>
65
+ <a href="../providers.html" class="sidebar-link"><span class="link-icon"></span>Providers</a>
66
+ </div>
67
+ </aside>
68
+ <div class="sidebar-overlay"></div>
69
+ <div class="content-wrap"><main class="content">
70
+ <div class="breadcrumbs"><a href="../index.html">Home</a><span class="sep">/</span><span>${Title}</span></div>
71
+ <h1>${Title}</h1>
72
+ <p class="section-subtitle"><em>Auto-generated from source code. Last updated: ${new Date().toISOString().slice(0, 10)}.</em></p>
73
+ ${body}
74
+ </main></div>
75
+ </div>
76
+ </body>
77
+ </html>`;
78
+ }
79
+
80
+ function cap(s: string): string {
81
+ return s.charAt(0).toUpperCase() + s.slice(1);
82
+ }
83
+
84
+ // ═══════════════════════════════════════════════════════════════════════
85
+ // 1. GENERATE PROVIDERS
86
+ // ═══════════════════════════════════════════════════════════════════════
87
+
88
+ function generateProviders(): void {
89
+ const providers = JSON.parse(
90
+ readFileSync(join(SRC, 'services/ai/providers.json'), 'utf8'),
91
+ ) as Record<string, any>;
92
+
93
+ const CAP_KEYS: Record<string, string> = {
94
+ chat: 'Chat',
95
+ streaming: 'Streaming',
96
+ toolCalling: 'Tools',
97
+ vision: 'Vision',
98
+ jsonSchema: 'JSON Schema',
99
+ reasoningEffort: 'Reasoning',
100
+ };
101
+
102
+ let rows = '';
103
+ let detailSections = '';
104
+
105
+ for (const [id, p] of Object.entries(providers)) {
106
+ const caps = Object.entries(p.capabilities ?? {})
107
+ .filter(([, v]) => v === true || v === 'full' || v === 'native')
108
+ .map(([k]) => CAP_KEYS[k] ?? cap(k))
109
+ .join(', ') || '—';
110
+
111
+ const models = (p.models ?? []).map((m: any) => m.id).join(', ') || '—';
112
+
113
+ rows += `<tr>
114
+ <td><strong>${p.label}</strong></td>
115
+ <td><code>${p.envKey}</code></td>
116
+ <td>${p.defaultModel || '—'}</td>
117
+ <td style="font-size:0.75rem">${caps}</td>
118
+ </tr>`;
119
+
120
+ detailSections += `<details>
121
+ <summary><strong>${p.label}</strong> <code>${id}</code></summary>
122
+ <p>${p.note || 'No description.'}</p>
123
+ <table>
124
+ <tr><th>Property</th><th>Value</th></tr>
125
+ <tr><td>Provider ID</td><td><code>${id}</code></td></tr>
126
+ <tr><td>Env Key</td><td><code>${p.envKey}</code></td></tr>
127
+ <tr><td>Default Model</td><td>${p.defaultModel || '—'}</td></tr>
128
+ <tr><td>Base URL</td><td><code>${p.defaultBaseUrl}</code></td></tr>
129
+ <tr><td>Models URL</td><td>${p.modelsUrl ? `<code>${p.modelsUrl}</code>` : '—'}</td></tr>
130
+ <tr><td>Local</td><td>${p.isLocal ? '✅' : '—'}</td></tr>
131
+ <tr><td>Capabilities</td><td>${caps || '—'}</td></tr>
132
+ </table>
133
+ <p><strong>Models (${(p.models ?? []).length}):</strong> ${models}</p>
134
+ </details>`;
135
+ }
136
+
137
+ const body = `
138
+ <h2>All Providers</h2>
139
+ <p>Clew supports <strong>${Object.keys(providers).length} AI providers</strong>. Switch between them at runtime with <code>/model</code> or <code>/provider-select</code>.</p>
140
+ <div class="table-wrap"><table>
141
+ <tr><th>Provider</th><th>Env Key</th><th>Default Model</th><th>Capabilities</th></tr>
142
+ ${rows}
143
+ </table></div>
144
+
145
+ <h2>Provider Details</h2>
146
+ <div class="provider-group">${detailSections}</div>
147
+
148
+ <style>
149
+ .provider-group details {
150
+ background: var(--bg-card);
151
+ border: 1px solid var(--border-subtle);
152
+ border-radius: 8px;
153
+ padding: 0.75rem 1rem;
154
+ margin: 0.4rem 0;
155
+ }
156
+ .provider-group summary { cursor: pointer; font-weight: 600; font-size: var(--text-sm); }
157
+ .provider-group details p { margin: 0.75rem 0; font-size: var(--text-sm); color: var(--text-secondary); }
158
+ .provider-group details table { margin: 0.5rem 0; }
159
+ </style>
160
+ `;
161
+
162
+ writeFileSync(join(OUT, 'providers.html'), html(
163
+ 'Providers (Auto-generated)',
164
+ body,
165
+ `Auto-generated provider reference for ${Object.keys(providers).length} AI providers in Clew Code.`,
166
+ ));
167
+ console.log(`✅ Generated providers.html (${Object.keys(providers).length} providers)`);
168
+ }
169
+
170
+ // ═══════════════════════════════════════════════════════════════════════
171
+ // 2. GENERATE TOOLS
172
+ // ═══════════════════════════════════════════════════════════════════════
173
+
174
+ function generateTools(): void {
175
+ const TOOLS_DIR = join(SRC, 'tools');
176
+ const entries = existsSync(TOOLS_DIR) ? readFileSystem(TOOLS_DIR, 1) : [];
177
+
178
+ const toolEntries: Array<{ name: string; prompt: string; description: string }> = [];
179
+
180
+ for (const dir of entries) {
181
+ const promptPath = join(TOOLS_DIR, dir, 'prompt.ts');
182
+ if (!existsSync(promptPath)) continue;
183
+
184
+ const content = readFileSync(promptPath, 'utf8');
185
+
186
+ // Extract DESCRIPTION export
187
+ const descMatch = content.match(/export\s+const\s+DESCRIPTION\s*=\s*([`'"])(.+?)\1/s);
188
+ const description = descMatch ? descMatch[2] : '';
189
+
190
+ // Extract PROMPT export (multiline template literals)
191
+ let prompt = '';
192
+ const promptMatch = content.match(/export\s+const\s+PROMPT\s*=\s*`([\s\S]*?)`/);
193
+ if (promptMatch) {
194
+ prompt = promptMatch[1].trim();
195
+ }
196
+
197
+ // If no template literal, try string literal
198
+ if (!prompt) {
199
+ const strMatch = content.match(/export\s+const\s+PROMPT\s*=\s*['"](.+?)['"]/s);
200
+ if (strMatch) prompt = strMatch[1].trim();
201
+ }
202
+
203
+ toolEntries.push({
204
+ name: dir.replace(/Tool$/, ''),
205
+ prompt,
206
+ description: description || prompt.slice(0, 120),
207
+ });
208
+ }
209
+
210
+ toolEntries.sort((a, b) => a.name.localeCompare(b.name));
211
+
212
+ let rows = '';
213
+ let promptSections = '';
214
+
215
+ for (const t of toolEntries) {
216
+ const desc = t.description || `Built-in tool "${t.name}". See system prompt for details.`;
217
+ rows += `<tr><td><code>${t.name}</code></td><td>${escapeHtml(desc)}</td></tr>\n`;
218
+
219
+ if (t.prompt) {
220
+ promptSections += `<details>
221
+ <summary><code>${t.name}</code> — System Prompt</summary>
222
+ <div class="prompt-box">${escapeHtml(t.prompt)}</div>
223
+ </details>\n`;
224
+ }
225
+ }
226
+
227
+ const body = `
228
+ <h2>All Tools with Prompts</h2>
229
+ <p>Clew provides <strong>${toolEntries.length} tools</strong> with system prompts. These prompts are injected into the AI model's context at session start to define the tool interface.</p>
230
+ <div class="table-wrap"><table>
231
+ <tr><th>Tool</th><th>Description</th></tr>
232
+ ${rows}
233
+ </table></div>
234
+
235
+ <h2>Tool Prompts</h2>
236
+ <p>Each tool's system prompt, as seen by the AI model:</p>
237
+ <div class="tool-prompts">${promptSections}</div>
238
+
239
+ <style>
240
+ .tool-prompts details {
241
+ background: var(--bg-card);
242
+ border: 1px solid var(--border-subtle);
243
+ border-radius: 8px;
244
+ margin: 0.5rem 0;
245
+ padding: 0.75rem 1rem;
246
+ }
247
+ .tool-prompts summary { cursor: pointer; font-weight: 600; font-size: var(--text-sm); }
248
+ .prompt-box {
249
+ background: var(--code-bg);
250
+ border: 1px solid var(--code-border);
251
+ border-radius: 6px;
252
+ padding: 1rem;
253
+ margin: 0.75rem 0;
254
+ font-family: var(--font-mono);
255
+ font-size: var(--text-xs);
256
+ line-height: 1.6;
257
+ color: var(--code-text);
258
+ white-space: pre-wrap;
259
+ overflow-x: auto;
260
+ }
261
+ </style>
262
+ `;
263
+
264
+ writeFileSync(join(OUT, 'tools.html'), html(
265
+ 'Tools & Prompts (Auto-generated)',
266
+ body,
267
+ `Auto-generated tool reference with ${toolEntries.length} system prompts for Clew Code.`,
268
+ ));
269
+ console.log(`✅ Generated tools.html (${toolEntries.length} tools with prompts)`);
270
+ }
271
+
272
+ // ═══════════════════════════════════════════════════════════════════════
273
+ // UTILITIES
274
+ // ═══════════════════════════════════════════════════════════════════════
275
+
276
+ function readFileSystem(dir: string, depth: number): string[] {
277
+ const results: string[] = [];
278
+ const entries = existsSync(dir) ? readdirSync(dir) : [];
279
+ for (const entry of entries) {
280
+ const fullPath = join(dir, entry);
281
+ if (statSync(fullPath).isDirectory()) {
282
+ if (depth > 0) {
283
+ results.push(entry);
284
+ }
285
+ }
286
+ }
287
+ return results;
288
+ }
289
+
290
+ import { readdirSync, statSync } from 'node:fs';
291
+
292
+ function escapeHtml(s: string): string {
293
+ return s
294
+ .replace(/&/g, '&amp;')
295
+ .replace(/</g, '&lt;')
296
+ .replace(/>/g, '&gt;')
297
+ .replace(/"/g, '&quot;');
298
+ }
299
+
300
+ // ═══════════════════════════════════════════════════════════════════════
301
+ // MAIN
302
+ // ═══════════════════════════════════════════════════════════════════════
303
+
304
+ console.log('📄 Generating docs from source...\n');
305
+ generateProviders();
306
+ generateTools();
307
+ console.log('\n✨ Done — outputs in docs/generated/');