@aion0/forge 0.5.26 → 0.5.27
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/.forge/worktrees/pipeline-4dd8dc2d/CLAUDE.md +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/README.md +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/RELEASE_NOTES.md +36 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/agents/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/[...nextauth]/route.ts +3 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/verify/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/stream/route.ts +63 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/route.ts +28 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/entries/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/sync/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-templates/route.ts +145 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/code/route.ts +299 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/[id]/route.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/detect-cli/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/route.ts +176 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/sessions/route.ts +54 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/favorites/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/run/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/git/route.ts +149 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/help/route.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/issue-scanner/route.ts +116 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/logs/route.ts +100 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/mobile-chat/route.ts +115 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/monitor/route.ts +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notifications/route.ts +42 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notify/test/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/online/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/[id]/route.ts +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/route.ts +90 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/plugins/route.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/[...path]/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/route.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-pipelines/route.ts +91 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-sessions/route.ts +61 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/projects/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/chat/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/messages/route.ts +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/settings/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/local/route.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/route.ts +182 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/smith-templates/route.ts +81 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/status/route.ts +12 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tabs/route.ts +25 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/route.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/stream/route.ts +77 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/link/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/route.ts +44 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/session/route.ts +14 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/telegram/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/templates/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-bell/route.ts +39 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-cwd/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-state/route.ts +15 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tunnel/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/upgrade/route.ts +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/usage/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/version/route.ts +78 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/watchers/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/agents/route.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/memory/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/smith/route.ts +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/stream/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/route.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/global-error.tsx +21 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/globals.css +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.ico +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/layout.tsx +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/LoginForm.tsx +96 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/mobile/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/page.tsx +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/bin/forge-server.mjs +484 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/check-forge-status.sh +71 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/cli/mw.ts +579 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/BrowserPanel.tsx +175 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ChatPanel.tsx +191 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ClaudeTerminal.tsx +267 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/CodeViewer.tsx +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationEditor.tsx +411 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationGraphView.tsx +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationTerminalView.tsx +303 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/Dashboard.tsx +807 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DashboardWrapper.tsx +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryFlowEditor.tsx +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryList.tsx +230 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryWorkspace.tsx +589 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocTerminal.tsx +187 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocsViewer.tsx +574 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpDialog.tsx +169 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpTerminal.tsx +141 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/InlinePipelineView.tsx +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/LogViewer.tsx +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MarkdownContent.tsx +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MobileView.tsx +385 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MonitorPanel.tsx +122 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewSessionModal.tsx +93 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewTaskModal.tsx +492 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineEditor.tsx +570 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineView.tsx +1018 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PluginsPanel.tsx +472 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectDetail.tsx +1618 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectList.tsx +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectManager.tsx +401 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionList.tsx +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionView.tsx +726 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SettingsModal.tsx +1647 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SkillsPanel.tsx +969 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/StatusBar.tsx +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TabBar.tsx +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskBoard.tsx +113 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskDetail.tsx +372 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TerminalLauncher.tsx +398 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TunnelToggle.tsx +206 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/UsagePanel.tsx +207 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WebTerminal.tsx +1743 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceTree.tsx +221 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceView.tsx +4048 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/dev-test.sh +5 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Memory_Layer_Design.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Strategy_Research_2026.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/LOCAL-DEPLOY.md +144 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/roadmap-multi-agent-workflow.md +330 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/hooks/useSidebarResize.ts +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/install.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/instrumentation.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/claude-adapter.ts +104 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/generic-adapter.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/index.ts +245 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/types.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/artifacts.ts +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/auth.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/docker.yaml +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/http.yaml +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/jenkins.yaml +92 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/llm-vision.yaml +85 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/playwright.yaml +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/shell-command.yaml +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/slack.yaml +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/webhook.yaml +56 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-process.ts +361 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-sessions.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-templates.ts +227 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/cloudflared.ts +424 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/crypto.ts +67 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/delivery.ts +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/dirs.ts +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/flows.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-mcp-server.ts +732 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-inbox.md +38 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-send.md +47 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-status.md +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/00-overview.md +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/02-telegram.md +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/03-tunnel.md +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/04-tasks.md +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/05-pipelines.md +460 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/06-skills.md +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/08-rules.md +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/09-issue-autofix.md +55 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/10-troubleshooting.md +89 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/11-workspace.md +810 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/CLAUDE.md +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/init.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/issue-scanner.ts +298 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/logger.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notifications.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notify.ts +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/password.ts +97 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline-scheduler.ts +373 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline.ts +1565 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/executor.ts +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/registry.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/types.ts +103 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/project-sessions.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/projects.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-manager.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-utils.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-watcher.ts +345 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/settings.ts +195 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/skills.ts +458 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/task-manager.ts +951 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-bot.ts +1477 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-standalone.ts +83 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-server.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-standalone.ts +438 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/usage-scanner.ts +249 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-bus.ts +416 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-worker.ts +655 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/api-backend.ts +262 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/cli-backend.ts +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/index.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/manager.ts +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/orchestrator.ts +3415 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/persistence.ts +309 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/presets.ts +649 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/requests.ts +287 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/session-monitor.ts +240 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/skill-installer.ts +275 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/smith-memory.ts +498 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/types.ts +241 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/watch-manager.ts +560 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace-standalone.ts +978 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/middleware.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/next.config.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/package.json +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-lock.yaml +3719 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-workspace.yaml +1 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/postcss.config.mjs +7 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/publish.sh +133 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/README.md +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/results/.gitignore +2 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/run.ts +635 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/task.md +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/validator.sh +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/setup.sh +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/task.md +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/validator.sh +69 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/setup.sh +82 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/task.md +30 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/validator.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/verify-usage.ts +178 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/config/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/db/database.ts +259 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/memory/strategy.ts +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/chat.ts +65 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/registry.ts +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/session/manager.ts +190 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/types/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/start.sh +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/templates/smith-lead.json +45 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/tsconfig.json +42 -0
- package/RELEASE_NOTES.md +11 -28
- package/app/api/terminal-bell/route.ts +6 -2
- package/components/WebTerminal.tsx +36 -2
- package/lib/terminal-standalone.ts +19 -2
- package/next-env.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
import { encryptSecret, decryptSecret, isEncrypted, SECRET_FIELDS } from './crypto';
|
|
5
|
+
import { getDataDir } from './dirs';
|
|
6
|
+
|
|
7
|
+
const DATA_DIR = getDataDir();
|
|
8
|
+
const SETTINGS_FILE = join(DATA_DIR, 'settings.yaml');
|
|
9
|
+
|
|
10
|
+
export interface AgentEntry {
|
|
11
|
+
// Base agent fields (for detected agents like claude, codex, aider)
|
|
12
|
+
path?: string; name?: string; enabled?: boolean;
|
|
13
|
+
flags?: string[]; taskFlags?: string; interactiveCmd?: string; resumeFlag?: string; outputFormat?: string;
|
|
14
|
+
models?: { terminal?: string; task?: string; telegram?: string; help?: string; mobile?: string };
|
|
15
|
+
skipPermissionsFlag?: string;
|
|
16
|
+
requiresTTY?: boolean;
|
|
17
|
+
// Profile fields (for profiles that extend a base agent)
|
|
18
|
+
base?: string; // base agent ID (e.g., 'claude') — makes this a profile
|
|
19
|
+
// API profile fields
|
|
20
|
+
type?: 'cli' | 'api'; // 'api' = API mode, default = 'cli'
|
|
21
|
+
provider?: string; // API provider (e.g., 'anthropic', 'google')
|
|
22
|
+
model?: string; // model override (for both CLI and API profiles)
|
|
23
|
+
apiKey?: string; // per-profile API key (encrypted)
|
|
24
|
+
env?: Record<string, string>; // environment variables injected when spawning CLI
|
|
25
|
+
cliType?: 'claude-code' | 'codex' | 'aider' | 'generic'; // CLI tool type — determines session support, resume flags, etc.
|
|
26
|
+
profile?: string; // linked profile ID — overrides model, env, etc. when launching
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ProviderEntry {
|
|
30
|
+
apiKey?: string; // encrypted, fallback to env var
|
|
31
|
+
defaultModel?: string;
|
|
32
|
+
enabled?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Settings {
|
|
36
|
+
projectRoots: string[];
|
|
37
|
+
docRoots: string[];
|
|
38
|
+
claudePath: string;
|
|
39
|
+
claudeHome: string;
|
|
40
|
+
telegramBotToken: string;
|
|
41
|
+
telegramChatId: string;
|
|
42
|
+
notifyOnComplete: boolean;
|
|
43
|
+
notifyOnFailure: boolean;
|
|
44
|
+
tunnelAutoStart: boolean;
|
|
45
|
+
telegramTunnelPassword: string;
|
|
46
|
+
taskModel: string;
|
|
47
|
+
pipelineModel: string;
|
|
48
|
+
telegramModel: string;
|
|
49
|
+
skipPermissions: boolean;
|
|
50
|
+
notificationRetentionDays: number;
|
|
51
|
+
skillsRepoUrl: string;
|
|
52
|
+
displayName: string;
|
|
53
|
+
displayEmail: string;
|
|
54
|
+
favoriteProjects: string[];
|
|
55
|
+
defaultAgent: string;
|
|
56
|
+
telegramAgent: string;
|
|
57
|
+
docsAgent: string;
|
|
58
|
+
agents: Record<string, AgentEntry>;
|
|
59
|
+
providers: Record<string, ProviderEntry>; // API provider configs
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const defaults: Settings = {
|
|
63
|
+
projectRoots: [],
|
|
64
|
+
docRoots: [],
|
|
65
|
+
claudePath: '',
|
|
66
|
+
claudeHome: '',
|
|
67
|
+
telegramBotToken: '',
|
|
68
|
+
telegramChatId: '',
|
|
69
|
+
notifyOnComplete: true,
|
|
70
|
+
notifyOnFailure: true,
|
|
71
|
+
tunnelAutoStart: false,
|
|
72
|
+
telegramTunnelPassword: '',
|
|
73
|
+
taskModel: 'default',
|
|
74
|
+
pipelineModel: 'default',
|
|
75
|
+
telegramModel: 'sonnet',
|
|
76
|
+
skipPermissions: false,
|
|
77
|
+
notificationRetentionDays: 30,
|
|
78
|
+
skillsRepoUrl: 'https://raw.githubusercontent.com/aiwatching/forge-skills/main',
|
|
79
|
+
displayName: 'Forge',
|
|
80
|
+
displayEmail: '',
|
|
81
|
+
favoriteProjects: [],
|
|
82
|
+
defaultAgent: 'claude',
|
|
83
|
+
telegramAgent: '',
|
|
84
|
+
docsAgent: '',
|
|
85
|
+
agents: {},
|
|
86
|
+
providers: {},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Decrypt nested apiKey fields in agents and providers */
|
|
90
|
+
function decryptNestedSecrets(settings: Settings): void {
|
|
91
|
+
// Decrypt provider apiKeys
|
|
92
|
+
if (settings.providers) {
|
|
93
|
+
for (const p of Object.values(settings.providers)) {
|
|
94
|
+
if (p.apiKey && isEncrypted(p.apiKey)) {
|
|
95
|
+
p.apiKey = decryptSecret(p.apiKey);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Decrypt agent profile apiKeys
|
|
100
|
+
if (settings.agents) {
|
|
101
|
+
for (const a of Object.values(settings.agents)) {
|
|
102
|
+
if (a.apiKey && isEncrypted(a.apiKey)) {
|
|
103
|
+
a.apiKey = decryptSecret(a.apiKey);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Encrypt nested apiKey fields in agents and providers */
|
|
110
|
+
function encryptNestedSecrets(settings: Settings): void {
|
|
111
|
+
if (settings.providers) {
|
|
112
|
+
for (const p of Object.values(settings.providers)) {
|
|
113
|
+
if (p.apiKey && !isEncrypted(p.apiKey)) {
|
|
114
|
+
p.apiKey = encryptSecret(p.apiKey);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (settings.agents) {
|
|
119
|
+
for (const a of Object.values(settings.agents)) {
|
|
120
|
+
if (a.apiKey && !isEncrypted(a.apiKey)) {
|
|
121
|
+
a.apiKey = encryptSecret(a.apiKey);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Load settings with secrets decrypted (for internal use) */
|
|
128
|
+
export function loadSettings(): Settings {
|
|
129
|
+
if (!existsSync(SETTINGS_FILE)) return { ...defaults };
|
|
130
|
+
try {
|
|
131
|
+
const raw = readFileSync(SETTINGS_FILE, 'utf-8');
|
|
132
|
+
const parsed = { ...defaults, ...YAML.parse(raw) };
|
|
133
|
+
// Decrypt top-level secret fields
|
|
134
|
+
for (const field of SECRET_FIELDS) {
|
|
135
|
+
if (parsed[field] && isEncrypted(parsed[field])) {
|
|
136
|
+
parsed[field] = decryptSecret(parsed[field]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Decrypt nested apiKeys
|
|
140
|
+
decryptNestedSecrets(parsed);
|
|
141
|
+
return parsed;
|
|
142
|
+
} catch {
|
|
143
|
+
return { ...defaults };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Load settings with secrets masked (for API response to frontend) */
|
|
148
|
+
export function loadSettingsMasked(): Settings & { _secretStatus: Record<string, boolean> } {
|
|
149
|
+
const settings = loadSettings();
|
|
150
|
+
const status: Record<string, boolean> = {};
|
|
151
|
+
for (const field of SECRET_FIELDS) {
|
|
152
|
+
status[field] = !!settings[field];
|
|
153
|
+
settings[field] = settings[field] ? '••••••••' : '';
|
|
154
|
+
}
|
|
155
|
+
// Mask nested apiKeys
|
|
156
|
+
if (settings.providers) {
|
|
157
|
+
for (const [name, p] of Object.entries(settings.providers)) {
|
|
158
|
+
status[`providers.${name}.apiKey`] = !!p.apiKey;
|
|
159
|
+
p.apiKey = p.apiKey ? '••••••••' : '';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (settings.agents) {
|
|
163
|
+
for (const [name, a] of Object.entries(settings.agents)) {
|
|
164
|
+
if (a.apiKey) {
|
|
165
|
+
status[`agents.${name}.apiKey`] = true;
|
|
166
|
+
a.apiKey = '••••••••';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { ...settings, _secretStatus: status };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Save settings, encrypting secret fields */
|
|
174
|
+
export function saveSettings(settings: Settings) {
|
|
175
|
+
const dir = dirname(SETTINGS_FILE);
|
|
176
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
177
|
+
// Deep copy to avoid mutating original
|
|
178
|
+
const toSave = JSON.parse(JSON.stringify(settings));
|
|
179
|
+
// Encrypt top-level secret fields
|
|
180
|
+
for (const field of SECRET_FIELDS) {
|
|
181
|
+
if (toSave[field] && !isEncrypted(toSave[field])) {
|
|
182
|
+
toSave[field] = encryptSecret(toSave[field]);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Encrypt nested apiKeys
|
|
186
|
+
encryptNestedSecrets(toSave);
|
|
187
|
+
writeFileSync(SETTINGS_FILE, YAML.stringify(toSave), 'utf-8');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Verify a secret field's current value */
|
|
191
|
+
export function verifySecret(field: string, value: string): boolean {
|
|
192
|
+
const settings = loadSettings();
|
|
193
|
+
const current = (settings as any)[field] || '';
|
|
194
|
+
return current === value;
|
|
195
|
+
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Marketplace — sync, install, uninstall from remote registry.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync, rmSync, cpSync } from 'node:fs';
|
|
6
|
+
import { join, dirname, basename, relative, sep } from 'node:path';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { getDb } from '@/src/core/db/database';
|
|
9
|
+
import { getDbPath } from '@/src/config';
|
|
10
|
+
import { loadSettings } from './settings';
|
|
11
|
+
|
|
12
|
+
type ItemType = 'skill' | 'command';
|
|
13
|
+
|
|
14
|
+
interface SkillItem {
|
|
15
|
+
name: string;
|
|
16
|
+
type: ItemType;
|
|
17
|
+
displayName: string;
|
|
18
|
+
description: string;
|
|
19
|
+
author: string;
|
|
20
|
+
version: string;
|
|
21
|
+
tags: string[];
|
|
22
|
+
score: number;
|
|
23
|
+
rating: number;
|
|
24
|
+
sourceUrl: string;
|
|
25
|
+
installedGlobal: boolean;
|
|
26
|
+
installedProjects: string[];
|
|
27
|
+
installedVersion: string;
|
|
28
|
+
hasUpdate: boolean;
|
|
29
|
+
deletedRemotely: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function db() {
|
|
33
|
+
return getDb(getDbPath());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getBaseUrl(): string {
|
|
37
|
+
const settings = loadSettings();
|
|
38
|
+
return settings.skillsRepoUrl || 'https://raw.githubusercontent.com/aiwatching/forge-skills/main';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getRepoInfo(): { owner: string; repo: string; branch: string } {
|
|
42
|
+
const url = getBaseUrl();
|
|
43
|
+
const match = url.match(/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+))?/) ||
|
|
44
|
+
url.match(/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)/);
|
|
45
|
+
if (match) return { owner: match[1], repo: match[2], branch: match[3] || 'main' };
|
|
46
|
+
return { owner: 'aiwatching', repo: 'forge-skills', branch: 'main' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function compareVersions(a: string, b: string): number {
|
|
50
|
+
const pa = (a || '0.0.0').split('.').map(Number);
|
|
51
|
+
const pb = (b || '0.0.0').split('.').map(Number);
|
|
52
|
+
for (let i = 0; i < 3; i++) {
|
|
53
|
+
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
54
|
+
if (diff !== 0) return diff;
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Sync ─────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/** Max info.json enrichments per sync (incremental) */
|
|
62
|
+
const ENRICH_BATCH_SIZE = 10;
|
|
63
|
+
|
|
64
|
+
export async function syncSkills(): Promise<{ synced: number; enriched: number; total?: number; remaining?: number; error?: string }> {
|
|
65
|
+
console.log('[skills] Syncing from registry...');
|
|
66
|
+
const baseUrl = getBaseUrl();
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Step 1: Fetch registry.json (always fresh)
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
72
|
+
const cacheBust = `_t=${Date.now()}`;
|
|
73
|
+
const res = await fetch(`${baseUrl}/registry.json?${cacheBust}`, {
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' },
|
|
76
|
+
});
|
|
77
|
+
clearTimeout(timeout);
|
|
78
|
+
|
|
79
|
+
if (!res.ok) return { synced: 0, enriched: 0, error: `Registry fetch failed: ${res.status}` };
|
|
80
|
+
|
|
81
|
+
const data = await res.json();
|
|
82
|
+
|
|
83
|
+
// Parse registry items (v1 + v2 support)
|
|
84
|
+
let rawItems: any[] = [];
|
|
85
|
+
if (data.version === 2) {
|
|
86
|
+
rawItems = [
|
|
87
|
+
...(data.skills || []).map((s: any) => ({ ...s, type: s.type || 'skill' })),
|
|
88
|
+
...(data.commands || []).map((c: any) => ({ ...c, type: c.type || 'command' })),
|
|
89
|
+
];
|
|
90
|
+
} else {
|
|
91
|
+
rawItems = (data.skills || []).map((s: any) => ({ ...s, type: s.type || 'command' }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Step 2: Upsert all items from registry.json directly (fast, no extra fetch)
|
|
95
|
+
const upsertStmt = db().prepare(`
|
|
96
|
+
INSERT OR REPLACE INTO skills (name, type, display_name, description, author, version, tags, score, rating, source_url, archive, synced_at,
|
|
97
|
+
installed_global, installed_projects, installed_version)
|
|
98
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
99
|
+
COALESCE((SELECT synced_at FROM skills WHERE name = ?), datetime('now')),
|
|
100
|
+
COALESCE((SELECT installed_global FROM skills WHERE name = ?), 0),
|
|
101
|
+
COALESCE((SELECT installed_projects FROM skills WHERE name = ?), '[]'),
|
|
102
|
+
COALESCE((SELECT installed_version FROM skills WHERE name = ?), ''))
|
|
103
|
+
`);
|
|
104
|
+
|
|
105
|
+
const tx = db().transaction(() => {
|
|
106
|
+
for (const s of rawItems) {
|
|
107
|
+
upsertStmt.run(
|
|
108
|
+
s.name || '', s.type || 'skill',
|
|
109
|
+
s.display_name || '', s.description || '',
|
|
110
|
+
(s.author?.name || s.author || '').toString(), s.version || '',
|
|
111
|
+
JSON.stringify(s.tags || []),
|
|
112
|
+
s.score ?? 0, s.rating ?? 0, s.source?.url || s.source_url || '',
|
|
113
|
+
'', // archive
|
|
114
|
+
s.name || '', s.name || '', s.name || '', s.name || ''
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
tx();
|
|
119
|
+
|
|
120
|
+
// Step 3: Handle items no longer in registry
|
|
121
|
+
const registryNames = new Set(rawItems.map((s: any) => s.name));
|
|
122
|
+
const dbItems = db().prepare('SELECT name, installed_global, installed_projects FROM skills').all() as any[];
|
|
123
|
+
for (const row of dbItems) {
|
|
124
|
+
if (!registryNames.has(row.name)) {
|
|
125
|
+
const hasLocal = !!row.installed_global || JSON.parse(row.installed_projects || '[]').length > 0;
|
|
126
|
+
if (hasLocal) {
|
|
127
|
+
db().prepare('UPDATE skills SET deleted_remotely = 1 WHERE name = ?').run(row.name);
|
|
128
|
+
} else {
|
|
129
|
+
db().prepare('DELETE FROM skills WHERE name = ?').run(row.name);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Step 4: Incremental enrichment — fetch info.json for oldest-synced items
|
|
135
|
+
// Pick items whose synced_at is oldest (or version changed since last enrich)
|
|
136
|
+
const staleItems = db().prepare(`
|
|
137
|
+
SELECT name, type, version FROM skills
|
|
138
|
+
WHERE deleted_remotely = 0
|
|
139
|
+
ORDER BY synced_at ASC
|
|
140
|
+
LIMIT ?
|
|
141
|
+
`).all(ENRICH_BATCH_SIZE) as any[];
|
|
142
|
+
|
|
143
|
+
let enriched = 0;
|
|
144
|
+
const enrichStmt = db().prepare(`
|
|
145
|
+
UPDATE skills SET
|
|
146
|
+
version = COALESCE(?, version),
|
|
147
|
+
tags = COALESCE(?, tags),
|
|
148
|
+
score = COALESCE(?, score),
|
|
149
|
+
rating = COALESCE(?, rating),
|
|
150
|
+
description = COALESCE(?, description),
|
|
151
|
+
synced_at = datetime('now')
|
|
152
|
+
WHERE name = ?
|
|
153
|
+
`);
|
|
154
|
+
|
|
155
|
+
await Promise.all(staleItems.map(async (s: any) => {
|
|
156
|
+
try {
|
|
157
|
+
const repoDir = s.type === 'skill' ? 'skills' : 'commands';
|
|
158
|
+
let infoRes = await fetch(`${baseUrl}/${repoDir}/${s.name}/info.json?${cacheBust}`, { signal: AbortSignal.timeout(5000) });
|
|
159
|
+
if (!infoRes.ok) {
|
|
160
|
+
const altDir = s.type === 'skill' ? 'commands' : 'skills';
|
|
161
|
+
infoRes = await fetch(`${baseUrl}/${altDir}/${s.name}/info.json?${cacheBust}`, { signal: AbortSignal.timeout(5000) });
|
|
162
|
+
}
|
|
163
|
+
if (infoRes.ok) {
|
|
164
|
+
const info = await infoRes.json();
|
|
165
|
+
enrichStmt.run(
|
|
166
|
+
info.version || null,
|
|
167
|
+
info.tags?.length ? JSON.stringify(info.tags) : null,
|
|
168
|
+
info.score ?? null,
|
|
169
|
+
info.rating ?? null,
|
|
170
|
+
info.description || null,
|
|
171
|
+
s.name
|
|
172
|
+
);
|
|
173
|
+
enriched++;
|
|
174
|
+
} else {
|
|
175
|
+
// No info.json — just update synced_at so it rotates to the back
|
|
176
|
+
db().prepare('UPDATE skills SET synced_at = datetime(\'now\') WHERE name = ?').run(s.name);
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
// Timeout/error — update synced_at to avoid retrying immediately
|
|
180
|
+
db().prepare('UPDATE skills SET synced_at = datetime(\'now\') WHERE name = ?').run(s.name);
|
|
181
|
+
}
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
const totalCount = (db().prepare('SELECT count(*) as c FROM skills WHERE deleted_remotely = 0').get() as any).c;
|
|
185
|
+
const remaining = totalCount - ENRICH_BATCH_SIZE; // approximate items not yet enriched this round
|
|
186
|
+
console.log(`[skills] Synced ${rawItems.length} items, enriched ${enriched}/${staleItems.length} from info.json`);
|
|
187
|
+
return { synced: rawItems.length, enriched, total: totalCount, remaining: Math.max(0, remaining) };
|
|
188
|
+
} catch (e) {
|
|
189
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
190
|
+
console.error(`[skills] Sync failed:`, msg);
|
|
191
|
+
return { synced: 0, enriched: 0, error: msg };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── List ────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
export function listSkills(): SkillItem[] {
|
|
198
|
+
const rows = db().prepare('SELECT * FROM skills ORDER BY type ASC, score DESC, display_name ASC').all() as any[];
|
|
199
|
+
return rows.map(r => {
|
|
200
|
+
const installedVersion = r.installed_version || '';
|
|
201
|
+
const registryVersion = r.version || '';
|
|
202
|
+
const isInstalled = !!r.installed_global || JSON.parse(r.installed_projects || '[]').length > 0;
|
|
203
|
+
return {
|
|
204
|
+
name: r.name,
|
|
205
|
+
type: r.type || 'skill',
|
|
206
|
+
displayName: r.display_name,
|
|
207
|
+
description: r.description,
|
|
208
|
+
author: r.author,
|
|
209
|
+
version: registryVersion,
|
|
210
|
+
tags: JSON.parse(r.tags || '[]'),
|
|
211
|
+
score: r.score,
|
|
212
|
+
rating: r.rating || 0,
|
|
213
|
+
sourceUrl: r.source_url,
|
|
214
|
+
installedGlobal: !!r.installed_global,
|
|
215
|
+
installedProjects: JSON.parse(r.installed_projects || '[]'),
|
|
216
|
+
installedVersion,
|
|
217
|
+
hasUpdate: isInstalled && !!registryVersion && !!installedVersion && compareVersions(registryVersion, installedVersion) > 0,
|
|
218
|
+
deletedRemotely: !!r.deleted_remotely,
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ─── Download directory from GitHub ──────────────────────────
|
|
224
|
+
|
|
225
|
+
/** Recursively list all files in a skill/command directory via GitHub API */
|
|
226
|
+
async function listRepoFiles(name: string, type: ItemType): Promise<{ path: string; download_url: string }[]> {
|
|
227
|
+
const repo = getRepoInfo();
|
|
228
|
+
const files: { path: string; download_url: string }[] = [];
|
|
229
|
+
|
|
230
|
+
async function recurse(apiUrl: string, prefix: string) {
|
|
231
|
+
const res = await fetch(apiUrl, {
|
|
232
|
+
headers: { 'Accept': 'application/vnd.github.v3+json' },
|
|
233
|
+
});
|
|
234
|
+
if (!res.ok) return;
|
|
235
|
+
const items = await res.json();
|
|
236
|
+
if (!Array.isArray(items)) return;
|
|
237
|
+
for (const item of items) {
|
|
238
|
+
if (item.type === 'file' && item.download_url) {
|
|
239
|
+
files.push({ path: join(prefix, item.name), download_url: item.download_url });
|
|
240
|
+
} else if (item.type === 'dir') {
|
|
241
|
+
await recurse(item.url, join(prefix, item.name));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Try skills/ first, then commands/
|
|
247
|
+
const dirs = type === 'skill' ? ['skills', 'commands'] : ['commands', 'skills'];
|
|
248
|
+
for (const dir of dirs) {
|
|
249
|
+
const url = `https://api.github.com/repos/${repo.owner}/${repo.repo}/contents/${dir}/${name}?ref=${repo.branch}`;
|
|
250
|
+
await recurse(url, '');
|
|
251
|
+
if (files.length > 0) return files;
|
|
252
|
+
}
|
|
253
|
+
return files;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function downloadFile(url: string): Promise<string> {
|
|
257
|
+
const res = await fetch(url);
|
|
258
|
+
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
259
|
+
return res.text();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ─── Install ─────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
function getClaudeHome(): string {
|
|
265
|
+
const settings = loadSettings();
|
|
266
|
+
return settings.claudeHome || join(homedir(), '.claude');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function getSkillDir(name: string, type: ItemType, projectPath?: string): string {
|
|
270
|
+
const base = projectPath || getClaudeHome();
|
|
271
|
+
const subdir = type === 'skill' ? 'skills' : 'commands';
|
|
272
|
+
return join(base, '.claude', subdir, name);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function installGlobal(name: string): Promise<void> {
|
|
276
|
+
const skill = db().prepare('SELECT * FROM skills WHERE name = ?').get(name) as any;
|
|
277
|
+
if (!skill) throw new Error(`Skill "${name}" not found`);
|
|
278
|
+
|
|
279
|
+
const type: ItemType = skill.type || 'skill';
|
|
280
|
+
const claudeHome = getClaudeHome();
|
|
281
|
+
const subdir = type === 'skill' ? 'skills' : 'commands';
|
|
282
|
+
const targetDir = join(claudeHome, subdir, name);
|
|
283
|
+
|
|
284
|
+
const files = await listRepoFiles(name, type);
|
|
285
|
+
if (files.length === 0) throw new Error(`No files found for ${name}`);
|
|
286
|
+
|
|
287
|
+
mkdirSync(targetDir, { recursive: true });
|
|
288
|
+
for (const f of files) {
|
|
289
|
+
const content = await downloadFile(f.download_url);
|
|
290
|
+
const targetPath = join(targetDir, f.path);
|
|
291
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
292
|
+
writeFileSync(targetPath, content);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Update installed state
|
|
296
|
+
db().prepare('UPDATE skills SET installed_global = 1, installed_version = ? WHERE name = ?')
|
|
297
|
+
.run(skill.version || '', name);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function installProject(name: string, projectPath: string): Promise<void> {
|
|
301
|
+
const skill = db().prepare('SELECT * FROM skills WHERE name = ?').get(name) as any;
|
|
302
|
+
if (!skill) throw new Error(`Skill "${name}" not found`);
|
|
303
|
+
|
|
304
|
+
const type: ItemType = skill.type || 'skill';
|
|
305
|
+
const subdir = type === 'skill' ? 'skills' : 'commands';
|
|
306
|
+
const targetDir = join(projectPath, '.claude', subdir, name);
|
|
307
|
+
|
|
308
|
+
const files = await listRepoFiles(name, type);
|
|
309
|
+
if (files.length === 0) throw new Error(`No files found for ${name}`);
|
|
310
|
+
|
|
311
|
+
mkdirSync(targetDir, { recursive: true });
|
|
312
|
+
for (const f of files) {
|
|
313
|
+
const content = await downloadFile(f.download_url);
|
|
314
|
+
const targetPath = join(targetDir, f.path);
|
|
315
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
316
|
+
writeFileSync(targetPath, content);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Update installed state
|
|
320
|
+
const existing = JSON.parse(skill.installed_projects || '[]');
|
|
321
|
+
if (!existing.includes(projectPath)) existing.push(projectPath);
|
|
322
|
+
db().prepare('UPDATE skills SET installed_projects = ?, installed_version = ? WHERE name = ?')
|
|
323
|
+
.run(JSON.stringify(existing), skill.version || '', name);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ─── Uninstall ───────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
export function uninstallGlobal(name: string): void {
|
|
329
|
+
const skill = db().prepare('SELECT * FROM skills WHERE name = ?').get(name) as any;
|
|
330
|
+
if (!skill) return;
|
|
331
|
+
|
|
332
|
+
const type: ItemType = skill.type || 'skill';
|
|
333
|
+
const claudeHome = getClaudeHome();
|
|
334
|
+
const subdir = type === 'skill' ? 'skills' : 'commands';
|
|
335
|
+
const targetDir = join(claudeHome, subdir, name);
|
|
336
|
+
|
|
337
|
+
if (existsSync(targetDir)) rmSync(targetDir, { recursive: true, force: true });
|
|
338
|
+
|
|
339
|
+
db().prepare('UPDATE skills SET installed_global = 0 WHERE name = ?').run(name);
|
|
340
|
+
// Clear installed_version if no project installs remain
|
|
341
|
+
const remaining = JSON.parse(skill.installed_projects || '[]');
|
|
342
|
+
if (remaining.length === 0) {
|
|
343
|
+
db().prepare('UPDATE skills SET installed_version = ? WHERE name = ?').run('', name);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function uninstallProject(name: string, projectPath: string): void {
|
|
348
|
+
const skill = db().prepare('SELECT * FROM skills WHERE name = ?').get(name) as any;
|
|
349
|
+
if (!skill) return;
|
|
350
|
+
|
|
351
|
+
const type: ItemType = skill.type || 'skill';
|
|
352
|
+
const subdir = type === 'skill' ? 'skills' : 'commands';
|
|
353
|
+
const targetDir = join(projectPath, '.claude', subdir, name);
|
|
354
|
+
|
|
355
|
+
if (existsSync(targetDir)) rmSync(targetDir, { recursive: true, force: true });
|
|
356
|
+
|
|
357
|
+
const existing = JSON.parse(skill.installed_projects || '[]').filter((p: string) => p !== projectPath);
|
|
358
|
+
db().prepare('UPDATE skills SET installed_projects = ? WHERE name = ?')
|
|
359
|
+
.run(JSON.stringify(existing), name);
|
|
360
|
+
// Clear installed_version if nothing remains
|
|
361
|
+
if (!skill.installed_global && existing.length === 0) {
|
|
362
|
+
db().prepare('UPDATE skills SET installed_version = ? WHERE name = ?').run('', name);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ─── Refresh install state from filesystem ───────────────────
|
|
367
|
+
|
|
368
|
+
export function refreshInstallState(projectPaths: string[]): void {
|
|
369
|
+
const claudeHome = getClaudeHome();
|
|
370
|
+
const rows = db().prepare('SELECT name, type FROM skills').all() as any[];
|
|
371
|
+
|
|
372
|
+
for (const row of rows) {
|
|
373
|
+
const type: ItemType = row.type || 'skill';
|
|
374
|
+
const subdir = type === 'skill' ? 'skills' : 'commands';
|
|
375
|
+
|
|
376
|
+
// Check global
|
|
377
|
+
const globalDir = join(claudeHome, subdir, row.name);
|
|
378
|
+
const globalInstalled = existsSync(globalDir);
|
|
379
|
+
|
|
380
|
+
// Check projects
|
|
381
|
+
const installedIn: string[] = [];
|
|
382
|
+
for (const pp of projectPaths) {
|
|
383
|
+
const projDir = join(pp, '.claude', subdir, row.name);
|
|
384
|
+
if (existsSync(projDir)) installedIn.push(pp);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Read installed version from info.json if available
|
|
388
|
+
let installedVersion = '';
|
|
389
|
+
const checkDirs = globalInstalled ? [globalDir] : installedIn.length > 0 ? [join(installedIn[0], '.claude', subdir, row.name)] : [];
|
|
390
|
+
for (const d of checkDirs) {
|
|
391
|
+
const infoPath = join(d, 'info.json');
|
|
392
|
+
if (existsSync(infoPath)) {
|
|
393
|
+
try {
|
|
394
|
+
const info = JSON.parse(readFileSync(infoPath, 'utf-8'));
|
|
395
|
+
installedVersion = info.version || '';
|
|
396
|
+
} catch {}
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
db().prepare('UPDATE skills SET installed_global = ?, installed_projects = ?, installed_version = ? WHERE name = ?')
|
|
402
|
+
.run(globalInstalled ? 1 : 0, JSON.stringify(installedIn), installedVersion, row.name);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ─── Check local modifications ───────────────────────────────
|
|
407
|
+
|
|
408
|
+
export async function checkLocalModified(name: string): Promise<boolean> {
|
|
409
|
+
const skill = db().prepare('SELECT * FROM skills WHERE name = ?').get(name) as any;
|
|
410
|
+
if (!skill) return false;
|
|
411
|
+
|
|
412
|
+
const type: ItemType = skill.type || 'skill';
|
|
413
|
+
const claudeHome = getClaudeHome();
|
|
414
|
+
const subdir = type === 'skill' ? 'skills' : 'commands';
|
|
415
|
+
const localDir = join(claudeHome, subdir, name);
|
|
416
|
+
|
|
417
|
+
if (!existsSync(localDir)) return false;
|
|
418
|
+
|
|
419
|
+
// Compare with remote files
|
|
420
|
+
try {
|
|
421
|
+
const remoteFiles = await listRepoFiles(name, type);
|
|
422
|
+
for (const rf of remoteFiles) {
|
|
423
|
+
const localPath = join(localDir, rf.path);
|
|
424
|
+
if (!existsSync(localPath)) return true;
|
|
425
|
+
const localContent = readFileSync(localPath, 'utf-8');
|
|
426
|
+
const remoteContent = await downloadFile(rf.download_url);
|
|
427
|
+
if (localContent !== remoteContent) return true;
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ─── Purge deleted skill ─────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
export function purgeDeletedSkill(name: string): void {
|
|
439
|
+
const skill = db().prepare('SELECT * FROM skills WHERE name = ?').get(name) as any;
|
|
440
|
+
if (!skill) return;
|
|
441
|
+
|
|
442
|
+
const type: ItemType = skill.type || 'skill';
|
|
443
|
+
const claudeHome = getClaudeHome();
|
|
444
|
+
const subdir = type === 'skill' ? 'skills' : 'commands';
|
|
445
|
+
|
|
446
|
+
// Remove global
|
|
447
|
+
const globalDir = join(claudeHome, subdir, name);
|
|
448
|
+
if (existsSync(globalDir)) rmSync(globalDir, { recursive: true, force: true });
|
|
449
|
+
|
|
450
|
+
// Remove from projects
|
|
451
|
+
const projects = JSON.parse(skill.installed_projects || '[]');
|
|
452
|
+
for (const pp of projects) {
|
|
453
|
+
const projDir = join(pp, '.claude', subdir, name);
|
|
454
|
+
if (existsSync(projDir)) rmSync(projDir, { recursive: true, force: true });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
db().prepare('DELETE FROM skills WHERE name = ?').run(name);
|
|
458
|
+
}
|