@bolloon/bolloon-agent 0.1.34 → 0.1.35
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/.auto-evolve-calls +1 -0
- package/.last-auto-evolve-baseline +1 -0
- package/Bolloon.md +103 -0
- package/dist/agents/pi-sdk.js +264 -12
- package/dist/bootstrap/bootstrap.js +114 -0
- package/dist/bootstrap/context-collector.js +296 -0
- package/dist/bootstrap/lifecycle-hooks.js +109 -0
- package/dist/bootstrap/project-context.js +151 -0
- package/dist/index.js +11 -0
- package/dist/llm/pi-ai.js +31 -21
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +231 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
- package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
- package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
- package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
- package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
- package/dist/pi-ecosystem-judgment/injection-gate.js +166 -0
- package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
- package/dist/security/builtin-guards.js +124 -0
- package/dist/security/context-router-tool.js +106 -0
- package/dist/security/react-harness.js +143 -0
- package/dist/security/tool-gate.js +235 -0
- package/dist/utils/auto-evolve-policy.js +117 -0
- package/dist/utils/clamp.js +7 -0
- package/dist/utils/double.js +6 -0
- package/dist/web/client.js +668 -204
- package/dist/web/index.html +24 -4
- package/dist/web/server.js +531 -10
- package/lefthook.yml +29 -0
- package/package.json +3 -2
- package/scripts/auto-evolve-loop.ts +376 -0
- package/scripts/auto-evolve-oneshot.sh +155 -0
- package/scripts/auto-evolve-snapshot.sh +136 -0
- package/scripts/detect-schema-changes.sh +48 -0
- package/scripts/diff-reviewer.ts +159 -0
- package/scripts/weekly-report.ts +364 -0
- package/src/agents/pi-sdk.ts +293 -15
- package/src/bootstrap/bootstrap.ts +132 -0
- package/src/bootstrap/context-collector.ts +342 -0
- package/src/bootstrap/lifecycle-hooks.ts +176 -0
- package/src/bootstrap/project-context.ts +163 -0
- package/src/index.ts +11 -0
- package/src/llm/pi-ai.ts +33 -22
- package/src/security/builtin-guards.ts +162 -0
- package/src/security/context-router-tool.ts +122 -0
- package/src/security/react-harness.ts +177 -0
- package/src/security/tool-gate.ts +294 -0
- package/src/utils/auto-evolve-policy.ts +138 -0
- package/src/utils/clamp.ts +5 -0
- package/src/web/client.js +668 -204
- package/src/web/index.html +24 -4
- package/src/web/server.ts +596 -10
- package/staging/auto-evolve/clean-001/.review-verdict +9 -0
- package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
- package/staging/auto-evolve/e2e-001/.patch-id +1 -0
- package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
- package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
- package/staging/auto-evolve/test-bad/.review-verdict +12 -0
- package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# auto-evolve-snapshot.sh — 阶段 C 护栏 2 + 3
|
|
3
|
+
#
|
|
4
|
+
# 用法 (LLM 改源码前必调):
|
|
5
|
+
# bash scripts/auto-evolve-snapshot.sh # 打 baseline tag + 记当前 HEAD
|
|
6
|
+
# bash scripts/auto-evolve-snapshot.sh apply <patch-id> # 人类批准后合并 staging → main
|
|
7
|
+
# bash scripts/auto-evolve-snapshot.sh list # 列所有 baseline
|
|
8
|
+
# bash scripts/auto-evolve-snapshot.sh rollback <tag> # 回滚到指定 baseline
|
|
9
|
+
#
|
|
10
|
+
# 流程:
|
|
11
|
+
# 1. LLM 改之前调 snapshot: 当前 HEAD 打 auto-evolve-baseline-<ts> tag
|
|
12
|
+
# 2. LLM 改 staging/auto-evolve/<patch-id>/ (不进 src/)
|
|
13
|
+
# 3. 护栏 1 (lefthook) 跑 vitest + tsc, 坏就 abort
|
|
14
|
+
# 4. 护栏 4 (reviewer hook) 审 diff, 通过才准 apply
|
|
15
|
+
# 5. 人类调 apply: git apply staging/auto-evolve/<patch-id>/*.patch → src/
|
|
16
|
+
# 6. 出问题: 调 rollback → git reset --hard <tag>
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
21
|
+
cd "$REPO_ROOT"
|
|
22
|
+
|
|
23
|
+
STAGING_DIR="staging/auto-evolve"
|
|
24
|
+
TAG_PREFIX="auto-evolve-baseline-"
|
|
25
|
+
|
|
26
|
+
cmd="${1:-snapshot}"
|
|
27
|
+
patch_id="${2:-}"
|
|
28
|
+
|
|
29
|
+
case "$cmd" in
|
|
30
|
+
snapshot)
|
|
31
|
+
# 护栏 2: 打 baseline tag
|
|
32
|
+
if ! git diff --quiet HEAD 2>/dev/null; then
|
|
33
|
+
echo "❌ 有未提交改动. 先 git commit 或 git stash"
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
ts="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
37
|
+
tag="${TAG_PREFIX}${ts}"
|
|
38
|
+
git tag -a "$tag" -m "auto-evolve baseline @ ${ts}" HEAD
|
|
39
|
+
echo "✅ baseline tag: $tag"
|
|
40
|
+
echo "$tag" > .last-auto-evolve-baseline
|
|
41
|
+
echo " 回滚命令: bash $0 rollback $tag"
|
|
42
|
+
;;
|
|
43
|
+
|
|
44
|
+
prepare)
|
|
45
|
+
# LLM 改之前调: 创建 staging 目录
|
|
46
|
+
if [ -z "$patch_id" ]; then
|
|
47
|
+
echo "用法: $0 prepare <patch-id>"
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
mkdir -p "$STAGING_DIR/$patch_id"
|
|
51
|
+
echo "$patch_id" > "$STAGING_DIR/$patch_id/.patch-id"
|
|
52
|
+
echo "✅ staging 创建: $STAGING_DIR/$patch_id/"
|
|
53
|
+
echo " LLM 改完后把 patch 放这里: $STAGING_DIR/$patch_id/*.patch"
|
|
54
|
+
;;
|
|
55
|
+
|
|
56
|
+
apply)
|
|
57
|
+
# 人类批准: 把 staging 的 patch 合并到 src/
|
|
58
|
+
if [ -z "$patch_id" ]; then
|
|
59
|
+
echo "用法: $0 apply <patch-id>"
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
patch_dir="$STAGING_DIR/$patch_id"
|
|
63
|
+
if [ ! -d "$patch_dir" ]; then
|
|
64
|
+
echo "❌ staging 不存在: $patch_dir"
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
# 必须先 snapshot
|
|
68
|
+
if [ ! -f .last-auto-evolve-baseline ]; then
|
|
69
|
+
echo "❌ 没有 baseline. 先跑: $0 snapshot"
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
baseline="$(cat .last-auto-evolve-baseline)"
|
|
73
|
+
|
|
74
|
+
# 应用所有 patch
|
|
75
|
+
applied=0
|
|
76
|
+
for p in "$patch_dir"/*.patch; do
|
|
77
|
+
[ -e "$p" ] || continue
|
|
78
|
+
echo " applying: $p"
|
|
79
|
+
if ! git apply --check "$p"; then
|
|
80
|
+
echo "❌ patch 不可用: $p (可能已应用过)"
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
git apply "$p"
|
|
84
|
+
applied=$((applied + 1))
|
|
85
|
+
done
|
|
86
|
+
|
|
87
|
+
if [ $applied -eq 0 ]; then
|
|
88
|
+
echo "❌ staging 里没有 .patch 文件"
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
echo "✅ 应用 $applied 个 patch"
|
|
93
|
+
echo " 建议现在跑: npm test + git commit -m 'auto-evolve: $patch_id'"
|
|
94
|
+
echo " 出问题回滚: $0 rollback $baseline"
|
|
95
|
+
;;
|
|
96
|
+
|
|
97
|
+
rollback)
|
|
98
|
+
# 回滚到指定 baseline
|
|
99
|
+
if [ -z "$patch_id" ]; then
|
|
100
|
+
echo "用法: $0 rollback <tag>"
|
|
101
|
+
echo "可用的 baseline:"
|
|
102
|
+
git tag -l "${TAG_PREFIX}*" | sort -r | head -10
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
if ! git tag -l | grep -qx "$patch_id"; then
|
|
106
|
+
echo "❌ tag 不存在: $patch_id"
|
|
107
|
+
exit 1
|
|
108
|
+
fi
|
|
109
|
+
echo "⚠️ 将回滚到 $patch_id (hard reset, 丢弃之后所有改动)"
|
|
110
|
+
read -p "确认? [y/N] " -n 1 -r
|
|
111
|
+
echo
|
|
112
|
+
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
113
|
+
git reset --hard "$patch_id"
|
|
114
|
+
echo "✅ 回滚到 $patch_id"
|
|
115
|
+
else
|
|
116
|
+
echo "取消"
|
|
117
|
+
fi
|
|
118
|
+
;;
|
|
119
|
+
|
|
120
|
+
list)
|
|
121
|
+
echo "auto-evolve baselines (最近 10):"
|
|
122
|
+
git tag -l "${TAG_PREFIX}*" | sort -r | head -10 | while read tag; do
|
|
123
|
+
msg="$(git tag -l --format='%(contents)' "$tag" | head -1)"
|
|
124
|
+
echo " $tag — $msg"
|
|
125
|
+
done
|
|
126
|
+
if [ -f .last-auto-evolve-baseline ]; then
|
|
127
|
+
echo ""
|
|
128
|
+
echo "当前 baseline: $(cat .last-auto-evolve-baseline)"
|
|
129
|
+
fi
|
|
130
|
+
;;
|
|
131
|
+
|
|
132
|
+
*)
|
|
133
|
+
echo "用法: $0 {snapshot|prepare <id>|apply <id>|rollback <tag>|list}"
|
|
134
|
+
exit 1
|
|
135
|
+
;;
|
|
136
|
+
esac
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# detect-schema-changes.sh — 阶段 C 护栏 6
|
|
3
|
+
#
|
|
4
|
+
# 检查 LLM 改的文件里有没有动 schema (interface / type / enum 声明)
|
|
5
|
+
# 有则强制要求 reviewer 双签 (在 .auto-evolve-review-required 文件)
|
|
6
|
+
#
|
|
7
|
+
# 用法 (在 staging 准备好 patch 后, apply 前):
|
|
8
|
+
# bash scripts/detect-schema-changes.sh <patch-id>
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
13
|
+
cd "$REPO_ROOT"
|
|
14
|
+
|
|
15
|
+
patch_id="${1:-}"
|
|
16
|
+
if [ -z "$patch_id" ]; then
|
|
17
|
+
echo "用法: $0 <patch-id>"
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
staging_dir="staging/auto-evolve/$patch_id"
|
|
22
|
+
if [ ! -d "$staging_dir" ]; then
|
|
23
|
+
echo "❌ staging 不存在: $staging_dir"
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
flag="$staging_dir/.schema-changed"
|
|
28
|
+
rm -f "$flag"
|
|
29
|
+
|
|
30
|
+
found=0
|
|
31
|
+
for patch in "$staging_dir"/*.patch; do
|
|
32
|
+
[ -e "$patch" ] || continue
|
|
33
|
+
# 检查 patch 里有没有新增/修改 interface、type、enum 声明
|
|
34
|
+
if grep -E '^\+.*(interface |type [A-Z][a-zA-Z0-9_]* =|enum [A-Z][a-zA-Z0-9_]*)' "$patch" > /dev/null 2>&1; then
|
|
35
|
+
found=1
|
|
36
|
+
echo "⚠️ schema 改动检测到: $patch"
|
|
37
|
+
grep -E '^\+.*(interface |type [A-Z][a-zA-Z0-9_]* =|enum [A-Z][a-zA-Z0-9_]*)' "$patch" | head -3
|
|
38
|
+
fi
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
if [ $found -eq 1 ]; then
|
|
42
|
+
touch "$flag"
|
|
43
|
+
echo ""
|
|
44
|
+
echo "🚨 schema 改动标记: $flag"
|
|
45
|
+
echo " 护栏 6 触发: apply 时会强制要求双签 reviewer"
|
|
46
|
+
else
|
|
47
|
+
echo "✅ 无 schema 改动, 走单签"
|
|
48
|
+
fi
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* diff-reviewer.ts — 阶段 C 护栏 4
|
|
4
|
+
*
|
|
5
|
+
* 2nd LLM call 审第 1 个的 diff:
|
|
6
|
+
* - 默认用 Haiku (便宜, ~$0.001/diff)
|
|
7
|
+
* - schema 改动升级 Sonnet (护栏 6 双签)
|
|
8
|
+
*
|
|
9
|
+
* 输入: patch 文件 (git format-patch 风格)
|
|
10
|
+
* 输出: .review-verdict 写到 staging/<patch-id>/, 含 PASS/FAIL + 原因
|
|
11
|
+
*
|
|
12
|
+
* 用法:
|
|
13
|
+
* tsx scripts/diff-reviewer.ts <patch-id> [--model sonnet]
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from 'fs/promises';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
// Anthropic SDK 在 review() 里 lazy import — 无 API key 或没装 SDK 都不应炸
|
|
19
|
+
|
|
20
|
+
const STAGING_DIR = 'staging/auto-evolve';
|
|
21
|
+
const ENV_KEY_MODEL = 'AUTO_EVOLVE_REVIEW_MODEL';
|
|
22
|
+
|
|
23
|
+
const REVIEW_PROMPT = `你是一个严格的代码审查员. 审查以下 git diff (LLM 自动生成的源码改动).
|
|
24
|
+
|
|
25
|
+
只回答 JSON, 字段:
|
|
26
|
+
{
|
|
27
|
+
"verdict": "PASS" | "FAIL",
|
|
28
|
+
"concerns": ["concern 1", "concern 2", ...],
|
|
29
|
+
"suggestions": ["suggestion 1", ...]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
PASS 条件 (全部满足才 PASS):
|
|
33
|
+
1. 改动能解决它声称要解决的问题
|
|
34
|
+
2. 没引入明显 bug (空指针, 内存泄漏, 死循环, race condition)
|
|
35
|
+
3. 没引入安全漏洞 (injection, XSS, 越权)
|
|
36
|
+
4. 边界条件考虑 (空数组, undefined, 极值, 并发)
|
|
37
|
+
5. 改动局限在它声称的范围, 不夹带私货
|
|
38
|
+
6. 跟现有代码风格一致
|
|
39
|
+
|
|
40
|
+
FAIL 条件 (任一即 FAIL):
|
|
41
|
+
- 满足上面任一 PASS 条件的反例
|
|
42
|
+
- 引入了新依赖 (必须在 concerns 里说)
|
|
43
|
+
- 删除了测试 / 注释掉失败用例
|
|
44
|
+
- 用了 any / unknown / @ts-ignore 偷懒
|
|
45
|
+
|
|
46
|
+
DIFF:
|
|
47
|
+
{{DIFF}}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
function degradedGrepReview(patchContent: string): { verdict: string; concerns: string[]; suggestions: string[]; raw: string } {
|
|
51
|
+
console.warn('[diff-reviewer] ⚠️ 降级 grep 检查 (无 API key 或无 SDK)');
|
|
52
|
+
const concerns: string[] = [];
|
|
53
|
+
if (/\+.*\bany\b/.test(patchContent)) concerns.push('使用了 any');
|
|
54
|
+
if (/\+.*\b@ts-ignore\b/.test(patchContent)) concerns.push('使用了 @ts-ignore');
|
|
55
|
+
if (/\+.*\bconsole\.log\b/.test(patchContent)) concerns.push('留了 console.log');
|
|
56
|
+
if (/\-.*test\(/.test(patchContent)) concerns.push('删除了测试');
|
|
57
|
+
return {
|
|
58
|
+
verdict: concerns.length > 0 ? 'FAIL' : 'PASS',
|
|
59
|
+
concerns,
|
|
60
|
+
suggestions: [],
|
|
61
|
+
raw: '(degraded grep mode)',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function review(patchContent: string, model: string): Promise<{ verdict: string; concerns: string[]; suggestions: string[]; raw: string }> {
|
|
66
|
+
// 无 API key → 直接降级, 不 import SDK (避免 no-ANTHROPIC 环境下也炸)
|
|
67
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
68
|
+
return degradedGrepReview(patchContent);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 有 API key → 尝试 lazy import SDK
|
|
72
|
+
let Anthropic: any;
|
|
73
|
+
try {
|
|
74
|
+
({ Anthropic } = await import('@anthropic-ai/sdk'));
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.warn('[diff-reviewer] ⚠️ @anthropic-ai/sdk 未装, 降级 grep');
|
|
77
|
+
return degradedGrepReview(patchContent);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const client = new Anthropic();
|
|
81
|
+
const prompt = REVIEW_PROMPT.replace('{{DIFF}}', patchContent.slice(0, 20000)); // 限长
|
|
82
|
+
const resp = await client.messages.create({
|
|
83
|
+
model,
|
|
84
|
+
max_tokens: 1024,
|
|
85
|
+
messages: [{ role: 'user', content: prompt }],
|
|
86
|
+
});
|
|
87
|
+
const text = resp.content[0].type === 'text' ? resp.content[0].text : '';
|
|
88
|
+
// 解析 JSON
|
|
89
|
+
const m = /\{[\s\S]*\}/.exec(text);
|
|
90
|
+
if (!m) {
|
|
91
|
+
return { verdict: 'FAIL', concerns: ['LLM 没返回 JSON'], suggestions: [], raw: text };
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(m[0]);
|
|
95
|
+
return {
|
|
96
|
+
verdict: parsed.verdict === 'PASS' ? 'PASS' : 'FAIL',
|
|
97
|
+
concerns: parsed.concerns || [],
|
|
98
|
+
suggestions: parsed.suggestions || [],
|
|
99
|
+
raw: text,
|
|
100
|
+
};
|
|
101
|
+
} catch {
|
|
102
|
+
return { verdict: 'FAIL', concerns: ['JSON 解析失败'], suggestions: [], raw: text };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function main() {
|
|
107
|
+
const args = process.argv.slice(2);
|
|
108
|
+
const patchId = args[0];
|
|
109
|
+
if (!patchId) {
|
|
110
|
+
console.error('用法: tsx diff-reviewer.ts <patch-id> [--model sonnet]');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
const forceModel = args.includes('--model') ? args[args.indexOf('--model') + 1] : null;
|
|
114
|
+
|
|
115
|
+
const stagingDir = path.join(STAGING_DIR, patchId);
|
|
116
|
+
const schemaFlag = path.join(stagingDir, '.schema-changed');
|
|
117
|
+
const isSchemaChange = await fs.access(schemaFlag).then(() => true).catch(() => false);
|
|
118
|
+
|
|
119
|
+
const defaultModel = isSchemaChange ? 'claude-sonnet-4-6' : 'claude-haiku-4-5';
|
|
120
|
+
const model = forceModel || process.env[ENV_KEY_MODEL] || defaultModel;
|
|
121
|
+
|
|
122
|
+
console.log(`[diff-reviewer] patch=${patchId} schema=${isSchemaChange} model=${model}`);
|
|
123
|
+
|
|
124
|
+
// 合并所有 patch
|
|
125
|
+
const files = await fs.readdir(stagingDir);
|
|
126
|
+
const patches = files.filter((f) => f.endsWith('.patch'));
|
|
127
|
+
if (patches.length === 0) {
|
|
128
|
+
console.error('❌ staging 里没有 .patch 文件');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let combined = '';
|
|
133
|
+
for (const p of patches) {
|
|
134
|
+
combined += `\n=== ${p} ===\n` + (await fs.readFile(path.join(stagingDir, p), 'utf-8'));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const r = await review(combined, model);
|
|
138
|
+
|
|
139
|
+
const verdict = {
|
|
140
|
+
patchId,
|
|
141
|
+
model,
|
|
142
|
+
schemaChange: isSchemaChange,
|
|
143
|
+
verdict: r.verdict,
|
|
144
|
+
concerns: r.concerns,
|
|
145
|
+
suggestions: r.suggestions,
|
|
146
|
+
reviewedAt: new Date().toISOString(),
|
|
147
|
+
};
|
|
148
|
+
await fs.writeFile(path.join(stagingDir, '.review-verdict'), JSON.stringify(verdict, null, 2));
|
|
149
|
+
|
|
150
|
+
console.log(`[diff-reviewer] verdict=${r.verdict} concerns=${r.concerns.length}`);
|
|
151
|
+
if (r.verdict === 'FAIL') {
|
|
152
|
+
for (const c of r.concerns) console.log(` - ${c}`);
|
|
153
|
+
process.exit(2);
|
|
154
|
+
} else {
|
|
155
|
+
for (const s of r.suggestions) console.log(` 💡 ${s}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main().catch((e) => { console.error(e); process.exit(1); });
|