@loicngr/kobo 1.6.0 → 1.6.2
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/AGENTS.md +6 -1
- package/README.md +43 -29
- package/dist/server/db/index.js +17 -0
- package/dist/server/db/migrations.js +10 -0
- package/dist/server/db/schema.js +2 -1
- package/dist/server/index.js +26 -5
- package/dist/server/middleware/migration-guard.js +15 -0
- package/dist/server/routes/dev-server.js +3 -2
- package/dist/server/routes/documents.js +113 -0
- package/dist/server/routes/engines.js +9 -0
- package/dist/server/routes/migration.js +5 -0
- package/dist/server/routes/workspaces.js +35 -11
- package/dist/server/services/agent/engines/claude-code/args-builder.js +22 -0
- package/dist/server/services/agent/engines/claude-code/capabilities.js +17 -0
- package/dist/server/services/agent/engines/claude-code/engine.js +163 -0
- package/dist/server/services/agent/engines/claude-code/mcp-config.js +23 -0
- package/dist/server/services/agent/engines/claude-code/stream-parser.js +224 -0
- package/dist/server/services/agent/engines/registry.js +21 -0
- package/dist/server/services/agent/engines/types.js +18 -0
- package/dist/server/services/agent/event-router.js +4 -0
- package/dist/server/services/agent/orchestrator.js +582 -0
- package/dist/server/services/agent/session-controller.js +79 -0
- package/dist/server/services/content-migration-service.js +155 -0
- package/dist/server/services/db-backup-service.js +15 -0
- package/dist/server/services/websocket-service.js +81 -50
- package/dist/server/services/workspace-service.js +11 -5
- package/dist/server/utils/paths.js +1 -1
- package/dist/shared/models.js +50 -0
- package/package.json +1 -1
- package/src/client/dist/spa/assets/ActivityFeed-CFuT6H5u.js +7 -0
- package/src/client/dist/spa/assets/ActivityFeed-DXYafbn4.css +1 -0
- package/src/client/dist/spa/assets/ClosePopup-DhM1C4Zw.js +1 -0
- package/src/client/dist/spa/assets/CreatePage-sGrkfyOm.js +2 -0
- package/src/client/dist/spa/assets/CreatePage-yu2IH7GW.css +1 -0
- package/src/client/dist/spa/assets/DiffViewer-BVU58ujc.css +1 -0
- package/src/client/dist/spa/assets/DiffViewer-BwSRtVRI.js +2 -0
- package/src/client/dist/spa/assets/HealthPage-BO-bMpEu.js +1 -0
- package/src/client/dist/spa/assets/MainLayout-BiBJtDTk.css +1 -0
- package/src/client/dist/spa/assets/{MainLayout-_oPM07ln.js → MainLayout-BpqOChIX.js} +17 -17
- package/src/client/dist/spa/assets/QBadge-DqtcDv8D.js +1 -0
- package/src/client/dist/spa/assets/QBtn-CyzfM9-_.js +1 -0
- package/src/client/dist/spa/assets/QChip-KJoHYE6F.js +1 -0
- package/src/client/dist/spa/assets/QDialog-DQeAxY3-.js +1 -0
- package/src/client/dist/spa/assets/QExpansionItem-DCRks-Ra.js +1 -0
- package/src/client/dist/spa/assets/{QSpinner-CliSLjf8.js → QIcon-B0-pH3Qs.js} +1 -1
- package/src/client/dist/spa/assets/QItemLabel-Codqjisk.js +1 -0
- package/src/client/dist/spa/assets/QItemSection-CGpX7GcL.js +1 -0
- package/src/client/dist/spa/assets/QList-B-MkPF7n.js +1 -0
- package/src/client/dist/spa/assets/QPage-yqdKDG7-.js +1 -0
- package/src/client/dist/spa/assets/QScrollArea-e5qTqwcb.js +1 -0
- package/src/client/dist/spa/assets/QSeparator-rkjCbX2M.js +1 -0
- package/src/client/dist/spa/assets/QSlideTransition-BQxI8l5r.js +1 -0
- package/src/client/dist/spa/assets/QSpace-BNr0AftG.js +1 -0
- package/src/client/dist/spa/assets/QSpinnerDots-DEiRooBD.js +1 -0
- package/src/client/dist/spa/assets/QTabPanels--6cYe2US.js +1 -0
- package/src/client/dist/spa/assets/QTooltip-C4CPesBX.js +1 -0
- package/src/client/dist/spa/assets/SearchPage-BrUbbtgI.js +1 -0
- package/src/client/dist/spa/assets/SettingsPage-B3elO1PX.js +1 -0
- package/src/client/dist/spa/assets/TouchPan-BT6phK1f.js +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-BHl17_tY.js +4 -0
- package/src/client/dist/spa/assets/WorkspacePage-DPGiH02q.css +1 -0
- package/src/client/dist/spa/assets/build-path-tree-Bgl2q74t.js +1 -0
- package/src/client/dist/spa/assets/{cssMode-DMX8jq8u.js → cssMode-B1wQ-79R.js} +1 -1
- package/src/client/dist/spa/assets/documents-CHc8t22V.js +60 -0
- package/src/client/dist/spa/assets/{editor.api-DirOkGGg.js → editor.api-CRb_5Zw6.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-DC4ezIu0.js → editor.main-C5sdCvGW.js} +3 -3
- package/src/client/dist/spa/assets/format-Bttc9ToS.js +1 -0
- package/src/client/dist/spa/assets/{formatters-BzaS4w0I.js → formatters-BDadphwz.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-DI9xJfj0.js → freemarker2-CVSnsZk-.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-B9F-pScn.js → handlebars-uL_pucGI.js} +1 -1
- package/src/client/dist/spa/assets/{html-DTe2v8Q8.js → html-CatZVwWp.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-F_XLjWfJ.js → htmlMode-DTDzEngo.js} +1 -1
- package/src/client/dist/spa/assets/i18n-D-VdPLEh.js +1 -0
- package/src/client/dist/spa/assets/index-CUI-zN26.js +2 -0
- package/src/client/dist/spa/assets/{javascript-B9xJRPC6.js → javascript-DeHBpolA.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-DTZ6j6UO.js → jsonMode-Bma_YGGm.js} +1 -1
- package/src/client/dist/spa/assets/{liquid-BjU5MtW6.js → liquid-CW7xQEG_.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-BMUpG7Be.js → mdx-BsYUhMzF.js} +1 -1
- package/src/client/dist/spa/assets/models-BbSRHL9b.js +1 -0
- package/src/client/dist/spa/assets/{monaco.contribution-D7JUf8DP.js → monaco.contribution-Du0atePv.js} +2 -2
- package/src/client/dist/spa/assets/private.use-form-D1RuEt2P.js +1 -0
- package/src/client/dist/spa/assets/{python-Dz0D4uSk.js → python-D7DQWXZm.js} +1 -1
- package/src/client/dist/spa/assets/{razor-D7CFxuwR.js → razor-B2ZxF301.js} +1 -1
- package/src/client/dist/spa/assets/scroll-JVVkg2Ng.js +1 -0
- package/src/client/dist/spa/assets/touch-CBLrR6_z.js +1 -0
- package/src/client/dist/spa/assets/{tsMode-DjscaxpS.js → tsMode-B4Xul5xA.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-DozCWZl2.js → typescript-CdsKQuLT.js} +1 -1
- package/src/client/dist/spa/assets/use-checkbox-DYiZQsbF.js +1 -0
- package/src/client/dist/spa/assets/use-id-CeduaJbU.js +1 -0
- package/src/client/dist/spa/assets/use-portal-DBe4lcC2.js +1 -0
- package/src/client/dist/spa/assets/use-quasar-Ch82z8H5.js +1 -0
- package/src/client/dist/spa/assets/{xml-DFOJMT39.js → xml-Wap00dMv.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-yEefnsXm.js → yaml-BxRDHC24.js} +1 -1
- package/src/client/dist/spa/index.html +12 -14
- package/src/mcp-server/README.md +1 -1
- package/dist/server/routes/plans.js +0 -89
- package/dist/server/services/agent-manager.js +0 -621
- package/src/client/dist/spa/assets/ActivityFeed-0GR1zPoc.js +0 -10
- package/src/client/dist/spa/assets/ActivityFeed-CfsKExt9.css +0 -1
- package/src/client/dist/spa/assets/ClosePopup-CdSn7HO8.js +0 -1
- package/src/client/dist/spa/assets/CreatePage-dMi4xVYN.css +0 -1
- package/src/client/dist/spa/assets/CreatePage-je_7dC5I.js +0 -2
- package/src/client/dist/spa/assets/DiffViewer-DREYX-8k.js +0 -2
- package/src/client/dist/spa/assets/DiffViewer-DiHFLSk4.css +0 -1
- package/src/client/dist/spa/assets/HealthPage-Do8QZdxw.js +0 -1
- package/src/client/dist/spa/assets/MainLayout-B5poKEy_.css +0 -1
- package/src/client/dist/spa/assets/QBadge-Bvh-hQ8K.js +0 -1
- package/src/client/dist/spa/assets/QBtn-BsD8vrWq.js +0 -1
- package/src/client/dist/spa/assets/QDialog-CkbLS1If.js +0 -1
- package/src/client/dist/spa/assets/QExpansionItem-UgkE560c.js +0 -1
- package/src/client/dist/spa/assets/QList-D80ms7bw.js +0 -1
- package/src/client/dist/spa/assets/QMenu-DU-wiY_A.js +0 -1
- package/src/client/dist/spa/assets/QPage-BKY2-sf-.js +0 -1
- package/src/client/dist/spa/assets/QSpace-C5Ebr0vq.js +0 -1
- package/src/client/dist/spa/assets/QSpinnerDots-Dp12eHrB.js +0 -1
- package/src/client/dist/spa/assets/QTabPanels-C7lWp1yU.js +0 -1
- package/src/client/dist/spa/assets/QToggle-B0HvuNEg.js +0 -1
- package/src/client/dist/spa/assets/QTooltip-kLXuUa_m.js +0 -1
- package/src/client/dist/spa/assets/SearchPage-CCfyqBKh.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-CmyIsV-S.js +0 -1
- package/src/client/dist/spa/assets/TouchPan-CVMnGs0y.js +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-CWRMLYs-.css +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-Cl7YrG51.js +0 -4
- package/src/client/dist/spa/assets/focus-manager-DYbz9jFW.js +0 -1
- package/src/client/dist/spa/assets/format-Cyg8IgRi.js +0 -1
- package/src/client/dist/spa/assets/i18n-B13zBh1H.js +0 -1
- package/src/client/dist/spa/assets/i18n-CCWLBc0p.js +0 -1
- package/src/client/dist/spa/assets/index-DoNZ_5QK.js +0 -5
- package/src/client/dist/spa/assets/marked.esm-DCmk6NO8.js +0 -60
- package/src/client/dist/spa/assets/models-B8fzv7K4.js +0 -1
- package/src/client/dist/spa/assets/pinia-C3JsrLkB.js +0 -1
- package/src/client/dist/spa/assets/private.use-form-BhKyDtO7.js +0 -1
- package/src/client/dist/spa/assets/scroll-CLibRGI-.js +0 -1
- package/src/client/dist/spa/assets/settings-B69lIVX0.js +0 -1
- package/src/client/dist/spa/assets/touch-ChrvzrnI.js +0 -1
- package/src/client/dist/spa/assets/use-dark-DnuCB6tC.js +0 -1
- package/src/client/dist/spa/assets/use-quasar-DBoizHBW.js +0 -1
- /package/src/client/dist/spa/assets/{_plugin-vue_export-helper-Cxt1D8wE.js → _plugin-vue_export-helper-r4mAJOHR.js} +0 -0
- /package/src/client/dist/spa/assets/{abap-CFuyUYKP.js → abap-Bgec7Keq.js} +0 -0
- /package/src/client/dist/spa/assets/{apex-Ctq_xcrv.js → apex-VBlPwEoQ.js} +0 -0
- /package/src/client/dist/spa/assets/{azcli-BBQSVn-C.js → azcli-DKqrEFBx.js} +0 -0
- /package/src/client/dist/spa/assets/{bat-DbnqAfvr.js → bat-DdgQWy_0.js} +0 -0
- /package/src/client/dist/spa/assets/{bicep-BtDlIXop.js → bicep-CRMM43EB.js} +0 -0
- /package/src/client/dist/spa/assets/{cameligo-BLeJgKTj.js → cameligo-UatALtML.js} +0 -0
- /package/src/client/dist/spa/assets/{clojure-aZUQIUKP.js → clojure-D8JU08RA.js} +0 -0
- /package/src/client/dist/spa/assets/{coffee-Secadq9U.js → coffee-C56wu358.js} +0 -0
- /package/src/client/dist/spa/assets/{cpp-JicRPTRv.js → cpp-CyZLvhJG.js} +0 -0
- /package/src/client/dist/spa/assets/{csharp-C7NSOZyj.js → csharp-BJl3ixva.js} +0 -0
- /package/src/client/dist/spa/assets/{csp-CIje7830.js → csp-CxEKxmO-.js} +0 -0
- /package/src/client/dist/spa/assets/{css-G0bm1q_M.js → css-B0t_muXd.js} +0 -0
- /package/src/client/dist/spa/assets/{cypher-CldD5D0u.js → cypher-D1hqiMFD.js} +0 -0
- /package/src/client/dist/spa/assets/{dart-DIK3l8YT.js → dart-Bz550Pyv.js} +0 -0
- /package/src/client/dist/spa/assets/{dockerfile-czxaGh2L.js → dockerfile-CIXgVAuA.js} +0 -0
- /package/src/client/dist/spa/assets/{ecl-BqdYhwmw.js → ecl-D9qbvZoA.js} +0 -0
- /package/src/client/dist/spa/assets/{elixir-m52LePTW.js → elixir-b2M38fAy.js} +0 -0
- /package/src/client/dist/spa/assets/{flow9-B5QJ9GvZ.js → flow9-Dq1UYMkt.js} +0 -0
- /package/src/client/dist/spa/assets/{fsharp-B15czHsH.js → fsharp-CFNadkg7.js} +0 -0
- /package/src/client/dist/spa/assets/{go-BkoQxDo1.js → go-dSur1iB2.js} +0 -0
- /package/src/client/dist/spa/assets/{graphql-BnI6uRa_.js → graphql-qyhAo11d.js} +0 -0
- /package/src/client/dist/spa/assets/{hcl-CAwwENT7.js → hcl-DFzjMyzm.js} +0 -0
- /package/src/client/dist/spa/assets/{ini-BHM5zh1H.js → ini-TdzA8TIl.js} +0 -0
- /package/src/client/dist/spa/assets/{java-B5i95QvQ.js → java-CSGA9pkE.js} +0 -0
- /package/src/client/dist/spa/assets/{julia-DPDm885q.js → julia-9izz5OsY.js} +0 -0
- /package/src/client/dist/spa/assets/{kotlin-qoccd5BP.js → kotlin-DuPK7AtF.js} +0 -0
- /package/src/client/dist/spa/assets/{less-B6RU166D.js → less-B8d93iCg.js} +0 -0
- /package/src/client/dist/spa/assets/{lexon-YfUeoL1V.js → lexon-DWtEIyu7.js} +0 -0
- /package/src/client/dist/spa/assets/{lua-BIUI5y9b.js → lua-Ciq0OGgt.js} +0 -0
- /package/src/client/dist/spa/assets/{m3-D5SAbSdU.js → m3-Cki6JWj_.js} +0 -0
- /package/src/client/dist/spa/assets/{markdown-CVJLwHzJ.js → markdown-Cu47xwU0.js} +0 -0
- /package/src/client/dist/spa/assets/{mips-R-FZ3zOR.js → mips-BM8ui995.js} +0 -0
- /package/src/client/dist/spa/assets/{msdax-Blveyl9r.js → msdax-DqLio0_c.js} +0 -0
- /package/src/client/dist/spa/assets/{mysql-D4mY1AFx.js → mysql-v1wbjJOq.js} +0 -0
- /package/src/client/dist/spa/assets/{objective-c-BmXrLr4h.js → objective-c-CQl3PGSB.js} +0 -0
- /package/src/client/dist/spa/assets/{pascal-yxckoyvV.js → pascal-D4iW0ZtD.js} +0 -0
- /package/src/client/dist/spa/assets/{pascaligo-Q5JCwXMI.js → pascaligo-BdC9CZdj.js} +0 -0
- /package/src/client/dist/spa/assets/{perl-BF1Rrs5h.js → perl-BL10m4XD.js} +0 -0
- /package/src/client/dist/spa/assets/{pgsql-CnYB97wm.js → pgsql-Be_oqVo3.js} +0 -0
- /package/src/client/dist/spa/assets/{php-CdDfQfSg.js → php-BtvXSFRI.js} +0 -0
- /package/src/client/dist/spa/assets/{pla-whj-d71F.js → pla-B2vUy15C.js} +0 -0
- /package/src/client/dist/spa/assets/{postiats-ClfLr4I-.js → postiats-CbmTTfXr.js} +0 -0
- /package/src/client/dist/spa/assets/{powerquery-iRaBhuuk.js → powerquery-DszLhJGx.js} +0 -0
- /package/src/client/dist/spa/assets/{powershell-DjiEt5xK.js → powershell-B0dYktF6.js} +0 -0
- /package/src/client/dist/spa/assets/{protobuf-B6dcIEUr.js → protobuf-CZvaj1VX.js} +0 -0
- /package/src/client/dist/spa/assets/{pug-DtmHnjM9.js → pug-CPDx1B3S.js} +0 -0
- /package/src/client/dist/spa/assets/{qsharp-CELCyd79.js → qsharp-CDP9TFLl.js} +0 -0
- /package/src/client/dist/spa/assets/{r-ZpJXWV-o.js → r-8DbbFX2l.js} +0 -0
- /package/src/client/dist/spa/assets/{rate-limit-labels-dCPVjS61.js → rate-limit-labels-BoDORKFj.js} +0 -0
- /package/src/client/dist/spa/assets/{redis-BiHSNkAl.js → redis-DRWj9MtJ.js} +0 -0
- /package/src/client/dist/spa/assets/{redshift-DzuwYCHP.js → redshift-C6cElE_5.js} +0 -0
- /package/src/client/dist/spa/assets/{restructuredtext-YOT94bbS.js → restructuredtext-W9pS9n3m.js} +0 -0
- /package/src/client/dist/spa/assets/{ruby-BfiHr6Uu.js → ruby-BKnzWnk-.js} +0 -0
- /package/src/client/dist/spa/assets/{rust-JZ-uOoYM.js → rust-YPCclWwe.js} +0 -0
- /package/src/client/dist/spa/assets/{sb-CBglP1-t.js → sb-BgM4DTFb.js} +0 -0
- /package/src/client/dist/spa/assets/{scala-C9l41paw.js → scala-fz1OPLMl.js} +0 -0
- /package/src/client/dist/spa/assets/{scheme-B-InQ6hy.js → scheme-8Uz1RIbu.js} +0 -0
- /package/src/client/dist/spa/assets/{scss-v6OmJRN9.js → scss-Djo3IYXr.js} +0 -0
- /package/src/client/dist/spa/assets/{shell-Dyp6iwB6.js → shell-CINF5Tx_.js} +0 -0
- /package/src/client/dist/spa/assets/{solidity-D5epNWue.js → solidity-GgiNEuUm.js} +0 -0
- /package/src/client/dist/spa/assets/{sophia-Eva-79sB.js → sophia-Culj97P9.js} +0 -0
- /package/src/client/dist/spa/assets/{sparql-gvALLO1w.js → sparql-C2ZlpxOY.js} +0 -0
- /package/src/client/dist/spa/assets/{sql-COdamZYI.js → sql-BEf5Pg7Y.js} +0 -0
- /package/src/client/dist/spa/assets/{st-eMoImIwE.js → st-CT6UUoeH.js} +0 -0
- /package/src/client/dist/spa/assets/{swift-7R_T9RYH.js → swift-B5g0xTG3.js} +0 -0
- /package/src/client/dist/spa/assets/{symbols-CAg-nBkV.js → symbols-DCYodwb2.js} +0 -0
- /package/src/client/dist/spa/assets/{systemverilog-1pCEfaHU.js → systemverilog-CEgQz9DR.js} +0 -0
- /package/src/client/dist/spa/assets/{tcl-B_KgnhfE.js → tcl-D0qL2L0I.js} +0 -0
- /package/src/client/dist/spa/assets/{twig-CFZUJxb9.js → twig-BFUAVf1E.js} +0 -0
- /package/src/client/dist/spa/assets/{typespec-B1ZgHlud.js → typespec-CjVVcNKm.js} +0 -0
- /package/src/client/dist/spa/assets/{vb-DKdun5tL.js → vb-CZJr-DQz.js} +0 -0
- /package/src/client/dist/spa/assets/{vue-i18n-eUDnMrPl.js → vue-i18n-CeG0hR0Z.js} +0 -0
- /package/src/client/dist/spa/assets/{wgsl-CzNaxTrn.js → wgsl-ivoXUo2e.js} +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CLAUDE_MODELS } from '../../../../../shared/models.js';
|
|
2
|
+
export const CLAUDE_CODE_CAPABILITIES = {
|
|
3
|
+
// Models come from the shared catalogue in `src/shared/models.ts` — the
|
|
4
|
+
// ONE source of truth, consumed both by this file (for /api/engines and
|
|
5
|
+
// for validation in POST /api/workspaces) and by the frontend selectors.
|
|
6
|
+
models: CLAUDE_MODELS.map((m) => ({ id: m.id, label: m.label })),
|
|
7
|
+
effortLevels: [
|
|
8
|
+
{ id: 'auto', label: 'Auto' },
|
|
9
|
+
{ id: 'low', label: 'Low' },
|
|
10
|
+
{ id: 'medium', label: 'Medium' },
|
|
11
|
+
{ id: 'high', label: 'High' },
|
|
12
|
+
],
|
|
13
|
+
permissionModes: ['auto-accept', 'plan'],
|
|
14
|
+
supportsResume: true,
|
|
15
|
+
supportsMcp: true,
|
|
16
|
+
supportsSkills: true,
|
|
17
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import { buildClaudeArgs } from './args-builder.js';
|
|
4
|
+
import { CLAUDE_CODE_CAPABILITIES } from './capabilities.js';
|
|
5
|
+
import { cleanupMcpConfig, writeMcpConfig } from './mcp-config.js';
|
|
6
|
+
import { createParserState, parseClaudeLine } from './stream-parser.js';
|
|
7
|
+
export function createClaudeCodeEngine() {
|
|
8
|
+
return {
|
|
9
|
+
id: 'claude-code',
|
|
10
|
+
displayName: 'Claude Code',
|
|
11
|
+
capabilities: CLAUDE_CODE_CAPABILITIES,
|
|
12
|
+
async start(options, onEvent) {
|
|
13
|
+
// Write MCP config if any servers requested + engine supports MCP
|
|
14
|
+
let mcpConfigPath;
|
|
15
|
+
if (options.mcpServers && options.mcpServers.length > 0) {
|
|
16
|
+
mcpConfigPath = writeMcpConfig(options.workingDir, options.mcpServers);
|
|
17
|
+
}
|
|
18
|
+
const { args } = buildClaudeArgs({
|
|
19
|
+
prompt: options.prompt,
|
|
20
|
+
model: options.model,
|
|
21
|
+
effort: options.effort,
|
|
22
|
+
permissionMode: options.permissionMode ?? 'auto-accept',
|
|
23
|
+
skipPermissions: options.settings.dangerouslySkipPermissions ?? true,
|
|
24
|
+
resumeFromEngineSessionId: options.resumeFromEngineSessionId,
|
|
25
|
+
mcpConfigPath,
|
|
26
|
+
});
|
|
27
|
+
const proc = spawn('claude', args, {
|
|
28
|
+
cwd: options.workingDir,
|
|
29
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
|
+
});
|
|
31
|
+
const parserState = createParserState();
|
|
32
|
+
if (!proc.stdout)
|
|
33
|
+
throw new Error('Claude process has no stdout');
|
|
34
|
+
const rl = readline.createInterface({
|
|
35
|
+
input: proc.stdout,
|
|
36
|
+
crlfDelay: Number.POSITIVE_INFINITY,
|
|
37
|
+
});
|
|
38
|
+
let discoveredSessionId;
|
|
39
|
+
rl.on('line', (line) => {
|
|
40
|
+
const { events } = parseClaudeLine(line, parserState);
|
|
41
|
+
for (const ev of events) {
|
|
42
|
+
if (ev.kind === 'session:started')
|
|
43
|
+
discoveredSessionId = ev.engineSessionId;
|
|
44
|
+
onEvent(ev);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
// Line-buffer stderr so we see one event per log line instead of
|
|
48
|
+
// arbitrary byte chunks, and restrict quota detection to clear rate-
|
|
49
|
+
// limit signals (not every occurrence of the word "rate" or "quota").
|
|
50
|
+
// Non-quota stderr lines are logged to the console but do NOT emit
|
|
51
|
+
// an error event — this avoids false positives flooding the UI.
|
|
52
|
+
const stderrRl = proc.stderr
|
|
53
|
+
? readline.createInterface({
|
|
54
|
+
input: proc.stderr,
|
|
55
|
+
crlfDelay: Number.POSITIVE_INFINITY,
|
|
56
|
+
})
|
|
57
|
+
: undefined;
|
|
58
|
+
// Known benign stderr lines from the Claude CLI that should NOT be
|
|
59
|
+
// logged — they flood the dev console and carry no actionable info.
|
|
60
|
+
// Strip ANSI color codes before matching.
|
|
61
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escapes by design
|
|
62
|
+
const stripAnsi = (s) => s.replace(/\u001b\[\d+m/g, '');
|
|
63
|
+
function isBenignStderr(line) {
|
|
64
|
+
const cleaned = stripAnsi(line).trim();
|
|
65
|
+
return /^warning: no stdin data received in \d+s/i.test(cleaned);
|
|
66
|
+
}
|
|
67
|
+
stderrRl?.on('line', (line) => {
|
|
68
|
+
const lower = line.toLowerCase();
|
|
69
|
+
const isQuota = lower.includes('rate limit exceeded') ||
|
|
70
|
+
lower.includes('rate_limit_exceeded') ||
|
|
71
|
+
(lower.includes('429') && lower.includes('rate')) ||
|
|
72
|
+
lower.includes('quota exceeded');
|
|
73
|
+
if (isQuota) {
|
|
74
|
+
onEvent({ kind: 'error', category: 'quota', message: line });
|
|
75
|
+
}
|
|
76
|
+
else if (line.trim().length > 0 && !isBenignStderr(line)) {
|
|
77
|
+
console.warn(`[claude-engine stderr] ${line}`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
// 'error' fires when spawn itself fails (e.g. ENOENT if the `claude`
|
|
81
|
+
// binary is missing from PATH). In that case 'exit' never fires, so we
|
|
82
|
+
// emit the lifecycle pair here and clean the MCP config ourselves.
|
|
83
|
+
proc.on('error', (err) => {
|
|
84
|
+
onEvent({ kind: 'error', category: 'spawn_failed', message: err.message });
|
|
85
|
+
onEvent({ kind: 'session:ended', reason: 'error', exitCode: null });
|
|
86
|
+
cleanupMcpConfig(options.workingDir);
|
|
87
|
+
rl.close();
|
|
88
|
+
stderrRl?.close();
|
|
89
|
+
});
|
|
90
|
+
proc.on('exit', (code) => {
|
|
91
|
+
cleanupMcpConfig(options.workingDir);
|
|
92
|
+
rl.close();
|
|
93
|
+
stderrRl?.close();
|
|
94
|
+
onEvent({
|
|
95
|
+
kind: 'session:ended',
|
|
96
|
+
reason: code === 0 ? 'completed' : code === null ? 'killed' : 'error',
|
|
97
|
+
exitCode: code,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
const engineProcess = {
|
|
101
|
+
get pid() {
|
|
102
|
+
return proc.pid;
|
|
103
|
+
},
|
|
104
|
+
get engineSessionId() {
|
|
105
|
+
return discoveredSessionId;
|
|
106
|
+
},
|
|
107
|
+
sendMessage(text) {
|
|
108
|
+
if (!proc.stdin?.writable)
|
|
109
|
+
throw new Error('Agent stdin not writable');
|
|
110
|
+
proc.stdin.write(`${text}\n`);
|
|
111
|
+
},
|
|
112
|
+
interrupt() {
|
|
113
|
+
if (proc.pid !== undefined)
|
|
114
|
+
process.kill(proc.pid, 'SIGINT');
|
|
115
|
+
},
|
|
116
|
+
stop() {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
if (proc.killed || proc.exitCode !== null)
|
|
119
|
+
return resolve();
|
|
120
|
+
let resolved = false;
|
|
121
|
+
let killTimer;
|
|
122
|
+
let hardTimeout;
|
|
123
|
+
const doResolve = () => {
|
|
124
|
+
if (resolved)
|
|
125
|
+
return;
|
|
126
|
+
resolved = true;
|
|
127
|
+
if (killTimer)
|
|
128
|
+
clearTimeout(killTimer);
|
|
129
|
+
if (hardTimeout)
|
|
130
|
+
clearTimeout(hardTimeout);
|
|
131
|
+
resolve();
|
|
132
|
+
};
|
|
133
|
+
proc.once('exit', doResolve);
|
|
134
|
+
try {
|
|
135
|
+
proc.kill('SIGTERM');
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Already dead
|
|
139
|
+
}
|
|
140
|
+
killTimer = setTimeout(() => {
|
|
141
|
+
try {
|
|
142
|
+
if (!proc.killed)
|
|
143
|
+
proc.kill('SIGKILL');
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Ignore
|
|
147
|
+
}
|
|
148
|
+
}, 5000);
|
|
149
|
+
killTimer.unref?.();
|
|
150
|
+
// Hard-timeout safety net: if the process hasn't exited within 10s
|
|
151
|
+
// (5s after SIGKILL), resolve anyway so callers never hang forever.
|
|
152
|
+
hardTimeout = setTimeout(() => {
|
|
153
|
+
console.warn('[claude-engine] stop() hard-timeout reached, resolving anyway');
|
|
154
|
+
doResolve();
|
|
155
|
+
}, 10000);
|
|
156
|
+
hardTimeout.unref?.();
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
return engineProcess;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { existsSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const MCP_FILENAME = '.mcp.json';
|
|
4
|
+
export function writeMcpConfig(workingDir, servers) {
|
|
5
|
+
const filePath = join(workingDir, MCP_FILENAME);
|
|
6
|
+
const mcpServers = {};
|
|
7
|
+
for (const s of servers) {
|
|
8
|
+
mcpServers[s.name] = { command: s.command, args: s.args, env: s.env };
|
|
9
|
+
}
|
|
10
|
+
writeFileSync(filePath, JSON.stringify({ mcpServers }, null, 2));
|
|
11
|
+
return filePath;
|
|
12
|
+
}
|
|
13
|
+
export function cleanupMcpConfig(workingDir) {
|
|
14
|
+
const filePath = join(workingDir, MCP_FILENAME);
|
|
15
|
+
if (existsSync(filePath)) {
|
|
16
|
+
try {
|
|
17
|
+
unlinkSync(filePath);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Best-effort cleanup
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
function normalizeResetsAt(raw) {
|
|
2
|
+
if (typeof raw === 'string' && raw.length > 0)
|
|
3
|
+
return raw;
|
|
4
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
5
|
+
return new Date(raw * 1000).toISOString();
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
function extractUsedPct(source) {
|
|
9
|
+
const raw = (source.utilization ?? source.used_percent ?? source.percent_used ?? source.usedPct);
|
|
10
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
11
|
+
return raw <= 1 ? raw * 100 : raw;
|
|
12
|
+
const used = source.used ?? source.current ?? source.spent;
|
|
13
|
+
const limit = source.limit ?? source.max ?? source.allowed;
|
|
14
|
+
if (typeof used === 'number' && typeof limit === 'number' && limit > 0)
|
|
15
|
+
return (used / limit) * 100;
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function makeBucket(id, source) {
|
|
19
|
+
const usedPct = extractUsedPct(source) ?? source.__fallbackPct ?? null;
|
|
20
|
+
if (usedPct === null)
|
|
21
|
+
return null;
|
|
22
|
+
const resetsAt = normalizeResetsAt(source.resets_at ?? source.reset_at ?? source.resetsAt ?? source.resetAt);
|
|
23
|
+
const label = (typeof source.label === 'string' && source.label) || undefined;
|
|
24
|
+
const used = source.used ?? source.current ?? source.spent;
|
|
25
|
+
const limit = source.limit ?? source.max ?? source.allowed;
|
|
26
|
+
const details = used !== undefined && limit !== undefined ? `${String(used)} / ${String(limit)}` : undefined;
|
|
27
|
+
return { id, label, usedPct: Math.max(0, Math.min(100, usedPct)), resetsAt, details };
|
|
28
|
+
}
|
|
29
|
+
function normalizeRateLimitInfo(info) {
|
|
30
|
+
const buckets = [];
|
|
31
|
+
if (typeof info.rateLimitType === 'string') {
|
|
32
|
+
const b = makeBucket(info.rateLimitType, { ...info, __fallbackPct: 0 });
|
|
33
|
+
if (b)
|
|
34
|
+
buckets.push(b);
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(info.buckets)) {
|
|
37
|
+
for (const entry of info.buckets) {
|
|
38
|
+
if (!entry || typeof entry !== 'object')
|
|
39
|
+
continue;
|
|
40
|
+
const obj = entry;
|
|
41
|
+
const id = (typeof obj.id === 'string' && obj.id) ||
|
|
42
|
+
(typeof obj.name === 'string' && obj.name) ||
|
|
43
|
+
(typeof obj.rateLimitType === 'string' && obj.rateLimitType) ||
|
|
44
|
+
'unknown';
|
|
45
|
+
const b = makeBucket(id, obj);
|
|
46
|
+
if (b)
|
|
47
|
+
buckets.push(b);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { buckets };
|
|
51
|
+
}
|
|
52
|
+
export function createParserState() {
|
|
53
|
+
return { sessionStartedEmitted: false, openMessages: new Map() };
|
|
54
|
+
}
|
|
55
|
+
export function parseClaudeLine(line, state) {
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
if (!trimmed)
|
|
58
|
+
return { events: [], state };
|
|
59
|
+
// The marker can appear as a raw stdout line OR inside an assistant text block.
|
|
60
|
+
// We detect it in the raw line first so even unparseable lines that contain it
|
|
61
|
+
// still emit the signal. The assistant-branch handling below catches the
|
|
62
|
+
// structured case.
|
|
63
|
+
const markerDetected = trimmed.includes('[BRAINSTORM_COMPLETE]');
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(trimmed);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
const events = [{ kind: 'message:raw', content: line }];
|
|
70
|
+
if (markerDetected)
|
|
71
|
+
events.push({ kind: 'session:brainstorm-complete' });
|
|
72
|
+
return { events, state };
|
|
73
|
+
}
|
|
74
|
+
const events = [];
|
|
75
|
+
const type = parsed.type;
|
|
76
|
+
const subtype = parsed.subtype;
|
|
77
|
+
const sessionId = typeof parsed.session_id === 'string' ? parsed.session_id : undefined;
|
|
78
|
+
if (type === 'system') {
|
|
79
|
+
if (subtype === 'compact' || subtype === 'compact_boundary') {
|
|
80
|
+
events.push({ kind: 'session:compacted' });
|
|
81
|
+
return { events, state };
|
|
82
|
+
}
|
|
83
|
+
if (subtype === 'rate_limit_event') {
|
|
84
|
+
const info = parsed.rate_limit_info;
|
|
85
|
+
if (info && typeof info === 'object') {
|
|
86
|
+
events.push({ kind: 'rate_limit', info: normalizeRateLimitInfo(info) });
|
|
87
|
+
}
|
|
88
|
+
return { events, state };
|
|
89
|
+
}
|
|
90
|
+
if (subtype === 'task_started' || subtype === 'task_progress' || subtype === 'task_notification') {
|
|
91
|
+
const toolCallId = typeof parsed.tool_use_id === 'string' ? parsed.tool_use_id : undefined;
|
|
92
|
+
if (toolCallId) {
|
|
93
|
+
const usage = parsed.usage;
|
|
94
|
+
const taskStatus = typeof parsed.status === 'string' ? parsed.status : undefined;
|
|
95
|
+
const isDone = subtype === 'task_notification' &&
|
|
96
|
+
taskStatus !== undefined &&
|
|
97
|
+
['completed', 'stopped', 'failed', 'cancelled'].includes(taskStatus);
|
|
98
|
+
events.push({
|
|
99
|
+
kind: 'subagent:progress',
|
|
100
|
+
toolCallId,
|
|
101
|
+
status: isDone ? 'done' : 'running',
|
|
102
|
+
description: typeof parsed.description === 'string' ? parsed.description : undefined,
|
|
103
|
+
taskType: typeof parsed.task_type === 'string' ? parsed.task_type : undefined,
|
|
104
|
+
lastToolName: typeof parsed.last_tool_name === 'string' ? parsed.last_tool_name : undefined,
|
|
105
|
+
totalTokens: typeof usage?.total_tokens === 'number' ? usage.total_tokens : undefined,
|
|
106
|
+
toolUses: typeof usage?.tool_uses === 'number' ? usage.tool_uses : undefined,
|
|
107
|
+
durationMs: typeof usage?.duration_ms === 'number' ? usage.duration_ms : undefined,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return { events, state };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (type === 'system' && subtype === 'init') {
|
|
114
|
+
if (sessionId && (!state.sessionStartedEmitted || state.sessionId !== sessionId)) {
|
|
115
|
+
events.push({
|
|
116
|
+
kind: 'session:started',
|
|
117
|
+
engineSessionId: sessionId,
|
|
118
|
+
model: typeof parsed.model === 'string' ? parsed.model : undefined,
|
|
119
|
+
});
|
|
120
|
+
state.sessionStartedEmitted = true;
|
|
121
|
+
state.sessionId = sessionId;
|
|
122
|
+
}
|
|
123
|
+
if (Array.isArray(parsed.slash_commands) && parsed.slash_commands.length > 0) {
|
|
124
|
+
events.push({ kind: 'skills:discovered', skills: parsed.slash_commands });
|
|
125
|
+
}
|
|
126
|
+
return { events, state };
|
|
127
|
+
}
|
|
128
|
+
if (type === 'assistant') {
|
|
129
|
+
const message = parsed.message;
|
|
130
|
+
const messageId = typeof message?.id === 'string' ? message.id : 'unknown';
|
|
131
|
+
const content = Array.isArray(message?.content) ? message?.content : [];
|
|
132
|
+
// A new messageId arriving means any previously-open message is done.
|
|
133
|
+
// Claude CLI's stream-json output doesn't always carry an explicit
|
|
134
|
+
// `stop_reason` or `message_stop` on the last chunk; some runs finish
|
|
135
|
+
// implicitly when the next turn begins. Close stale openMessages here
|
|
136
|
+
// so the UI's streaming spinner doesn't hang forever.
|
|
137
|
+
for (const openId of Array.from(state.openMessages.keys())) {
|
|
138
|
+
if (openId !== messageId) {
|
|
139
|
+
events.push({ kind: 'message:end', messageId: openId });
|
|
140
|
+
state.openMessages.delete(openId);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!state.openMessages.has(messageId)) {
|
|
144
|
+
state.openMessages.set(messageId, { sawText: false });
|
|
145
|
+
}
|
|
146
|
+
const msgState = state.openMessages.get(messageId);
|
|
147
|
+
for (const block of content) {
|
|
148
|
+
const blockType = block.type;
|
|
149
|
+
if (blockType === 'text' && typeof block.text === 'string') {
|
|
150
|
+
events.push({ kind: 'message:text', messageId, text: block.text, streaming: true });
|
|
151
|
+
msgState.sawText = true;
|
|
152
|
+
}
|
|
153
|
+
if (blockType === 'tool_use') {
|
|
154
|
+
events.push({
|
|
155
|
+
kind: 'tool:call',
|
|
156
|
+
messageId,
|
|
157
|
+
toolCallId: typeof block.id === 'string' ? block.id : 'unknown',
|
|
158
|
+
name: typeof block.name === 'string' ? block.name : 'unknown',
|
|
159
|
+
input: block.input ?? {},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (blockType === 'thinking') {
|
|
163
|
+
events.push({
|
|
164
|
+
kind: 'message:thinking',
|
|
165
|
+
messageId,
|
|
166
|
+
text: typeof block.thinking === 'string' ? block.thinking : '',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (blockType === 'text' &&
|
|
170
|
+
typeof block.text === 'string' &&
|
|
171
|
+
block.text.includes('[BRAINSTORM_COMPLETE]')) {
|
|
172
|
+
events.push({ kind: 'session:brainstorm-complete' });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Claude CLI sends many intermediate deltas for the same message; most of
|
|
176
|
+
// them carry `stop_reason: null`. Only a truly terminal event has either
|
|
177
|
+
// `message_stop: true` at the root, or a non-null `stop_reason`. Checking
|
|
178
|
+
// `!== undefined` would spuriously emit message:end on every delta.
|
|
179
|
+
const stopReason = message?.stop_reason;
|
|
180
|
+
const isStop = parsed.message_stop === true || (stopReason !== undefined && stopReason !== null);
|
|
181
|
+
if (isStop) {
|
|
182
|
+
events.push({ kind: 'message:end', messageId });
|
|
183
|
+
state.openMessages.delete(messageId);
|
|
184
|
+
}
|
|
185
|
+
return { events, state };
|
|
186
|
+
}
|
|
187
|
+
if (type === 'user') {
|
|
188
|
+
const message = parsed.message;
|
|
189
|
+
const content = Array.isArray(message?.content) ? message?.content : [];
|
|
190
|
+
for (const block of content) {
|
|
191
|
+
if (block.type === 'tool_result') {
|
|
192
|
+
events.push({
|
|
193
|
+
kind: 'tool:result',
|
|
194
|
+
toolCallId: typeof block.tool_use_id === 'string' ? block.tool_use_id : 'unknown',
|
|
195
|
+
output: block.content ?? null,
|
|
196
|
+
isError: block.is_error === true,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { events, state };
|
|
201
|
+
}
|
|
202
|
+
if (type === 'result') {
|
|
203
|
+
// Terminal event — close any message still considered "streaming".
|
|
204
|
+
for (const openId of Array.from(state.openMessages.keys())) {
|
|
205
|
+
events.push({ kind: 'message:end', messageId: openId });
|
|
206
|
+
state.openMessages.delete(openId);
|
|
207
|
+
}
|
|
208
|
+
const usage = parsed.usage;
|
|
209
|
+
if (usage) {
|
|
210
|
+
events.push({
|
|
211
|
+
kind: 'usage',
|
|
212
|
+
inputTokens: Number(usage.input_tokens ?? 0),
|
|
213
|
+
outputTokens: Number(usage.output_tokens ?? 0),
|
|
214
|
+
cacheRead: typeof usage.cache_read_input_tokens === 'number' ? usage.cache_read_input_tokens : undefined,
|
|
215
|
+
cacheWrite: typeof usage.cache_creation_input_tokens === 'number'
|
|
216
|
+
? usage.cache_creation_input_tokens
|
|
217
|
+
: undefined,
|
|
218
|
+
costUsd: typeof parsed.cost_usd === 'number' ? parsed.cost_usd : undefined,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return { events, state };
|
|
222
|
+
}
|
|
223
|
+
return { events, state };
|
|
224
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createClaudeCodeEngine } from './claude-code/engine.js';
|
|
2
|
+
const ENGINES = {
|
|
3
|
+
'claude-code': createClaudeCodeEngine(),
|
|
4
|
+
};
|
|
5
|
+
export function listEngines() {
|
|
6
|
+
return Object.values(ENGINES);
|
|
7
|
+
}
|
|
8
|
+
export function resolveEngine(id) {
|
|
9
|
+
const engine = ENGINES[id];
|
|
10
|
+
if (!engine)
|
|
11
|
+
throw new Error(`Unknown agent engine '${id}'`);
|
|
12
|
+
return engine;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Test-only seam. Replaces or adds an engine at runtime. Do not use in
|
|
16
|
+
* production — the static `ENGINES` map is the source of truth; this helper
|
|
17
|
+
* exists only so unit tests can inject fakes without wiring a DI container.
|
|
18
|
+
*/
|
|
19
|
+
export function _registerEngineForTest(engine) {
|
|
20
|
+
ENGINES[engine.id] = engine;
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Every AgentEvent kind, as a const for exhaustive iteration in tests. */
|
|
2
|
+
export const ALL_AGENT_EVENT_KINDS = [
|
|
3
|
+
'session:started',
|
|
4
|
+
'session:ended',
|
|
5
|
+
'session:compacted',
|
|
6
|
+
'session:brainstorm-complete',
|
|
7
|
+
'message:text',
|
|
8
|
+
'message:thinking',
|
|
9
|
+
'message:end',
|
|
10
|
+
'message:raw',
|
|
11
|
+
'tool:call',
|
|
12
|
+
'tool:result',
|
|
13
|
+
'subagent:progress',
|
|
14
|
+
'skills:discovered',
|
|
15
|
+
'usage',
|
|
16
|
+
'rate_limit',
|
|
17
|
+
'error',
|
|
18
|
+
];
|