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.
- package/README.md +38 -28
- package/bin/clew.cjs +2 -2
- package/dist/main.js +2318 -2331
- package/docs/architecture.html +90 -91
- package/docs/changelog.html +141 -150
- package/docs/cli-reference.html +89 -90
- package/docs/commands.html +10 -9
- package/docs/daemon.html +62 -62
- package/docs/generated/providers.html +625 -0
- package/docs/generated/tools.html +559 -0
- package/docs/index.html +10 -13
- package/docs/personal-profile.html +3 -3
- package/docs/providers.html +625 -87
- package/docs/quick-start.html +81 -81
- package/docs/tools.html +551 -175
- package/docs/troubleshooting.html +86 -86
- package/package.json +165 -165
- package/scripts/auto-close-duplicates.ts +277 -0
- package/scripts/backfill-duplicate-comments.ts +213 -0
- package/scripts/codegraph.ts +221 -0
- package/scripts/comment-on-duplicates.sh +95 -0
- package/scripts/edit-issue-labels.sh +84 -0
- package/scripts/final-peer-rename.mjs +46 -0
- package/scripts/fix-encoding.mjs +83 -0
- package/scripts/fix-inconsistencies.mjs +67 -0
- package/scripts/generate-docs.ts +307 -0
- package/scripts/gh.sh +96 -0
- package/scripts/install.ps1 +37 -0
- package/scripts/install.sh +49 -0
- package/scripts/issue-lifecycle.ts +38 -0
- package/scripts/lifecycle-comment.ts +53 -0
- package/scripts/normalize-html.mjs +37 -0
- package/scripts/preload.ts +159 -0
- package/scripts/rename-content-peer-to-swarm.mjs +67 -0
- package/scripts/rename-hooks-mesh.mjs +6 -0
- package/scripts/rename-peer-to-swarm.mjs +62 -0
- package/scripts/run_devcontainer_claude_code.ps1 +152 -0
- package/scripts/scrape.py +82 -0
- package/scripts/session.ts +195 -0
- package/scripts/sweep.ts +168 -0
- package/scripts/write-discovery-test.cjs +93 -0
- package/scripts/write-discovery-test2.cjs +65 -0
- package/scripts/write-server-final.cjs +108 -0
- package/scripts/write-server-test.cjs +69 -0
- package/scripts/write-server-test2.cjs +99 -0
- package/scripts/write-server-test3.cjs +59 -0
- package/scripts/write-server-test4.cjs +108 -0
- package/scripts/write-server-v2.cjs +142 -0
- package/scripts/write-server-v2b.cjs +129 -0
- package/scripts/write-server-v2c.cjs +136 -0
- package/scripts/write-store-test.cjs +12 -0
- 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, '&')
|
|
295
|
+
.replace(/</g, '<')
|
|
296
|
+
.replace(/>/g, '>')
|
|
297
|
+
.replace(/"/g, '"');
|
|
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/');
|