cli-claw-kit 0.0.1
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/LICENSE +21 -0
- package/README.md +245 -0
- package/config/default-groups.json +1 -0
- package/config/global-agents-md.template.md +37 -0
- package/config/mount-allowlist.json +11 -0
- package/container/Dockerfile +160 -0
- package/container/agent-runner/dist/.tsbuildinfo +1 -0
- package/container/agent-runner/dist/agent-definitions.js +22 -0
- package/container/agent-runner/dist/channel-prefixes.js +16 -0
- package/container/agent-runner/dist/codex-config.js +29 -0
- package/container/agent-runner/dist/image-detector.js +96 -0
- package/container/agent-runner/dist/index.js +2587 -0
- package/container/agent-runner/dist/mcp-tools.js +1076 -0
- package/container/agent-runner/dist/stream-event.types.js +5 -0
- package/container/agent-runner/dist/stream-processor.js +867 -0
- package/container/agent-runner/dist/types.js +6 -0
- package/container/agent-runner/dist/utils.js +115 -0
- package/container/agent-runner/package.json +36 -0
- package/container/agent-runner/prompts/security-rules.md +31 -0
- package/container/agent-runner/src/agent-definitions.ts +27 -0
- package/container/agent-runner/src/channel-prefixes.ts +16 -0
- package/container/agent-runner/src/codex-config.ts +40 -0
- package/container/agent-runner/src/image-detector.ts +116 -0
- package/container/agent-runner/src/index.ts +3107 -0
- package/container/agent-runner/src/mcp-tools.ts +1295 -0
- package/container/agent-runner/src/stream-event.types.ts +10 -0
- package/container/agent-runner/src/stream-processor.ts +932 -0
- package/container/agent-runner/src/types.ts +75 -0
- package/container/agent-runner/src/utils.ts +114 -0
- package/container/agent-runner/tsconfig.json +17 -0
- package/container/build.sh +28 -0
- package/container/entrypoint.sh +64 -0
- package/container/skills/agent-browser/SKILL.md +159 -0
- package/container/skills/install-skill/SKILL.md +64 -0
- package/container/skills/post-test-cleanup/SKILL.md +121 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/agent-output-parser.js +459 -0
- package/dist/app-root.js +52 -0
- package/dist/assistant-meta-footer.js +1 -0
- package/dist/auth.js +91 -0
- package/dist/billing.js +694 -0
- package/dist/channel-prefixes.js +16 -0
- package/dist/cli.js +86 -0
- package/dist/commands.js +79 -0
- package/dist/config.js +120 -0
- package/dist/container-runner.js +981 -0
- package/dist/daily-summary.js +210 -0
- package/dist/db.js +3683 -0
- package/dist/dingtalk.js +1347 -0
- package/dist/feishu-markdown-style.js +97 -0
- package/dist/feishu-streaming-card.js +1875 -0
- package/dist/feishu.js +1628 -0
- package/dist/file-manager.js +270 -0
- package/dist/group-queue.js +1070 -0
- package/dist/group-runtime.js +35 -0
- package/dist/host-workspace-cwd.js +85 -0
- package/dist/im-channel.js +384 -0
- package/dist/im-command-utils.js +142 -0
- package/dist/im-downloader.js +45 -0
- package/dist/im-manager.js +527 -0
- package/dist/im-utils.js +53 -0
- package/dist/image-detector.js +96 -0
- package/dist/index.js +5828 -0
- package/dist/logger.js +22 -0
- package/dist/mcp-utils.js +66 -0
- package/dist/message-attachments.js +69 -0
- package/dist/message-notifier.js +36 -0
- package/dist/middleware/auth.js +85 -0
- package/dist/mount-security.js +315 -0
- package/dist/permissions.js +67 -0
- package/dist/project-memory.js +6 -0
- package/dist/provider-pool.js +189 -0
- package/dist/qq.js +826 -0
- package/dist/reset-admin.js +42 -0
- package/dist/routes/admin.js +543 -0
- package/dist/routes/agent-definitions.js +241 -0
- package/dist/routes/agents.js +533 -0
- package/dist/routes/auth.js +675 -0
- package/dist/routes/billing.js +490 -0
- package/dist/routes/browse.js +210 -0
- package/dist/routes/bug-report.js +387 -0
- package/dist/routes/config.js +1868 -0
- package/dist/routes/files.js +671 -0
- package/dist/routes/groups.js +1367 -0
- package/dist/routes/mcp-servers.js +320 -0
- package/dist/routes/memory.js +523 -0
- package/dist/routes/monitor.js +307 -0
- package/dist/routes/skills.js +777 -0
- package/dist/routes/tasks.js +509 -0
- package/dist/routes/usage.js +64 -0
- package/dist/routes/workspace-config.js +458 -0
- package/dist/runtime-build.js +112 -0
- package/dist/runtime-command-handler.js +189 -0
- package/dist/runtime-command-registry.js +1 -0
- package/dist/runtime-config.js +1777 -0
- package/dist/runtime-identity.js +52 -0
- package/dist/schemas.js +590 -0
- package/dist/script-runner.js +64 -0
- package/dist/sdk-query.js +82 -0
- package/dist/skill-utils.js +145 -0
- package/dist/sqlite-compat.js +19 -0
- package/dist/stream-event.types.js +5 -0
- package/dist/streaming-runtime-meta.js +29 -0
- package/dist/task-scheduler.js +695 -0
- package/dist/task-utils.js +13 -0
- package/dist/telegram-pairing.js +59 -0
- package/dist/telegram.js +897 -0
- package/dist/terminal-manager.js +307 -0
- package/dist/tool-step-display.js +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +85 -0
- package/dist/web-context.js +161 -0
- package/dist/web.js +1377 -0
- package/dist/wechat-crypto.js +182 -0
- package/dist/wechat.js +589 -0
- package/dist/workspace-runtime-reset.js +35 -0
- package/package.json +107 -0
- package/shared/assistant-meta-footer.ts +127 -0
- package/shared/channel-prefixes.ts +16 -0
- package/shared/dist/assistant-meta-footer.d.ts +29 -0
- package/shared/dist/assistant-meta-footer.js +85 -0
- package/shared/dist/channel-prefixes.d.ts +4 -0
- package/shared/dist/channel-prefixes.js +16 -0
- package/shared/dist/image-detector.d.ts +20 -0
- package/shared/dist/image-detector.js +96 -0
- package/shared/dist/runtime-command-registry.d.ts +38 -0
- package/shared/dist/runtime-command-registry.js +185 -0
- package/shared/dist/stream-event.d.ts +65 -0
- package/shared/dist/stream-event.js +8 -0
- package/shared/dist/tool-step-display.d.ts +4 -0
- package/shared/dist/tool-step-display.js +11 -0
- package/shared/image-detector.ts +116 -0
- package/shared/runtime-command-registry.ts +252 -0
- package/shared/stream-event.ts +67 -0
- package/shared/tool-step-display.ts +21 -0
- package/shared/tsconfig.json +24 -0
- package/web/dist/assets/BillingPage-B1wBR_o-.js +52 -0
- package/web/dist/assets/ChatPage-6GBZ9nXN.css +32 -0
- package/web/dist/assets/ChatPage-BOJcXtaj.js +161 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/SettingsPage-DoY7FoZ_.js +153 -0
- package/web/dist/assets/ShareImageDialog-C1ga8b7l.js +22 -0
- package/web/dist/assets/TasksPage-CRivnNsx.js +14 -0
- package/web/dist/assets/_basePickBy-Bf-bSoS9.js +1 -0
- package/web/dist/assets/_baseUniq-zAOaCuKw.js +1 -0
- package/web/dist/assets/arc-Dm9mVQ9U.js +1 -0
- package/web/dist/assets/architectureDiagram-2XIMDMQ5-BLmzX1wr.js +36 -0
- package/web/dist/assets/band-CquvqAHh.js +1 -0
- package/web/dist/assets/blockDiagram-WCTKOSBZ-B9pcqm3j.js +132 -0
- package/web/dist/assets/c4Diagram-IC4MRINW-Cytx1q3b.js +10 -0
- package/web/dist/assets/channel-BOVj73LR.js +1 -0
- package/web/dist/assets/channel-meta-CQD0Pei-.js +41 -0
- package/web/dist/assets/chunk-4BX2VUAB-0ToDr6RE.js +1 -0
- package/web/dist/assets/chunk-55IACEB6-DQDjnXfS.js +1 -0
- package/web/dist/assets/chunk-FMBD7UC4-Di8ABm6c.js +15 -0
- package/web/dist/assets/chunk-JSJVCQXG-BZQN6rnX.js +1 -0
- package/web/dist/assets/chunk-KX2RTZJC-zBbcpaN_.js +1 -0
- package/web/dist/assets/chunk-NQ4KR5QH-BCrLoU88.js +220 -0
- package/web/dist/assets/chunk-QZHKN3VN-Bqk8juan.js +1 -0
- package/web/dist/assets/chunk-WL4C6EOR-D2YX-MHY.js +189 -0
- package/web/dist/assets/classDiagram-VBA2DB6C-DUUoMyaK.js +1 -0
- package/web/dist/assets/classDiagram-v2-RAHNMMFH-DUUoMyaK.js +1 -0
- package/web/dist/assets/clone-BmaCesfa.js +1 -0
- package/web/dist/assets/cose-bilkent-S5V4N54A-CTsv6qQA.js +1 -0
- package/web/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
- package/web/dist/assets/dagre-KLK3FWXG-Ci4Jh9nu.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-E7M64L7V-BFRnfTI2.js +24 -0
- package/web/dist/assets/diagram-IFDJBPK2-B7Zhnp0b.js +43 -0
- package/web/dist/assets/diagram-P4PSJMXO-BVyP7nwq.js +24 -0
- package/web/dist/assets/erDiagram-INFDFZHY-NorKdTOF.js +70 -0
- package/web/dist/assets/error-CGD5mp5f.js +1 -0
- package/web/dist/assets/flowDiagram-PKNHOUZH-Ch97nABF.js +162 -0
- package/web/dist/assets/ganttDiagram-A5KZAMGK-BQ2pLWsy.js +292 -0
- package/web/dist/assets/gitGraphDiagram-K3NZZRJ6-bcvnBsD2.js +65 -0
- package/web/dist/assets/graph-CeAEckur.js +1 -0
- package/web/dist/assets/index-CPnL1_qC.js +768 -0
- package/web/dist/assets/index-DVevCbcO.css +10 -0
- package/web/dist/assets/infoDiagram-LFFYTUFH-CcsrFdj-.js +2 -0
- package/web/dist/assets/init-Dmth1JHB.js +1 -0
- package/web/dist/assets/ishikawaDiagram-PHBUUO56-1upyMfHN.js +70 -0
- package/web/dist/assets/journeyDiagram-4ABVD52K-CKUi-V0c.js +139 -0
- package/web/dist/assets/kanban-definition-K7BYSVSG-DOnQwXfL.js +89 -0
- package/web/dist/assets/layout-BmMMqTnJ.js +1 -0
- package/web/dist/assets/linear-DiaJloY5.js +1 -0
- package/web/dist/assets/mermaid.core-BWLV1B2v.js +254 -0
- package/web/dist/assets/mindmap-definition-YRQLILUH-BeAKHVWP.js +68 -0
- package/web/dist/assets/ordinal-DILIJJjt.js +1 -0
- package/web/dist/assets/pieDiagram-SKSYHLDU-DfiMSfWo.js +30 -0
- package/web/dist/assets/quadrantDiagram-337W2JSQ-wZxZOJxd.js +7 -0
- package/web/dist/assets/requirementDiagram-Z7DCOOCP-BK4HHm17.js +73 -0
- package/web/dist/assets/sankeyDiagram-WA2Y5GQK-BX6t2avX.js +10 -0
- package/web/dist/assets/sequenceDiagram-2WXFIKYE-BPQlkbAa.js +145 -0
- package/web/dist/assets/sheet-rI0FfB1g.js +6 -0
- package/web/dist/assets/sliders-horizontal-CuijWFNK.js +6 -0
- package/web/dist/assets/sparkles-BsMYXJoT.js +11 -0
- package/web/dist/assets/square-0CqMX1Q3.js +11 -0
- package/web/dist/assets/stateDiagram-RAJIS63D-DxkV0Vwd.js +1 -0
- package/web/dist/assets/stateDiagram-v2-FVOUBMTO-qLYoiOPe.js +1 -0
- package/web/dist/assets/step-D51IIHGA.js +1 -0
- package/web/dist/assets/tasks-D8JjBTwx.js +1 -0
- package/web/dist/assets/time-O8zIGux3.js +1 -0
- package/web/dist/assets/timeline-definition-YZTLITO2-kNp1DyFc.js +61 -0
- package/web/dist/assets/treemap-KZPCXAKY-CkrClVhk.js +162 -0
- package/web/dist/assets/utils-KGAn0XTg.js +11 -0
- package/web/dist/assets/vennDiagram-LZ73GAT5-CgdzEZz4.js +34 -0
- package/web/dist/assets/xychartDiagram-JWTSCODW-DfYGPfNB.js +7 -0
- package/web/dist/assets/zap-_hKJYy7J.js +6 -0
- package/web/dist/favicon.svg +332 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-55-Regular.woff2 +0 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-65-Medium.woff2 +0 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-75-SemiBold.woff2 +0 -0
- package/web/dist/fonts/DMSans-latin-ext.woff2 +0 -0
- package/web/dist/fonts/DMSans-latin.woff2 +0 -0
- package/web/dist/icons/README.md +20 -0
- package/web/dist/icons/apple-touch-icon-180.png +0 -0
- package/web/dist/icons/icon-128.png +0 -0
- package/web/dist/icons/icon-144.png +0 -0
- package/web/dist/icons/icon-152.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-192.svg +332 -0
- package/web/dist/icons/icon-384.png +0 -0
- package/web/dist/icons/icon-48.png +0 -0
- package/web/dist/icons/icon-512-maskable.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon-512.svg +332 -0
- package/web/dist/icons/icon-72.png +0 -0
- package/web/dist/icons/icon-96.png +0 -0
- package/web/dist/icons/loading-logo.svg +332 -0
- package/web/dist/icons/logo-1024.png +0 -0
- package/web/dist/icons/logo-icon.svg +332 -0
- package/web/dist/icons/logo-text.svg +332 -0
- package/web/dist/index.html +30 -0
- package/web/dist/manifest.webmanifest +1 -0
- package/web/dist/registerSW.js +1 -0
- package/web/dist/sw.js +1 -0
- package/web/dist/workbox-08d6266a.js +1 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { hasHostExecutionPermission } from '../web-context.js';
|
|
5
|
+
import { authMiddleware } from '../middleware/auth.js';
|
|
6
|
+
import { logger } from '../logger.js';
|
|
7
|
+
import { loadMountAllowlist, expandPath, findAllowedRoot, matchesBlockedPattern, } from '../mount-security.js';
|
|
8
|
+
const MAX_ENTRIES = 200;
|
|
9
|
+
const browseRoutes = new Hono();
|
|
10
|
+
/**
|
|
11
|
+
* List subdirectories of a given path, filtering hidden dirs and blocked patterns.
|
|
12
|
+
*/
|
|
13
|
+
function listSubdirectories(dirPath, blockedPatterns) {
|
|
14
|
+
let entries;
|
|
15
|
+
try {
|
|
16
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const dirs = [];
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (!entry.isDirectory())
|
|
24
|
+
continue;
|
|
25
|
+
// Skip hidden directories
|
|
26
|
+
if (entry.name.startsWith('.'))
|
|
27
|
+
continue;
|
|
28
|
+
// Skip blocked patterns
|
|
29
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
30
|
+
if (matchesBlockedPattern(fullPath, blockedPatterns) !== null)
|
|
31
|
+
continue;
|
|
32
|
+
// Check if has subdirectories (for expand indicator)
|
|
33
|
+
let hasChildren = false;
|
|
34
|
+
try {
|
|
35
|
+
const children = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
36
|
+
hasChildren = children.some((c) => c.isDirectory() && !c.name.startsWith('.'));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Permission denied or other error — treat as no children
|
|
40
|
+
}
|
|
41
|
+
dirs.push({ name: entry.name, path: fullPath, hasChildren });
|
|
42
|
+
if (dirs.length >= MAX_ENTRIES)
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
dirs.sort((a, b) => a.name.localeCompare(b.name));
|
|
46
|
+
return dirs;
|
|
47
|
+
}
|
|
48
|
+
// GET /api/browse/directories?path=xxx
|
|
49
|
+
browseRoutes.get('/directories', authMiddleware, (c) => {
|
|
50
|
+
const authUser = c.get('user');
|
|
51
|
+
if (!hasHostExecutionPermission(authUser)) {
|
|
52
|
+
return c.json({ error: 'Insufficient permissions' }, 403);
|
|
53
|
+
}
|
|
54
|
+
const requestedPath = c.req.query('path');
|
|
55
|
+
const allowlist = loadMountAllowlist();
|
|
56
|
+
const blockedPatterns = allowlist?.blockedPatterns ?? [];
|
|
57
|
+
const hasAllowlist = allowlist !== null && allowlist.allowedRoots.length > 0;
|
|
58
|
+
// No path → return root listing
|
|
59
|
+
if (!requestedPath) {
|
|
60
|
+
if (hasAllowlist) {
|
|
61
|
+
// Return allowlist roots as top-level entries
|
|
62
|
+
const roots = [];
|
|
63
|
+
for (const root of allowlist.allowedRoots) {
|
|
64
|
+
const expanded = expandPath(root.path);
|
|
65
|
+
let realPath;
|
|
66
|
+
try {
|
|
67
|
+
realPath = fs.realpathSync(expanded);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
continue; // Root doesn't exist, skip
|
|
71
|
+
}
|
|
72
|
+
if (!fs.existsSync(realPath) || !fs.statSync(realPath).isDirectory())
|
|
73
|
+
continue;
|
|
74
|
+
let hasChildren = false;
|
|
75
|
+
try {
|
|
76
|
+
const children = fs.readdirSync(realPath, { withFileTypes: true });
|
|
77
|
+
hasChildren = children.some((ch) => ch.isDirectory() && !ch.name.startsWith('.'));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* ignore */
|
|
81
|
+
}
|
|
82
|
+
roots.push({
|
|
83
|
+
name: root.description || path.basename(realPath),
|
|
84
|
+
path: realPath,
|
|
85
|
+
hasChildren,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return c.json({
|
|
89
|
+
currentPath: null,
|
|
90
|
+
parentPath: null,
|
|
91
|
+
directories: roots,
|
|
92
|
+
hasAllowlist: true,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// No allowlist → return HOME directory
|
|
96
|
+
const homeDir = process.env.HOME || '/';
|
|
97
|
+
return c.json({
|
|
98
|
+
currentPath: homeDir,
|
|
99
|
+
parentPath: homeDir === '/' ? null : path.dirname(homeDir),
|
|
100
|
+
directories: listSubdirectories(homeDir, blockedPatterns),
|
|
101
|
+
hasAllowlist: false,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Validate path
|
|
105
|
+
if (!path.isAbsolute(requestedPath)) {
|
|
106
|
+
return c.json({ error: 'Path must be absolute' }, 400);
|
|
107
|
+
}
|
|
108
|
+
let realPath;
|
|
109
|
+
try {
|
|
110
|
+
realPath = fs.realpathSync(requestedPath);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return c.json({ error: 'Path does not exist' }, 400);
|
|
114
|
+
}
|
|
115
|
+
if (!fs.statSync(realPath).isDirectory()) {
|
|
116
|
+
return c.json({ error: 'Path is not a directory' }, 400);
|
|
117
|
+
}
|
|
118
|
+
// Allowlist range check
|
|
119
|
+
if (hasAllowlist) {
|
|
120
|
+
const root = findAllowedRoot(realPath, allowlist.allowedRoots);
|
|
121
|
+
if (!root) {
|
|
122
|
+
return c.json({ error: 'Path is not within allowed roots' }, 403);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Compute parentPath
|
|
126
|
+
let parentPath = path.dirname(realPath);
|
|
127
|
+
if (parentPath === realPath) {
|
|
128
|
+
// At filesystem root
|
|
129
|
+
parentPath = null;
|
|
130
|
+
}
|
|
131
|
+
else if (hasAllowlist) {
|
|
132
|
+
// Check if parent is still within an allowed root
|
|
133
|
+
const parentRoot = findAllowedRoot(parentPath, allowlist.allowedRoots);
|
|
134
|
+
if (!parentRoot) {
|
|
135
|
+
// Parent is outside allowed roots — return null to go back to root listing
|
|
136
|
+
parentPath = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return c.json({
|
|
140
|
+
currentPath: realPath,
|
|
141
|
+
parentPath,
|
|
142
|
+
directories: listSubdirectories(realPath, blockedPatterns),
|
|
143
|
+
hasAllowlist,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
// POST /api/browse/directories — create a new folder
|
|
147
|
+
browseRoutes.post('/directories', authMiddleware, async (c) => {
|
|
148
|
+
const authUser = c.get('user');
|
|
149
|
+
if (!hasHostExecutionPermission(authUser)) {
|
|
150
|
+
return c.json({ error: 'Insufficient permissions' }, 403);
|
|
151
|
+
}
|
|
152
|
+
const body = await c.req.json().catch(() => ({}));
|
|
153
|
+
const { parentPath, name } = body;
|
|
154
|
+
if (!parentPath || typeof parentPath !== 'string') {
|
|
155
|
+
return c.json({ error: 'parentPath is required' }, 400);
|
|
156
|
+
}
|
|
157
|
+
if (!name || typeof name !== 'string') {
|
|
158
|
+
return c.json({ error: 'name is required' }, 400);
|
|
159
|
+
}
|
|
160
|
+
// Validate name
|
|
161
|
+
if (name.includes('/') || name.includes('..') || name.startsWith('.')) {
|
|
162
|
+
return c.json({ error: 'Invalid folder name: must not contain /, .., or start with .' }, 400);
|
|
163
|
+
}
|
|
164
|
+
if (!path.isAbsolute(parentPath)) {
|
|
165
|
+
return c.json({ error: 'parentPath must be absolute' }, 400);
|
|
166
|
+
}
|
|
167
|
+
let realParent;
|
|
168
|
+
try {
|
|
169
|
+
realParent = fs.realpathSync(parentPath);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return c.json({ error: 'Parent path does not exist' }, 400);
|
|
173
|
+
}
|
|
174
|
+
if (!fs.statSync(realParent).isDirectory()) {
|
|
175
|
+
return c.json({ error: 'Parent path is not a directory' }, 400);
|
|
176
|
+
}
|
|
177
|
+
// Allowlist range check
|
|
178
|
+
const allowlist = loadMountAllowlist();
|
|
179
|
+
const hasAllowlist = allowlist !== null && allowlist.allowedRoots.length > 0;
|
|
180
|
+
if (hasAllowlist) {
|
|
181
|
+
const root = findAllowedRoot(realParent, allowlist.allowedRoots);
|
|
182
|
+
if (!root) {
|
|
183
|
+
return c.json({ error: 'Parent path is not within allowed roots' }, 403);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Blocked patterns check
|
|
187
|
+
const blockedPatterns = allowlist?.blockedPatterns ?? [];
|
|
188
|
+
const targetPath = path.join(realParent, name);
|
|
189
|
+
if (matchesBlockedPattern(targetPath, blockedPatterns) !== null) {
|
|
190
|
+
return c.json({ error: 'Folder name matches a blocked pattern' }, 400);
|
|
191
|
+
}
|
|
192
|
+
// Check if already exists
|
|
193
|
+
if (fs.existsSync(targetPath)) {
|
|
194
|
+
return c.json({ error: 'Directory already exists' }, 400);
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
fs.mkdirSync(targetPath, { recursive: false });
|
|
198
|
+
logger.info({ path: targetPath }, 'Directory created via browse API');
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
logger.error({ err, path: targetPath }, 'Failed to create directory');
|
|
202
|
+
return c.json({ error: 'Failed to create directory' }, 500);
|
|
203
|
+
}
|
|
204
|
+
return c.json({
|
|
205
|
+
name,
|
|
206
|
+
path: targetPath,
|
|
207
|
+
hasChildren: false,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
export default browseRoutes;
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import { Hono } from 'hono';
|
|
7
|
+
import { APP_ROOT, resolveAppPath } from '../app-root.js';
|
|
8
|
+
import { DATA_DIR } from '../config.js';
|
|
9
|
+
import { getUserHomeGroup } from '../db.js';
|
|
10
|
+
import { getClaudeProviderConfig } from '../runtime-config.js';
|
|
11
|
+
import { sdkQuery } from '../sdk-query.js';
|
|
12
|
+
import { logger } from '../logger.js';
|
|
13
|
+
import { authMiddleware } from '../middleware/auth.js';
|
|
14
|
+
import { BugReportGenerateSchema, BugReportSubmitSchema } from '../schemas.js';
|
|
15
|
+
import { getWebDeps } from '../web-context.js';
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
const bugReportRoutes = new Hono();
|
|
18
|
+
// --- Rate limiting (60s per user) ---
|
|
19
|
+
const cooldowns = new Map();
|
|
20
|
+
const COOLDOWN_MS = 60_000;
|
|
21
|
+
const generateCooldowns = new Map();
|
|
22
|
+
const GENERATE_COOLDOWN_MS = 30_000;
|
|
23
|
+
function checkCooldown(userId, map = cooldowns, cooldownMs = COOLDOWN_MS) {
|
|
24
|
+
const last = map.get(userId);
|
|
25
|
+
if (last) {
|
|
26
|
+
const remaining = cooldownMs - (Date.now() - last);
|
|
27
|
+
if (remaining > 0) {
|
|
28
|
+
return `请等待 ${Math.ceil(remaining / 1000)} 秒后再试`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
// --- Capability detection (cached 5min) ---
|
|
34
|
+
let capCache = null;
|
|
35
|
+
const CAP_CACHE_TTL = 5 * 60 * 1000;
|
|
36
|
+
async function checkCapabilities() {
|
|
37
|
+
if (capCache && Date.now() - capCache.checkedAt < CAP_CACHE_TTL) {
|
|
38
|
+
return {
|
|
39
|
+
ghAvailable: capCache.ghAvailable,
|
|
40
|
+
ghUsername: capCache.ghUsername,
|
|
41
|
+
claudeAvailable: capCache.claudeAvailable,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const [gh] = await Promise.all([
|
|
45
|
+
execFileAsync('gh', ['auth', 'status'], { timeout: 5000 })
|
|
46
|
+
.then(() => true)
|
|
47
|
+
.catch(() => false),
|
|
48
|
+
]);
|
|
49
|
+
// Claude availability is determined by provider config, not CLI presence
|
|
50
|
+
const providerConfig = getClaudeProviderConfig();
|
|
51
|
+
const claude = !!(providerConfig.anthropicApiKey ||
|
|
52
|
+
providerConfig.claudeCodeOauthToken ||
|
|
53
|
+
providerConfig.claudeOAuthCredentials);
|
|
54
|
+
// Get gh username if available
|
|
55
|
+
let ghUsername = null;
|
|
56
|
+
if (gh) {
|
|
57
|
+
try {
|
|
58
|
+
const { stdout } = await execFileAsync('gh', ['api', 'user', '--jq', '.login'], { timeout: 5000 });
|
|
59
|
+
ghUsername = stdout.trim() || null;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// gh available but can't get username
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
capCache = {
|
|
66
|
+
ghAvailable: gh,
|
|
67
|
+
ghUsername,
|
|
68
|
+
claudeAvailable: claude,
|
|
69
|
+
checkedAt: Date.now(),
|
|
70
|
+
};
|
|
71
|
+
return { ghAvailable: gh, ghUsername, claudeAvailable: claude };
|
|
72
|
+
}
|
|
73
|
+
// --- Helpers ---
|
|
74
|
+
function getVersion() {
|
|
75
|
+
try {
|
|
76
|
+
const pkg = JSON.parse(fs.readFileSync(resolveAppPath('package.json'), 'utf-8'));
|
|
77
|
+
return pkg.version || 'unknown';
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return 'unknown';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function readRecentLogs(folder, maxLines = 50) {
|
|
84
|
+
const logsDir = path.join(DATA_DIR, 'groups', folder, 'logs');
|
|
85
|
+
try {
|
|
86
|
+
if (!fs.existsSync(logsDir))
|
|
87
|
+
return '(no logs directory)';
|
|
88
|
+
const files = fs
|
|
89
|
+
.readdirSync(logsDir)
|
|
90
|
+
.filter((f) => f.endsWith('.log'))
|
|
91
|
+
.sort()
|
|
92
|
+
.reverse();
|
|
93
|
+
if (files.length === 0)
|
|
94
|
+
return '(no log files)';
|
|
95
|
+
const latestFile = path.join(logsDir, files[0]);
|
|
96
|
+
const content = fs.readFileSync(latestFile, 'utf-8');
|
|
97
|
+
const lines = content.split('\n');
|
|
98
|
+
return lines.slice(-maxLines).join('\n');
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return '(failed to read logs)';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Mask environment variable values in log text */
|
|
105
|
+
function sanitizeLogs(text) {
|
|
106
|
+
let result = text;
|
|
107
|
+
// Replace absolute paths to project root and home directory with placeholders
|
|
108
|
+
const projectRoot = APP_ROOT;
|
|
109
|
+
const homeDir = os.homedir();
|
|
110
|
+
// Replace longer path first to avoid partial replacement
|
|
111
|
+
if (projectRoot.startsWith(homeDir)) {
|
|
112
|
+
result = result.replaceAll(projectRoot, '<project>');
|
|
113
|
+
result = result.replaceAll(homeDir, '<home>');
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
result = result.replaceAll(homeDir, '<home>');
|
|
117
|
+
result = result.replaceAll(projectRoot, '<project>');
|
|
118
|
+
}
|
|
119
|
+
// Generic pattern matching any env var name containing sensitive keywords
|
|
120
|
+
const sensitivePattern = /(\b\w*(?:token|password|passwd|secret|api[_-]?key|auth[_-]?token|authorization|cookie|credential|private[_-]?key|access[_-]?key|app[_-]?secret)\w*)[=:]\s*\S+/gi;
|
|
121
|
+
result = result.replace(sensitivePattern, '$1=***');
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
function buildGeneratePrompt(description, systemInfo, logs, screenshotCount) {
|
|
125
|
+
const sysInfoText = Object.entries(systemInfo)
|
|
126
|
+
.map(([k, v]) => `- ${k}: ${v}`)
|
|
127
|
+
.join('\n');
|
|
128
|
+
const screenshotNote = screenshotCount > 0
|
|
129
|
+
? `\n\n## 附加截图\n用户附加了 ${screenshotCount} 张截图(截图内容无法在此展示)。请在 Issue 正文末尾添加提示:「报告者附加了 ${screenshotCount} 张截图,如需查看请联系报告者。」`
|
|
130
|
+
: '';
|
|
131
|
+
return `你是一个 Bug 报告助手,帮用户将 bug 描述整理为结构化的 GitHub Issue。
|
|
132
|
+
|
|
133
|
+
## 用户描述
|
|
134
|
+
${description}${screenshotNote}
|
|
135
|
+
|
|
136
|
+
## 系统信息
|
|
137
|
+
${sysInfoText}
|
|
138
|
+
|
|
139
|
+
## 最近日志(最后 50 行)
|
|
140
|
+
\`\`\`
|
|
141
|
+
${logs}
|
|
142
|
+
\`\`\`
|
|
143
|
+
|
|
144
|
+
请生成一个结构化的 GitHub Issue。输出**纯 JSON**(不要 markdown 代码块),包含两个字段:
|
|
145
|
+
- "title": 简洁的 issue 标题,格式为 "bug: 简要描述"(小写 bug: 前缀,不超过 80 字符)
|
|
146
|
+
- "body": 结构化的 Markdown 正文,严格按照以下模板:
|
|
147
|
+
|
|
148
|
+
## 用户现象
|
|
149
|
+
(从用户视角描述看到了什么、体验上有什么异常)
|
|
150
|
+
|
|
151
|
+
## 问题描述
|
|
152
|
+
(从技术视角简要说明发生了什么)
|
|
153
|
+
|
|
154
|
+
## 复现路径
|
|
155
|
+
1. 步骤一
|
|
156
|
+
2. 步骤二
|
|
157
|
+
3. 期望行为 vs 实际行为
|
|
158
|
+
(如果能从描述和日志推断出复现步骤就写,无法推断则省略此章节)
|
|
159
|
+
|
|
160
|
+
## 根因(可选)
|
|
161
|
+
(如果能从日志分析出代码层面的原因就写,否则省略)
|
|
162
|
+
|
|
163
|
+
## 影响
|
|
164
|
+
(对用户体验/数据/安全的影响)
|
|
165
|
+
|
|
166
|
+
## 环境信息
|
|
167
|
+
(系统信息表格)
|
|
168
|
+
|
|
169
|
+
## 相关日志
|
|
170
|
+
(如有错误日志,摘录关键部分)
|
|
171
|
+
|
|
172
|
+
只输出 JSON,不要其他内容。`;
|
|
173
|
+
}
|
|
174
|
+
function buildFallbackReport(description, systemInfo, logs) {
|
|
175
|
+
const sysInfoTable = Object.entries(systemInfo)
|
|
176
|
+
.map(([k, v]) => `| ${k} | ${v} |`)
|
|
177
|
+
.join('\n');
|
|
178
|
+
const body = `## 用户现象
|
|
179
|
+
|
|
180
|
+
${description}
|
|
181
|
+
|
|
182
|
+
## 问题描述
|
|
183
|
+
|
|
184
|
+
(待补充技术分析)
|
|
185
|
+
|
|
186
|
+
## 影响
|
|
187
|
+
|
|
188
|
+
(待补充)
|
|
189
|
+
|
|
190
|
+
## 环境信息
|
|
191
|
+
|
|
192
|
+
| 项目 | 值 |
|
|
193
|
+
|------|-----|
|
|
194
|
+
${sysInfoTable}
|
|
195
|
+
|
|
196
|
+
## 相关日志
|
|
197
|
+
|
|
198
|
+
\`\`\`
|
|
199
|
+
${logs.slice(0, 3000)}
|
|
200
|
+
\`\`\`
|
|
201
|
+
`;
|
|
202
|
+
return {
|
|
203
|
+
title: `bug: ${description.slice(0, 70)}`,
|
|
204
|
+
body,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/** Try multiple strategies to extract JSON { title, body } from Claude output */
|
|
208
|
+
function tryParseJsonOutput(raw) {
|
|
209
|
+
const candidates = [];
|
|
210
|
+
// Strategy 1: strip markdown fencing (greedy to handle nested backticks)
|
|
211
|
+
const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```\s*$/);
|
|
212
|
+
if (fenceMatch)
|
|
213
|
+
candidates.push(fenceMatch[1].trim());
|
|
214
|
+
// Strategy 2: extract first { ... } block
|
|
215
|
+
const braceMatch = raw.match(/\{[\s\S]*\}/);
|
|
216
|
+
if (braceMatch)
|
|
217
|
+
candidates.push(braceMatch[0]);
|
|
218
|
+
// Strategy 3: raw string as-is
|
|
219
|
+
candidates.push(raw.trim());
|
|
220
|
+
for (const candidate of candidates) {
|
|
221
|
+
try {
|
|
222
|
+
const parsed = JSON.parse(candidate);
|
|
223
|
+
if (typeof parsed === 'object' && parsed !== null && parsed.body) {
|
|
224
|
+
return parsed;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// try next candidate
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
// ========== Routes ==========
|
|
234
|
+
/**
|
|
235
|
+
* GET /api/bug-report/capabilities
|
|
236
|
+
* Check what tools are available for bug reporting
|
|
237
|
+
*/
|
|
238
|
+
bugReportRoutes.get('/capabilities', authMiddleware, async (c) => {
|
|
239
|
+
const caps = await checkCapabilities();
|
|
240
|
+
return c.json(caps);
|
|
241
|
+
});
|
|
242
|
+
/**
|
|
243
|
+
* POST /api/bug-report/generate
|
|
244
|
+
* Analyze the bug with Claude and generate a structured report
|
|
245
|
+
*/
|
|
246
|
+
bugReportRoutes.post('/generate', authMiddleware, async (c) => {
|
|
247
|
+
const user = c.get('user');
|
|
248
|
+
// Rate limiting — 30s cooldown per user for generate
|
|
249
|
+
const generateCooldownMsg = checkCooldown(user.id, generateCooldowns, GENERATE_COOLDOWN_MS);
|
|
250
|
+
if (generateCooldownMsg) {
|
|
251
|
+
return c.json({ error: generateCooldownMsg }, 429);
|
|
252
|
+
}
|
|
253
|
+
const parseResult = BugReportGenerateSchema.safeParse(await c.req.json());
|
|
254
|
+
if (!parseResult.success) {
|
|
255
|
+
return c.json({ error: 'Invalid request', details: parseResult.error.issues }, 400);
|
|
256
|
+
}
|
|
257
|
+
const { description, screenshots } = parseResult.data;
|
|
258
|
+
// Set cooldown immediately to prevent concurrent requests
|
|
259
|
+
generateCooldowns.set(user.id, Date.now());
|
|
260
|
+
// Collect system info
|
|
261
|
+
const homeGroup = getUserHomeGroup(user.id);
|
|
262
|
+
const folder = homeGroup?.folder || 'main';
|
|
263
|
+
const deps = getWebDeps();
|
|
264
|
+
const queueStatus = deps?.queue.getStatus();
|
|
265
|
+
const systemInfo = {
|
|
266
|
+
'cli-claw版本': getVersion(),
|
|
267
|
+
'Node.js': process.version,
|
|
268
|
+
操作系统: `${os.platform()} ${os.release()}`,
|
|
269
|
+
架构: os.arch(),
|
|
270
|
+
活跃容器数: String(queueStatus?.activeContainerCount ?? 'N/A'),
|
|
271
|
+
活跃宿主机进程: String(queueStatus?.activeHostProcessCount ?? 'N/A'),
|
|
272
|
+
等待队列: String(queueStatus?.waitingCount ?? 'N/A'),
|
|
273
|
+
截图数量: String(screenshots?.length || 0),
|
|
274
|
+
};
|
|
275
|
+
// Read recent logs
|
|
276
|
+
const rawLogs = readRecentLogs(folder);
|
|
277
|
+
const logs = sanitizeLogs(rawLogs);
|
|
278
|
+
// Try Claude analysis
|
|
279
|
+
const caps = await checkCapabilities();
|
|
280
|
+
if (!caps.claudeAvailable) {
|
|
281
|
+
logger.info('bug-report: claude CLI not available, using fallback template');
|
|
282
|
+
const fallback = buildFallbackReport(description, systemInfo, logs);
|
|
283
|
+
return c.json({ ...fallback, systemInfo });
|
|
284
|
+
}
|
|
285
|
+
const prompt = buildGeneratePrompt(description, systemInfo, logs, screenshots?.length || 0);
|
|
286
|
+
try {
|
|
287
|
+
logger.info({ promptLen: prompt.length, userId: user.id }, 'bug-report: invoking Claude SDK');
|
|
288
|
+
const model = process.env.RECALL_MODEL || undefined;
|
|
289
|
+
const result = await sdkQuery(prompt, { model, timeout: 60_000 });
|
|
290
|
+
if (!result) {
|
|
291
|
+
const fallback = buildFallbackReport(description, systemInfo, logs);
|
|
292
|
+
return c.json({ ...fallback, systemInfo });
|
|
293
|
+
}
|
|
294
|
+
// Try to parse Claude's JSON output
|
|
295
|
+
const parsed = tryParseJsonOutput(result);
|
|
296
|
+
if (parsed?.body) {
|
|
297
|
+
return c.json({
|
|
298
|
+
title: parsed.title || `Bug: ${description.slice(0, 70)}`,
|
|
299
|
+
body: parsed.body,
|
|
300
|
+
systemInfo,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// Claude didn't return valid JSON, use raw output as body
|
|
304
|
+
logger.info('bug-report: claude output was not valid JSON, using as raw body');
|
|
305
|
+
return c.json({
|
|
306
|
+
title: `Bug: ${description.slice(0, 70)}`,
|
|
307
|
+
body: result,
|
|
308
|
+
systemInfo,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
logger.error({ error: err.message }, 'bug-report: unexpected error during generation');
|
|
313
|
+
const fallback = buildFallbackReport(description, systemInfo, logs);
|
|
314
|
+
return c.json({ ...fallback, systemInfo });
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
/**
|
|
318
|
+
* POST /api/bug-report/submit
|
|
319
|
+
* Create a GitHub issue or return a pre-filled URL
|
|
320
|
+
*/
|
|
321
|
+
bugReportRoutes.post('/submit', authMiddleware, async (c) => {
|
|
322
|
+
const user = c.get('user');
|
|
323
|
+
// Rate limiting — only on actual issue submission
|
|
324
|
+
const cooldownMsg = checkCooldown(user.id);
|
|
325
|
+
if (cooldownMsg) {
|
|
326
|
+
return c.json({ error: cooldownMsg }, 429);
|
|
327
|
+
}
|
|
328
|
+
const parseResult = BugReportSubmitSchema.safeParse(await c.req.json());
|
|
329
|
+
if (!parseResult.success) {
|
|
330
|
+
return c.json({ error: 'Invalid request', details: parseResult.error.issues }, 400);
|
|
331
|
+
}
|
|
332
|
+
const { title, body } = parseResult.data;
|
|
333
|
+
// Append submitter info
|
|
334
|
+
const fullBody = `${body}\n\n---\n> Submitted via cli-claw by ${user.display_name || user.username}`;
|
|
335
|
+
// Try gh CLI first
|
|
336
|
+
const caps = await checkCapabilities();
|
|
337
|
+
if (caps.ghAvailable) {
|
|
338
|
+
try {
|
|
339
|
+
logger.info({ userId: user.id }, 'bug-report: attempting gh issue create');
|
|
340
|
+
const result = await new Promise((resolve, reject) => {
|
|
341
|
+
const child = execFile('gh', [
|
|
342
|
+
'issue',
|
|
343
|
+
'create',
|
|
344
|
+
'--repo',
|
|
345
|
+
'RyanProMax/cli-claw',
|
|
346
|
+
'--title',
|
|
347
|
+
title,
|
|
348
|
+
'--body-file',
|
|
349
|
+
'-',
|
|
350
|
+
], { timeout: 30000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
351
|
+
if (err) {
|
|
352
|
+
logger.warn({
|
|
353
|
+
message: err.message?.slice(0, 200),
|
|
354
|
+
stderr: stderr?.slice(0, 300),
|
|
355
|
+
}, 'bug-report: gh issue create failed');
|
|
356
|
+
reject(err);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
resolve(stdout.trim());
|
|
360
|
+
});
|
|
361
|
+
child.stdin?.write(fullBody);
|
|
362
|
+
child.stdin?.end();
|
|
363
|
+
});
|
|
364
|
+
// gh outputs the issue URL on success
|
|
365
|
+
const urlMatch = result.match(/https:\/\/github\.com\/[^\s]+\/issues\/\d+/);
|
|
366
|
+
const url = urlMatch ? urlMatch[0] : result;
|
|
367
|
+
logger.info({ url, userId: user.id }, 'bug-report: issue created via gh');
|
|
368
|
+
cooldowns.set(user.id, Date.now());
|
|
369
|
+
return c.json({ method: 'created', url });
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
// Fall through to manual URL
|
|
373
|
+
logger.info('bug-report: gh failed, falling back to manual URL');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Fallback: pre-filled GitHub URL
|
|
377
|
+
const maxBodyLen = 6000; // conservative limit for URL length
|
|
378
|
+
const truncatedBody = fullBody.length > maxBodyLen
|
|
379
|
+
? fullBody.slice(0, maxBodyLen) +
|
|
380
|
+
'\n\n...(内容过长已截断,请补充完整信息)'
|
|
381
|
+
: fullBody;
|
|
382
|
+
const url = `https://github.com/RyanProMax/cli-claw/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(truncatedBody)}`;
|
|
383
|
+
logger.info({ userId: user.id }, 'bug-report: returning pre-filled URL');
|
|
384
|
+
cooldowns.set(user.id, Date.now());
|
|
385
|
+
return c.json({ method: 'manual', url });
|
|
386
|
+
});
|
|
387
|
+
export default bugReportRoutes;
|