@loicngr/kobo 1.3.0 → 1.4.0
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 +14 -0
- package/dist/mcp-server/kobo-tasks-server.js +2 -0
- package/dist/server/db/index.js +1 -0
- package/dist/server/db/migrations.js +11 -0
- package/dist/server/db/schema.js +4 -0
- package/dist/server/index.js +58 -7
- package/dist/server/routes/workspaces.js +157 -38
- package/dist/server/services/agent-manager.js +24 -6
- package/dist/server/services/notion-service.js +6 -3
- package/dist/server/services/pr-watcher-service.js +27 -6
- package/dist/server/services/websocket-service.js +41 -4
- package/dist/server/services/workspace-service.js +19 -3
- package/dist/server/utils/git-ops.js +172 -4
- package/dist/server/utils/paths.js +13 -0
- package/dist/server/utils/process-tracker.js +0 -4
- package/package.json +4 -3
- package/src/client/dist/spa/assets/ActivityFeed-Dxuw_8et.js +60 -0
- package/src/client/dist/spa/assets/ActivityFeed-OvgJQL4-.css +1 -0
- package/src/client/dist/spa/assets/CreatePage-CTFi3DpD.js +2 -0
- package/src/client/dist/spa/assets/CreatePage-DOr3puTt.css +1 -0
- package/src/client/dist/spa/assets/DiffViewer-7dck6mJc.css +1 -0
- package/src/client/dist/spa/assets/DiffViewer-DV9gt8DT.js +2 -0
- package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-k1h7X_-h.woff +0 -0
- package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-B7du-70m.woff +0 -0
- package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-CoAZ_DKt.woff +0 -0
- package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-D0406B4n.woff +0 -0
- package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-CnAg2DeQ.woff +0 -0
- package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-BG9VWE5v.woff +0 -0
- package/src/client/dist/spa/assets/MainLayout-BxqZy-kp.js +2 -0
- package/src/client/dist/spa/assets/MainLayout-gjAr74zR.css +1 -0
- package/src/client/dist/spa/assets/QBadge-Y5QfSDtm.js +1 -0
- package/src/client/dist/spa/assets/QBtn-D_bkYnrl.js +1 -0
- package/src/client/dist/spa/assets/QExpansionItem-sghN-B7_.js +1 -0
- package/src/client/dist/spa/assets/QPage-DL4rY7LD.js +1 -0
- package/src/client/dist/spa/assets/QSeparator-MRAsTeNf.js +1 -0
- package/src/client/dist/spa/assets/QSpinnerDots-Bu0GloxK.js +1 -0
- package/src/client/dist/spa/assets/QTooltip-Cuj49WIu.js +1 -0
- package/src/client/dist/spa/assets/SettingsPage-50Nqrcsk.js +1 -0
- package/src/client/dist/spa/assets/SettingsPage-DV5avRbc.css +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-CFC48jKO.css +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-L46GJjcy.js +2 -0
- package/src/client/dist/spa/assets/_plugin-vue_export-helper-Bo392ayB.js +1 -0
- package/src/client/dist/spa/assets/abap-Co3wj02O.js +1 -0
- package/src/client/dist/spa/assets/apex-CUKwGs62.js +1 -0
- package/src/client/dist/spa/assets/azcli-DMImymmY.js +1 -0
- package/src/client/dist/spa/assets/bat--P_y70-E.js +1 -0
- package/src/client/dist/spa/assets/bicep-C3w6oSfK.js +2 -0
- package/src/client/dist/spa/assets/cameligo-D9NSR4Rj.js +1 -0
- package/src/client/dist/spa/assets/clojure-BMcQme0t.js +1 -0
- package/src/client/dist/spa/assets/codicon-CgENjH2v.ttf +0 -0
- package/src/client/dist/spa/assets/coffee-BbMZaWx7.js +1 -0
- package/src/client/dist/spa/assets/cpp-CbrtEGgw.js +1 -0
- package/src/client/dist/spa/assets/csharp-Bc0fjUxA.js +1 -0
- package/src/client/dist/spa/assets/csp-DmbXuMT0.js +1 -0
- package/src/client/dist/spa/assets/css-gdwCt5by.js +3 -0
- package/src/client/dist/spa/assets/css.worker-D1piIYC4.js +102 -0
- package/src/client/dist/spa/assets/cssMode-DO8hqIpD.js +4 -0
- package/src/client/dist/spa/assets/cypher-ocmmfoQr.js +1 -0
- package/src/client/dist/spa/assets/dart-DbZ5eklb.js +1 -0
- package/src/client/dist/spa/assets/dockerfile-BLaMayDc.js +1 -0
- package/src/client/dist/spa/assets/ecl-LxXpHirr.js +1 -0
- package/src/client/dist/spa/assets/editor-COGk2gAX.css +1 -0
- package/src/client/dist/spa/assets/editor-CS3NEPi9.css +1 -0
- package/src/client/dist/spa/assets/editor.api-BZP41lht.js +818 -0
- package/src/client/dist/spa/assets/editor.main-BOjf9Jyl.js +53 -0
- package/src/client/dist/spa/assets/editor.worker-CJ9iTmkr.js +26 -0
- package/src/client/dist/spa/assets/elixir-C_geKt5o.js +1 -0
- package/src/client/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-OUIwM9U8.woff +0 -0
- package/src/client/dist/spa/assets/flow9-DE2fI2ca.js +1 -0
- package/src/client/dist/spa/assets/formatters-CXx5Gzsp.js +1 -0
- package/src/client/dist/spa/assets/freemarker2-QAd0phKD.js +3 -0
- package/src/client/dist/spa/assets/fsharp-CJD6fImD.js +1 -0
- package/src/client/dist/spa/assets/go-jUCqQ7bD.js +1 -0
- package/src/client/dist/spa/assets/graphql-rw7g9h7D.js +1 -0
- package/src/client/dist/spa/assets/handlebars-D40ZA-yu.js +1 -0
- package/src/client/dist/spa/assets/hcl-BKX27Mn7.js +1 -0
- package/src/client/dist/spa/assets/html-Bzo97Bk0.js +1 -0
- package/src/client/dist/spa/assets/html.worker-C4q4XMPn.js +509 -0
- package/src/client/dist/spa/assets/htmlMode-7HShfg96.js +4 -0
- package/src/client/dist/spa/assets/i18n-BiMAFoN_.js +1 -0
- package/src/client/dist/spa/assets/index-CaOiQq0z.js +5 -0
- package/src/client/dist/spa/assets/{index-BThMCiY7.css → index-eX_lKHSg.css} +1 -1
- package/src/client/dist/spa/assets/ini-CrXjga2H.js +1 -0
- package/src/client/dist/spa/assets/java-D4jksGBb.js +1 -0
- package/src/client/dist/spa/assets/javascript-DpFlF6yx.js +1 -0
- package/src/client/dist/spa/assets/json.worker-C9p7xCYk.js +65 -0
- package/src/client/dist/spa/assets/jsonMode-DxEb1VXU.js +10 -0
- package/src/client/dist/spa/assets/julia-CbWxfkeS.js +1 -0
- package/src/client/dist/spa/assets/kotlin-B26Yx80V.js +1 -0
- package/src/client/dist/spa/assets/less-DFzn-zC9.js +2 -0
- package/src/client/dist/spa/assets/lexon-C-w-W8Yv.js +1 -0
- package/src/client/dist/spa/assets/liquid-IpMvWkVS.js +1 -0
- package/src/client/dist/spa/assets/lua-CHuE_HoG.js +1 -0
- package/src/client/dist/spa/assets/m3-DEFZN2qS.js +1 -0
- package/src/client/dist/spa/assets/markdown-Cbt4TlFt.js +1 -0
- package/src/client/dist/spa/assets/mdx-BM5S9XtA.js +1 -0
- package/src/client/dist/spa/assets/mips-C6m4XECw.js +1 -0
- package/src/client/dist/spa/assets/monaco.contribution-Cpcgk43V.js +2 -0
- package/src/client/dist/spa/assets/msdax-un0CFb_S.js +1 -0
- package/src/client/dist/spa/assets/mysql-CuAPeiOV.js +1 -0
- package/src/client/dist/spa/assets/nodes-Bo-5xQjA.js +1 -0
- package/src/client/dist/spa/assets/objective-c-DLVMdxAC.js +1 -0
- package/src/client/dist/spa/assets/pascal-BGCThuPY.js +1 -0
- package/src/client/dist/spa/assets/pascaligo-DfxSVpdo.js +1 -0
- package/src/client/dist/spa/assets/perl-BOE6y94t.js +1 -0
- package/src/client/dist/spa/assets/pgsql-Dn7JkY4F.js +1 -0
- package/src/client/dist/spa/assets/php-r1gD0KyT.js +1 -0
- package/src/client/dist/spa/assets/pla-CgXknhb0.js +1 -0
- package/src/client/dist/spa/assets/position-engine-CDz__T_5.js +1 -0
- package/src/client/dist/spa/assets/postiats-CsIEtnRB.js +1 -0
- package/src/client/dist/spa/assets/powerquery-yNJCmC_6.js +1 -0
- package/src/client/dist/spa/assets/powershell-CQcz1SqH.js +1 -0
- package/src/client/dist/spa/assets/protobuf-BmC34uvO.js +2 -0
- package/src/client/dist/spa/assets/pug-C20znvWM.js +1 -0
- package/src/client/dist/spa/assets/python-CBiKH2mZ.js +1 -0
- package/src/client/dist/spa/assets/qsharp-B7bnARMS.js +1 -0
- package/src/client/dist/spa/assets/r-ClvcLdqC.js +1 -0
- package/src/client/dist/spa/assets/razor-BV3hIY51.js +1 -0
- package/src/client/dist/spa/assets/redis-DCyda7_S.js +1 -0
- package/src/client/dist/spa/assets/redshift-BtWDr4pb.js +1 -0
- package/src/client/dist/spa/assets/restructuredtext-CLcnlkhl.js +1 -0
- package/src/client/dist/spa/assets/ruby-DY0SOSSZ.js +1 -0
- package/src/client/dist/spa/assets/runtime-core.esm-bundler-BLPLlWMG.js +1 -0
- package/src/client/dist/spa/assets/rust-JQd-fJZI.js +1 -0
- package/src/client/dist/spa/assets/sb-BV2j8yFF.js +1 -0
- package/src/client/dist/spa/assets/scala-DwbnREDs.js +1 -0
- package/src/client/dist/spa/assets/scheme-CrtA-vei.js +1 -0
- package/src/client/dist/spa/assets/scss-VxQz3zmI.js +3 -0
- package/src/client/dist/spa/assets/shell-CP9faqFI.js +1 -0
- package/src/client/dist/spa/assets/solidity-9IIb0b89.js +1 -0
- package/src/client/dist/spa/assets/sophia-D2LQU2AD.js +1 -0
- package/src/client/dist/spa/assets/sparql-DONCa5dy.js +1 -0
- package/src/client/dist/spa/assets/sql-DaAAHGEt.js +1 -0
- package/src/client/dist/spa/assets/st-CRY2V-j3.js +1 -0
- package/src/client/dist/spa/assets/swift-BlKbfloF.js +1 -0
- package/src/client/dist/spa/assets/systemverilog-B_h9Q_T_.js +1 -0
- package/src/client/dist/spa/assets/tcl-C4wN3A6M.js +1 -0
- package/src/client/dist/spa/assets/ts.worker-Cj3zTgVE.js +51353 -0
- package/src/client/dist/spa/assets/tsMode-DUqyritq.js +11 -0
- package/src/client/dist/spa/assets/twig-DDdaBLC9.js +1 -0
- package/src/client/dist/spa/assets/typescript-BvZDZzaz.js +1 -0
- package/src/client/dist/spa/assets/typespec-Dc1ipt8A.js +1 -0
- package/src/client/dist/spa/assets/use-checkbox-Dwcwf6Nj.js +1 -0
- package/src/client/dist/spa/assets/use-quasar-DMvrrord.js +1 -0
- package/src/client/dist/spa/assets/vb-C4BXIvrh.js +1 -0
- package/src/client/dist/spa/assets/vue-i18n-CoZsbeQK.js +3 -0
- package/src/client/dist/spa/assets/wgsl-XVg3Pi-r.js +298 -0
- package/src/client/dist/spa/assets/xml-BgsHEniP.js +1 -0
- package/src/client/dist/spa/assets/yaml-C-Mr6Xov.js +1 -0
- package/src/client/dist/spa/index.html +5 -3
- package/src/mcp-server/kobo-tasks-server.ts +2 -0
- package/src/client/dist/spa/assets/ActivityFeed-Bie-lcn7.js +0 -60
- package/src/client/dist/spa/assets/ActivityFeed-D88GOO2z.css +0 -1
- package/src/client/dist/spa/assets/CreatePage-BlgXsrJO.css +0 -1
- package/src/client/dist/spa/assets/CreatePage-OC-fnNGP.js +0 -2
- package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-BepdiOnY.woff +0 -0
- package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-4ZhHFPot.woff +0 -0
- package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-CNa4tw4G.woff +0 -0
- package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-CHKg1YId.woff +0 -0
- package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-yBxCyPWP.woff +0 -0
- package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-3fZ6d7DD.woff +0 -0
- package/src/client/dist/spa/assets/MainLayout-91cUoVYa.css +0 -1
- package/src/client/dist/spa/assets/MainLayout-BIQNJixM.js +0 -1
- package/src/client/dist/spa/assets/QBadge-DbE3eSf1.js +0 -1
- package/src/client/dist/spa/assets/QDialog-Cd_4PvgW.js +0 -1
- package/src/client/dist/spa/assets/QExpansionItem-pMQDDRMv.js +0 -1
- package/src/client/dist/spa/assets/QPage-lhV4XbI2.js +0 -1
- package/src/client/dist/spa/assets/QSpinnerDots-ByNZaBWw.js +0 -1
- package/src/client/dist/spa/assets/QTooltip-6GSFtFKP.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-BPH70mno.css +0 -1
- package/src/client/dist/spa/assets/SettingsPage-s2WJBreM.js +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-Dhkuuhf8.css +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-XT26aCJE.js +0 -2
- package/src/client/dist/spa/assets/_plugin-vue_export-helper-B6FaNy4R.js +0 -1
- package/src/client/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
- package/src/client/dist/spa/assets/index-BoQWbZtE.js +0 -5
- package/src/client/dist/spa/assets/nodes-CXdiSdC2.js +0 -1
- package/src/client/dist/spa/assets/use-checkbox-Z9pfihkw.js +0 -1
- package/src/client/dist/spa/assets/use-quasar-CtCe3LQU.js +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { getPackageVersion } from '../utils/paths.js';
|
|
3
4
|
// Gherkin keywords (French and English)
|
|
4
5
|
const GHERKIN_PATTERN = /^(Scénario|Étant donné|Quand|Alors|Scenario|Given|When|Then|Feature|Fonctionnalité|And|Et|But|Mais)/i;
|
|
5
6
|
// C2: rpcIdCounter encapsulated in a closure to avoid module-level mutable state
|
|
@@ -53,10 +54,11 @@ export async function callMcpTool(mcpProcess, toolName, args) {
|
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
56
|
let buffer = '';
|
|
56
|
-
// C1: 30s timeout
|
|
57
|
+
// C1: 30s timeout — I7: kill the MCP process on timeout to avoid zombie
|
|
57
58
|
const timeout = setTimeout(() => {
|
|
58
59
|
mcpProcess.stdout?.removeListener('data', onData);
|
|
59
60
|
mcpProcess.stdout?.removeListener('error', onError);
|
|
61
|
+
mcpProcess.kill();
|
|
60
62
|
reject(new Error(`callMcpTool('${toolName}') timed out after 30s`));
|
|
61
63
|
}, 30_000);
|
|
62
64
|
const onData = (chunk) => {
|
|
@@ -155,7 +157,7 @@ async function initializeMcp(mcpProcess) {
|
|
|
155
157
|
params: {
|
|
156
158
|
protocolVersion: '2024-11-05',
|
|
157
159
|
capabilities: {},
|
|
158
|
-
clientInfo: { name: 'kobo', version:
|
|
160
|
+
clientInfo: { name: 'kobo', version: getPackageVersion() },
|
|
159
161
|
},
|
|
160
162
|
});
|
|
161
163
|
await new Promise((resolve, reject) => {
|
|
@@ -164,9 +166,10 @@ async function initializeMcp(mcpProcess) {
|
|
|
164
166
|
return;
|
|
165
167
|
}
|
|
166
168
|
let buffer = '';
|
|
167
|
-
// C1: 10s timeout for initialization
|
|
169
|
+
// C1: 10s timeout for initialization — I7: kill the MCP process on timeout
|
|
168
170
|
const timeout = setTimeout(() => {
|
|
169
171
|
mcpProcess.stdout?.removeListener('data', onData);
|
|
172
|
+
mcpProcess.kill();
|
|
170
173
|
reject(new Error('initializeMcp timed out after 10s'));
|
|
171
174
|
}, 10_000);
|
|
172
175
|
const onData = (chunk) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getPrStatusAsync } from '../utils/git-ops.js';
|
|
2
2
|
import { emitEphemeral } from './websocket-service.js';
|
|
3
3
|
import { archiveWorkspace, listWorkspaces } from './workspace-service.js';
|
|
4
4
|
// ── PR Watcher ────────────────────────────────────────────────────────────────
|
|
@@ -11,9 +11,10 @@ import { archiveWorkspace, listWorkspaces } from './workspace-service.js';
|
|
|
11
11
|
// unarchived workspaces.
|
|
12
12
|
const POLL_INTERVAL_MS = 30 * 1000; // 30 seconds
|
|
13
13
|
let timer = null;
|
|
14
|
+
let checking = false;
|
|
14
15
|
/** Tracks the last known PR state per workspace to detect transitions. */
|
|
15
16
|
const lastKnownState = new Map();
|
|
16
|
-
function checkPrStatuses() {
|
|
17
|
+
async function checkPrStatuses() {
|
|
17
18
|
const workspaces = listWorkspaces(false); // non-archived only
|
|
18
19
|
// Clean up entries for workspaces that no longer exist
|
|
19
20
|
for (const id of lastKnownState.keys()) {
|
|
@@ -26,7 +27,7 @@ function checkPrStatuses() {
|
|
|
26
27
|
if (['extracting', 'brainstorming', 'executing'].includes(ws.status))
|
|
27
28
|
continue;
|
|
28
29
|
try {
|
|
29
|
-
const pr =
|
|
30
|
+
const pr = await getPrStatusAsync(ws.projectPath, ws.workingBranch);
|
|
30
31
|
if (!pr)
|
|
31
32
|
continue;
|
|
32
33
|
const prev = lastKnownState.get(ws.id);
|
|
@@ -47,15 +48,35 @@ function checkPrStatuses() {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
function scheduleNext() {
|
|
52
|
+
timer = setTimeout(async () => {
|
|
53
|
+
if (checking) {
|
|
54
|
+
// Previous run still in progress — skip and reschedule
|
|
55
|
+
scheduleNext();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
checking = true;
|
|
59
|
+
try {
|
|
60
|
+
await checkPrStatuses();
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.error('[pr-watcher] Unexpected error in checkPrStatuses:', err);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
checking = false;
|
|
67
|
+
scheduleNext();
|
|
68
|
+
}
|
|
69
|
+
}, POLL_INTERVAL_MS);
|
|
70
|
+
timer.unref?.();
|
|
71
|
+
}
|
|
50
72
|
export function startPrWatcher() {
|
|
51
73
|
if (timer)
|
|
52
74
|
return;
|
|
53
|
-
|
|
54
|
-
timer.unref?.();
|
|
75
|
+
scheduleNext();
|
|
55
76
|
}
|
|
56
77
|
export function stopPrWatcher() {
|
|
57
78
|
if (timer) {
|
|
58
|
-
|
|
79
|
+
clearTimeout(timer);
|
|
59
80
|
timer = null;
|
|
60
81
|
}
|
|
61
82
|
}
|
|
@@ -3,6 +3,9 @@ import { getDb } from '../db/index.js';
|
|
|
3
3
|
// ── State ──────────────────────────────────────────────────────────────────────
|
|
4
4
|
/** Maps each WS client to the set of workspaceIds they are subscribed to */
|
|
5
5
|
const clients = new Map();
|
|
6
|
+
/** I6: Per-workspace emit counter for periodic cleanup */
|
|
7
|
+
const emitCounters = new Map();
|
|
8
|
+
const EMIT_CLEANUP_THRESHOLD = 2000;
|
|
6
9
|
let messageHandler = null;
|
|
7
10
|
export function setMessageHandler(handler) {
|
|
8
11
|
messageHandler = handler;
|
|
@@ -67,10 +70,21 @@ export function handleConnection(ws) {
|
|
|
67
70
|
ws.send(JSON.stringify({ type: 'error', payload: { message: `Unknown message type: ${type}` } }));
|
|
68
71
|
}
|
|
69
72
|
});
|
|
73
|
+
// Ping every 30s to keep the connection alive and detect stale clients
|
|
74
|
+
const pingInterval = setInterval(() => {
|
|
75
|
+
if (ws.readyState === ws.OPEN) {
|
|
76
|
+
ws.ping();
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
clearInterval(pingInterval);
|
|
80
|
+
}
|
|
81
|
+
}, 30_000);
|
|
70
82
|
ws.on('close', () => {
|
|
83
|
+
clearInterval(pingInterval);
|
|
71
84
|
clients.delete(ws);
|
|
72
85
|
});
|
|
73
86
|
ws.on('error', () => {
|
|
87
|
+
clearInterval(pingInterval);
|
|
74
88
|
clients.delete(ws);
|
|
75
89
|
});
|
|
76
90
|
}
|
|
@@ -87,6 +101,15 @@ export function emit(workspaceId, type, payload, sessionId) {
|
|
|
87
101
|
try {
|
|
88
102
|
const db = getDb();
|
|
89
103
|
db.prepare('INSERT INTO ws_events (id, workspace_id, type, payload, session_id, created_at) VALUES (?, ?, ?, ?, ?, ?)').run(id, workspaceId, type, JSON.stringify(payload), sessionId ?? null, createdAt);
|
|
104
|
+
// I6: Periodic cleanup — increment counter and trigger cleanup when threshold is reached
|
|
105
|
+
const count = (emitCounters.get(workspaceId) ?? 0) + 1;
|
|
106
|
+
if (count >= EMIT_CLEANUP_THRESHOLD) {
|
|
107
|
+
cleanupOldEvents(workspaceId);
|
|
108
|
+
emitCounters.set(workspaceId, 0);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
emitCounters.set(workspaceId, count);
|
|
112
|
+
}
|
|
90
113
|
}
|
|
91
114
|
catch (err) {
|
|
92
115
|
console.error(`[websocket-service] Failed to persist event (workspace=${workspaceId}, type=${type}):`, err);
|
|
@@ -148,16 +171,16 @@ export function handleSyncRequest(ws, lastEventId, workspaceIds) {
|
|
|
148
171
|
.all(...resolvedIds, lastRow.rowid);
|
|
149
172
|
}
|
|
150
173
|
else {
|
|
151
|
-
// lastEventId not found — send
|
|
174
|
+
// I7: lastEventId not found — send events capped to avoid unbounded memory usage
|
|
152
175
|
rows = db
|
|
153
|
-
.prepare(`SELECT * FROM ws_events WHERE workspace_id IN (${placeholders}) ORDER BY rowid ASC`)
|
|
176
|
+
.prepare(`SELECT * FROM ws_events WHERE workspace_id IN (${placeholders}) ORDER BY rowid ASC LIMIT 10000`)
|
|
154
177
|
.all(...resolvedIds);
|
|
155
178
|
}
|
|
156
179
|
}
|
|
157
180
|
else {
|
|
158
|
-
// No lastEventId — send all events
|
|
181
|
+
// No lastEventId — send all events (capped to avoid unbounded memory usage)
|
|
159
182
|
rows = db
|
|
160
|
-
.prepare(`SELECT * FROM ws_events WHERE workspace_id IN (${placeholders}) ORDER BY rowid ASC`)
|
|
183
|
+
.prepare(`SELECT * FROM ws_events WHERE workspace_id IN (${placeholders}) ORDER BY rowid ASC LIMIT 10000`)
|
|
161
184
|
.all(...resolvedIds);
|
|
162
185
|
}
|
|
163
186
|
const events = rows.map((row) => {
|
|
@@ -178,6 +201,13 @@ export function handleSyncRequest(ws, lastEventId, workspaceIds) {
|
|
|
178
201
|
};
|
|
179
202
|
});
|
|
180
203
|
ws.send(JSON.stringify({ type: 'sync:response', payload: { events } }));
|
|
204
|
+
// Trigger cleanup when the event count is high to prevent unbounded growth
|
|
205
|
+
const CLEANUP_THRESHOLD = 5000;
|
|
206
|
+
if (rows.length >= CLEANUP_THRESHOLD) {
|
|
207
|
+
for (const wid of resolvedIds) {
|
|
208
|
+
cleanupOldEvents(wid);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
181
211
|
}
|
|
182
212
|
// ── Cleanup ────────────────────────────────────────────────────────────────────
|
|
183
213
|
/**
|
|
@@ -210,3 +240,10 @@ export function getClientCount() {
|
|
|
210
240
|
export function _getClients() {
|
|
211
241
|
return clients;
|
|
212
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Get the internal emit counters map — exposed for testing only.
|
|
245
|
+
* @internal
|
|
246
|
+
*/
|
|
247
|
+
export function _getEmitCounters() {
|
|
248
|
+
return emitCounters;
|
|
249
|
+
}
|
|
@@ -88,19 +88,30 @@ export function updateWorkspaceStatus(id, status) {
|
|
|
88
88
|
export function updateWorkspaceName(id, name) {
|
|
89
89
|
const db = getDb();
|
|
90
90
|
const now = new Date().toISOString();
|
|
91
|
-
db.prepare('UPDATE workspaces SET name = ?, updated_at = ? WHERE id = ?').run(name, now, id);
|
|
91
|
+
const result = db.prepare('UPDATE workspaces SET name = ?, updated_at = ? WHERE id = ?').run(name, now, id);
|
|
92
|
+
if (result.changes === 0) {
|
|
93
|
+
throw new Error(`Workspace '${id}' not found`);
|
|
94
|
+
}
|
|
92
95
|
return getWorkspace(id);
|
|
93
96
|
}
|
|
94
97
|
export function updateWorkspaceModel(id, model) {
|
|
95
98
|
const db = getDb();
|
|
96
99
|
const now = new Date().toISOString();
|
|
97
|
-
db.prepare('UPDATE workspaces SET model = ?, updated_at = ? WHERE id = ?').run(model, now, id);
|
|
100
|
+
const result = db.prepare('UPDATE workspaces SET model = ?, updated_at = ? WHERE id = ?').run(model, now, id);
|
|
101
|
+
if (result.changes === 0) {
|
|
102
|
+
throw new Error(`Workspace '${id}' not found`);
|
|
103
|
+
}
|
|
98
104
|
return getWorkspace(id);
|
|
99
105
|
}
|
|
100
106
|
export function updateWorkspacePermissionMode(id, permissionMode) {
|
|
101
107
|
const db = getDb();
|
|
102
108
|
const now = new Date().toISOString();
|
|
103
|
-
|
|
109
|
+
const result = db
|
|
110
|
+
.prepare('UPDATE workspaces SET permission_mode = ?, updated_at = ? WHERE id = ?')
|
|
111
|
+
.run(permissionMode, now, id);
|
|
112
|
+
if (result.changes === 0) {
|
|
113
|
+
throw new Error(`Workspace '${id}' not found`);
|
|
114
|
+
}
|
|
104
115
|
return getWorkspace(id);
|
|
105
116
|
}
|
|
106
117
|
export function updateDevServerStatus(id, status) {
|
|
@@ -127,6 +138,11 @@ export function createTask(workspaceId, data) {
|
|
|
127
138
|
const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
128
139
|
return mapTask(row);
|
|
129
140
|
}
|
|
141
|
+
export function getTask(taskId, workspaceId) {
|
|
142
|
+
const db = getDb();
|
|
143
|
+
const row = db.prepare('SELECT * FROM tasks WHERE id = ? AND workspace_id = ?').get(taskId, workspaceId);
|
|
144
|
+
return row ? mapTask(row) : null;
|
|
145
|
+
}
|
|
130
146
|
export function listTasks(workspaceId) {
|
|
131
147
|
const db = getDb();
|
|
132
148
|
const rows = db
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
1
|
+
import { execFile as execFileCb, execFileSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const execFileAsync = promisify(execFileCb);
|
|
2
6
|
function git(repoPath, args) {
|
|
3
7
|
return execFileSync('git', args, { cwd: repoPath, encoding: 'utf-8' }).trim();
|
|
4
8
|
}
|
|
@@ -99,9 +103,26 @@ export function pushBranch(repoPath, branchName, remote = 'origin') {
|
|
|
99
103
|
throw new Error(`Failed to push branch '${branchName}' to '${remote}': ${message}`);
|
|
100
104
|
}
|
|
101
105
|
}
|
|
106
|
+
/** Try a git command with `base`, falling back to `origin/base` if the local ref is missing. */
|
|
107
|
+
function resolveBase(repoPath, base) {
|
|
108
|
+
try {
|
|
109
|
+
git(repoPath, ['rev-parse', '--verify', base]);
|
|
110
|
+
return base;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
try {
|
|
114
|
+
git(repoPath, ['rev-parse', '--verify', `origin/${base}`]);
|
|
115
|
+
return `origin/${base}`;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return base;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
102
122
|
export function getCommitCount(repoPath, base, head) {
|
|
103
123
|
try {
|
|
104
|
-
const
|
|
124
|
+
const ref = resolveBase(repoPath, base);
|
|
125
|
+
const output = git(repoPath, ['rev-list', '--count', `${ref}..${head}`]);
|
|
105
126
|
return parseInt(output, 10) || 0;
|
|
106
127
|
}
|
|
107
128
|
catch {
|
|
@@ -110,7 +131,8 @@ export function getCommitCount(repoPath, base, head) {
|
|
|
110
131
|
}
|
|
111
132
|
export function getStructuredDiffStatsBetween(repoPath, base, head) {
|
|
112
133
|
try {
|
|
113
|
-
const
|
|
134
|
+
const ref = resolveBase(repoPath, base);
|
|
135
|
+
const output = git(repoPath, ['diff', '--shortstat', `${ref}...${head}`]);
|
|
114
136
|
return parseDiffShortstat(output);
|
|
115
137
|
}
|
|
116
138
|
catch {
|
|
@@ -119,7 +141,8 @@ export function getStructuredDiffStatsBetween(repoPath, base, head) {
|
|
|
119
141
|
}
|
|
120
142
|
export function getCommitsBetween(repoPath, base, head) {
|
|
121
143
|
try {
|
|
122
|
-
|
|
144
|
+
const ref = resolveBase(repoPath, base);
|
|
145
|
+
return git(repoPath, ['log', `${ref}..${head}`, '--pretty=format:- %s (%h)', '--no-merges']);
|
|
123
146
|
}
|
|
124
147
|
catch {
|
|
125
148
|
return '';
|
|
@@ -151,6 +174,107 @@ export function getPrStatus(repoPath, branchName) {
|
|
|
151
174
|
return null;
|
|
152
175
|
}
|
|
153
176
|
}
|
|
177
|
+
/** List files changed between base and HEAD (committed), plus working tree changes. */
|
|
178
|
+
export function getChangedFiles(repoPath, base) {
|
|
179
|
+
const ref = resolveBase(repoPath, base);
|
|
180
|
+
const files = [];
|
|
181
|
+
const seen = new Set();
|
|
182
|
+
// Committed changes (base..HEAD)
|
|
183
|
+
try {
|
|
184
|
+
const output = git(repoPath, ['diff', '--name-status', `${ref}...HEAD`]);
|
|
185
|
+
for (const line of output.split('\n')) {
|
|
186
|
+
if (!line)
|
|
187
|
+
continue;
|
|
188
|
+
const [statusCode, ...pathParts] = line.split('\t');
|
|
189
|
+
const filePath = pathParts.join('\t').replace(/\/$/, '');
|
|
190
|
+
if (!filePath)
|
|
191
|
+
continue;
|
|
192
|
+
let status = 'modified';
|
|
193
|
+
if (statusCode?.startsWith('A'))
|
|
194
|
+
status = 'added';
|
|
195
|
+
else if (statusCode?.startsWith('D'))
|
|
196
|
+
status = 'deleted';
|
|
197
|
+
else if (statusCode?.startsWith('R'))
|
|
198
|
+
status = 'renamed';
|
|
199
|
+
files.push({ path: filePath, status });
|
|
200
|
+
seen.add(filePath);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// No commits yet
|
|
205
|
+
}
|
|
206
|
+
// Working tree changes (uncommitted)
|
|
207
|
+
try {
|
|
208
|
+
const output = git(repoPath, ['status', '--porcelain', '-uall']);
|
|
209
|
+
for (const line of output.split('\n')) {
|
|
210
|
+
if (!line)
|
|
211
|
+
continue;
|
|
212
|
+
const filePath = line.substring(3).replace(/\/$/, '');
|
|
213
|
+
if (!filePath || seen.has(filePath))
|
|
214
|
+
continue;
|
|
215
|
+
const x = line[0];
|
|
216
|
+
const y = line[1];
|
|
217
|
+
let status = 'modified';
|
|
218
|
+
if (x === '?' && y === '?')
|
|
219
|
+
status = 'added';
|
|
220
|
+
else if (x === 'A' || y === 'A')
|
|
221
|
+
status = 'added';
|
|
222
|
+
else if (x === 'D' || y === 'D')
|
|
223
|
+
status = 'deleted';
|
|
224
|
+
files.push({ path: filePath, status });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Ignore
|
|
229
|
+
}
|
|
230
|
+
return files;
|
|
231
|
+
}
|
|
232
|
+
/** Get the original content of a file at a given ref. Returns null if the file didn't exist. */
|
|
233
|
+
export function getFileAtRef(repoPath, ref, filePath) {
|
|
234
|
+
const resolvedRef = resolveBase(repoPath, ref);
|
|
235
|
+
try {
|
|
236
|
+
return git(repoPath, ['show', `${resolvedRef}:${filePath}`]);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** Get the current content of a file in the worktree. Returns null if the file doesn't exist. */
|
|
243
|
+
export function getFileContent(repoPath, filePath) {
|
|
244
|
+
try {
|
|
245
|
+
return readFileSync(join(repoPath, filePath), 'utf-8');
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export function getWorkingTreeStatus(repoPath) {
|
|
252
|
+
try {
|
|
253
|
+
const output = git(repoPath, ['status', '--porcelain']);
|
|
254
|
+
let staged = 0;
|
|
255
|
+
let modified = 0;
|
|
256
|
+
let untracked = 0;
|
|
257
|
+
for (const line of output.split('\n')) {
|
|
258
|
+
if (!line)
|
|
259
|
+
continue;
|
|
260
|
+
const x = line[0]; // index status
|
|
261
|
+
const y = line[1]; // worktree status
|
|
262
|
+
if (x === '?' && y === '?') {
|
|
263
|
+
untracked++;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
if (x !== ' ' && x !== '?')
|
|
267
|
+
staged++;
|
|
268
|
+
if (y !== ' ' && y !== '?')
|
|
269
|
+
modified++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { staged, modified, untracked };
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return { staged: 0, modified: 0, untracked: 0 };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
154
278
|
/** Count commits ahead of upstream. Returns -1 if no upstream is set. */
|
|
155
279
|
export function getUnpushedCount(repoPath) {
|
|
156
280
|
try {
|
|
@@ -172,3 +296,47 @@ export function getDiffStatsBetween(repoPath, base, head) {
|
|
|
172
296
|
return '';
|
|
173
297
|
}
|
|
174
298
|
}
|
|
299
|
+
// ── Async versions ───────────────────────────────────────────────────────────
|
|
300
|
+
// Non-blocking alternatives for hot paths (pr-watcher, route handlers).
|
|
301
|
+
// The sync versions above are kept for callers that haven't migrated yet.
|
|
302
|
+
export async function getPrUrlAsync(repoPath, branchName) {
|
|
303
|
+
try {
|
|
304
|
+
const { stdout } = await execFileAsync('gh', ['pr', 'view', branchName, '--json', 'url', '--jq', '.url'], {
|
|
305
|
+
cwd: repoPath,
|
|
306
|
+
encoding: 'utf-8',
|
|
307
|
+
});
|
|
308
|
+
return stdout.trim() || null;
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
export async function getPrStatusAsync(repoPath, branchName) {
|
|
315
|
+
try {
|
|
316
|
+
const { stdout } = await execFileAsync('gh', ['pr', 'view', branchName, '--json', 'state,url'], {
|
|
317
|
+
cwd: repoPath,
|
|
318
|
+
encoding: 'utf-8',
|
|
319
|
+
});
|
|
320
|
+
const raw = stdout.trim();
|
|
321
|
+
if (!raw)
|
|
322
|
+
return null;
|
|
323
|
+
const parsed = JSON.parse(raw);
|
|
324
|
+
return { state: parsed.state, url: parsed.url };
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/** Async version of getUnpushedCount. Returns -1 if no upstream is set. */
|
|
331
|
+
export async function getUnpushedCountAsync(repoPath) {
|
|
332
|
+
try {
|
|
333
|
+
const { stdout } = await execFileAsync('git', ['rev-list', '@{u}..HEAD', '--count'], {
|
|
334
|
+
cwd: repoPath,
|
|
335
|
+
encoding: 'utf-8',
|
|
336
|
+
});
|
|
337
|
+
return parseInt(stdout.trim(), 10) || 0;
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return -1; // no upstream
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -85,6 +85,19 @@ export function getCompiledMcpServerPath() {
|
|
|
85
85
|
export function getMcpServerSourcePath() {
|
|
86
86
|
return getPackageAssetPath('src', 'mcp-server', 'kobo-tasks-server.ts');
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Returns the version string from the root package.json. The result is cached
|
|
90
|
+
* after the first call so the file is only read once per process lifetime.
|
|
91
|
+
*/
|
|
92
|
+
let _cachedVersion = null;
|
|
93
|
+
export function getPackageVersion() {
|
|
94
|
+
if (_cachedVersion)
|
|
95
|
+
return _cachedVersion;
|
|
96
|
+
const pkgPath = getPackageAssetPath('package.json');
|
|
97
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
98
|
+
_cachedVersion = pkg.version;
|
|
99
|
+
return _cachedVersion;
|
|
100
|
+
}
|
|
88
101
|
/**
|
|
89
102
|
* Absolute path to the built Quasar SPA (src/client/dist/spa). Returns null
|
|
90
103
|
* if the SPA has not been built yet.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loicngr/kobo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"build:client": "cd src/client && npx quasar build",
|
|
54
54
|
"build:server": "npx tsc && chmod +x dist/server/index.js",
|
|
55
55
|
"build": "npm run build:client && npm run build:server",
|
|
56
|
+
"install-all": "npm install && cd src/client && npm install",
|
|
56
57
|
"start": "node dist/server/index.js",
|
|
57
58
|
"test": "vitest run --config vitest.config.ts --root .",
|
|
58
59
|
"test:watch": "vitest --config vitest.config.ts --root .",
|
|
@@ -65,10 +66,10 @@
|
|
|
65
66
|
"prepublishOnly": "npm run build"
|
|
66
67
|
},
|
|
67
68
|
"dependencies": {
|
|
68
|
-
"@hono/node-server": "^1.19.
|
|
69
|
+
"@hono/node-server": "^1.19.13",
|
|
69
70
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
70
71
|
"better-sqlite3": "^12.8.0",
|
|
71
|
-
"hono": "^4.12.
|
|
72
|
+
"hono": "^4.12.12",
|
|
72
73
|
"nanoid": "^5.1.7",
|
|
73
74
|
"ws": "^8.20.0"
|
|
74
75
|
},
|