@loicngr/kobo 1.2.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 +72 -10
- package/dist/server/db/schema.js +5 -0
- package/dist/server/index.js +63 -9
- package/dist/server/routes/workspaces.js +208 -52
- package/dist/server/services/agent-manager.js +101 -9
- package/dist/server/services/notion-service.js +6 -3
- package/dist/server/services/pr-watcher-service.js +82 -0
- package/dist/server/services/settings-service.js +41 -22
- package/dist/server/services/websocket-service.js +41 -4
- package/dist/server/services/workspace-service.js +25 -2
- package/dist/server/utils/git-ops.js +200 -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-CPfYmybV.js +0 -60
- package/src/client/dist/spa/assets/ActivityFeed-DBljh9rq.css +0 -1
- package/src/client/dist/spa/assets/CreatePage-BlgXsrJO.css +0 -1
- package/src/client/dist/spa/assets/CreatePage-C_c3Gr0F.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-BMxEROm4.css +0 -1
- package/src/client/dist/spa/assets/MainLayout-QtbVbbnd.js +0 -1
- package/src/client/dist/spa/assets/QBadge-CNojh9Rl.js +0 -1
- package/src/client/dist/spa/assets/QDialog-DgR7t6Vf.js +0 -1
- package/src/client/dist/spa/assets/QExpansionItem-VVjlYOIT.js +0 -1
- package/src/client/dist/spa/assets/QPage-DX4g-Dpe.js +0 -1
- package/src/client/dist/spa/assets/QSpinnerDots-DeCf9Lr-.js +0 -1
- package/src/client/dist/spa/assets/QTooltip-DKYJ8kVW.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-DjWKsLC-.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-Yv31Z9aG.css +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-DkM58caD.css +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-EAh91w9s.js +0 -2
- package/src/client/dist/spa/assets/_plugin-vue_export-helper-C6NdfBK4.js +0 -1
- package/src/client/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
- package/src/client/dist/spa/assets/index-C4WDJfjD.js +0 -5
- package/src/client/dist/spa/assets/nodes-irfhA8FK.js +0 -1
- package/src/client/dist/spa/assets/use-checkbox-BS9cbwg_.js +0 -1
- package/src/client/dist/spa/assets/use-quasar-CH0pSHUf.js +0 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { getPrStatusAsync } from '../utils/git-ops.js';
|
|
2
|
+
import { emitEphemeral } from './websocket-service.js';
|
|
3
|
+
import { archiveWorkspace, listWorkspaces } from './workspace-service.js';
|
|
4
|
+
// ── PR Watcher ────────────────────────────────────────────────────────────────
|
|
5
|
+
// Polls GitHub every POLL_INTERVAL_MS to detect merged/closed PRs and
|
|
6
|
+
// automatically archive the corresponding workspace.
|
|
7
|
+
//
|
|
8
|
+
// Only archives on a STATE TRANSITION from OPEN → CLOSED/MERGED.
|
|
9
|
+
// If a PR is already closed/merged when first seen (e.g. after unarchive),
|
|
10
|
+
// it is recorded but NOT acted upon — prevents re-archiving manually
|
|
11
|
+
// unarchived workspaces.
|
|
12
|
+
const POLL_INTERVAL_MS = 30 * 1000; // 30 seconds
|
|
13
|
+
let timer = null;
|
|
14
|
+
let checking = false;
|
|
15
|
+
/** Tracks the last known PR state per workspace to detect transitions. */
|
|
16
|
+
const lastKnownState = new Map();
|
|
17
|
+
async function checkPrStatuses() {
|
|
18
|
+
const workspaces = listWorkspaces(false); // non-archived only
|
|
19
|
+
// Clean up entries for workspaces that no longer exist
|
|
20
|
+
for (const id of lastKnownState.keys()) {
|
|
21
|
+
if (!workspaces.some((ws) => ws.id === id)) {
|
|
22
|
+
lastKnownState.delete(id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const ws of workspaces) {
|
|
26
|
+
// Only check workspaces that are not actively running an agent
|
|
27
|
+
if (['extracting', 'brainstorming', 'executing'].includes(ws.status))
|
|
28
|
+
continue;
|
|
29
|
+
try {
|
|
30
|
+
const pr = await getPrStatusAsync(ws.projectPath, ws.workingBranch);
|
|
31
|
+
if (!pr)
|
|
32
|
+
continue;
|
|
33
|
+
const prev = lastKnownState.get(ws.id);
|
|
34
|
+
lastKnownState.set(ws.id, pr.state);
|
|
35
|
+
// Only archive on a transition FROM OPEN — not on first sight of CLOSED/MERGED
|
|
36
|
+
if (prev === 'OPEN' && (pr.state === 'MERGED' || pr.state === 'CLOSED')) {
|
|
37
|
+
console.log(`[pr-watcher] PR ${pr.state.toLowerCase()} for workspace '${ws.name}' — archiving`);
|
|
38
|
+
archiveWorkspace(ws.id);
|
|
39
|
+
lastKnownState.delete(ws.id);
|
|
40
|
+
emitEphemeral(ws.id, 'workspace:archived', {
|
|
41
|
+
reason: `PR ${pr.state.toLowerCase()}`,
|
|
42
|
+
prUrl: pr.url,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.error(`[pr-watcher] Failed to check PR for workspace '${ws.name}':`, err instanceof Error ? err.message : err);
|
|
48
|
+
}
|
|
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
|
+
}
|
|
72
|
+
export function startPrWatcher() {
|
|
73
|
+
if (timer)
|
|
74
|
+
return;
|
|
75
|
+
scheduleNext();
|
|
76
|
+
}
|
|
77
|
+
export function stopPrWatcher() {
|
|
78
|
+
if (timer) {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
timer = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -50,13 +50,36 @@ Please:
|
|
|
50
50
|
1. Review the PR description on GitHub and improve it if needed (add a proper summary, screenshots if relevant, a test plan)
|
|
51
51
|
2. Verify that all acceptance criteria are checked
|
|
52
52
|
3. Post a comment on the PR summarizing what was done and any follow-up items
|
|
53
|
+
4. Do NOT add a "Generated with Claude Code" footer or any AI attribution to the PR description
|
|
53
54
|
`;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
const settingsMigrations = [
|
|
56
|
+
{
|
|
57
|
+
version: 1,
|
|
58
|
+
name: 'add-git-conventions',
|
|
59
|
+
migrate: ({ global, projects }) => {
|
|
60
|
+
if (typeof global.gitConventions !== 'string')
|
|
61
|
+
global.gitConventions = '';
|
|
62
|
+
for (const p of projects) {
|
|
63
|
+
if (typeof p.gitConventions !== 'string')
|
|
64
|
+
p.gitConventions = '';
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
version: 2,
|
|
70
|
+
name: 'add-dangerously-skip-permissions',
|
|
71
|
+
migrate: ({ global, projects }) => {
|
|
72
|
+
if (typeof global.dangerouslySkipPermissions !== 'boolean')
|
|
73
|
+
global.dangerouslySkipPermissions = true;
|
|
74
|
+
for (const p of projects) {
|
|
75
|
+
if (typeof p.dangerouslySkipPermissions !== 'boolean')
|
|
76
|
+
p.dangerouslySkipPermissions = true;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
/** Current settings schema version — always equals the highest migration version. */
|
|
82
|
+
export const SETTINGS_SCHEMA_VERSION = settingsMigrations.length > 0 ? settingsMigrations[settingsMigrations.length - 1].version : 0;
|
|
60
83
|
let settingsFilePath = getSettingsPath();
|
|
61
84
|
/** Override the settings file path (used by tests). */
|
|
62
85
|
export function _setSettingsPath(p) {
|
|
@@ -67,6 +90,7 @@ function defaultSettings() {
|
|
|
67
90
|
schemaVersion: SETTINGS_SCHEMA_VERSION,
|
|
68
91
|
global: {
|
|
69
92
|
defaultModel: 'auto',
|
|
93
|
+
dangerouslySkipPermissions: true,
|
|
70
94
|
prPromptTemplate: DEFAULT_PR_PROMPT_TEMPLATE,
|
|
71
95
|
gitConventions: DEFAULT_GIT_CONVENTIONS,
|
|
72
96
|
},
|
|
@@ -79,6 +103,7 @@ function defaultProjectSettings(projectPath) {
|
|
|
79
103
|
displayName: '',
|
|
80
104
|
defaultSourceBranch: '',
|
|
81
105
|
defaultModel: '',
|
|
106
|
+
dangerouslySkipPermissions: true,
|
|
82
107
|
prPromptTemplate: '',
|
|
83
108
|
gitConventions: '',
|
|
84
109
|
devServer: {
|
|
@@ -92,12 +117,10 @@ function pickKnownKeys(data, allowedKeys) {
|
|
|
92
117
|
}
|
|
93
118
|
/**
|
|
94
119
|
* Apply migrations sequentially to bring an older settings object up to
|
|
95
|
-
* SETTINGS_SCHEMA_VERSION.
|
|
96
|
-
*
|
|
97
|
-
* schemaVersion; callers should persist it back to disk.
|
|
120
|
+
* SETTINGS_SCHEMA_VERSION. Append-only — never edit or reorder shipped entries.
|
|
121
|
+
* The returned object carries the bumped schemaVersion; callers persist it.
|
|
98
122
|
*/
|
|
99
123
|
export function runSettingsMigrations(raw) {
|
|
100
|
-
// Ensure a baseline shape so we can safely read .global and .projects
|
|
101
124
|
const current = raw;
|
|
102
125
|
if (!current.global || typeof current.global !== 'object') {
|
|
103
126
|
current.global = {};
|
|
@@ -105,20 +128,13 @@ export function runSettingsMigrations(raw) {
|
|
|
105
128
|
if (!Array.isArray(current.projects)) {
|
|
106
129
|
current.projects = [];
|
|
107
130
|
}
|
|
108
|
-
// Detect legacy (pre-versioned) settings as v0
|
|
109
131
|
let version = typeof current.schemaVersion === 'number' ? current.schemaVersion : 0;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
for (const p of current.projects) {
|
|
116
|
-
if (typeof p.gitConventions !== 'string')
|
|
117
|
-
p.gitConventions = '';
|
|
132
|
+
for (const m of settingsMigrations) {
|
|
133
|
+
if (version < m.version) {
|
|
134
|
+
m.migrate({ global: current.global, projects: current.projects });
|
|
135
|
+
version = m.version;
|
|
118
136
|
}
|
|
119
|
-
version = 1;
|
|
120
137
|
}
|
|
121
|
-
// Future migrations go here — increment SETTINGS_SCHEMA_VERSION in lockstep.
|
|
122
138
|
current.schemaVersion = version;
|
|
123
139
|
return current;
|
|
124
140
|
}
|
|
@@ -177,6 +193,7 @@ export function getEffectiveSettings(projectPath) {
|
|
|
177
193
|
if (!project) {
|
|
178
194
|
return {
|
|
179
195
|
model: settings.global.defaultModel,
|
|
196
|
+
dangerouslySkipPermissions: settings.global.dangerouslySkipPermissions,
|
|
180
197
|
prPromptTemplate: settings.global.prPromptTemplate,
|
|
181
198
|
gitConventions: settings.global.gitConventions,
|
|
182
199
|
sourceBranch: '',
|
|
@@ -185,6 +202,7 @@ export function getEffectiveSettings(projectPath) {
|
|
|
185
202
|
}
|
|
186
203
|
return {
|
|
187
204
|
model: project.defaultModel || settings.global.defaultModel,
|
|
205
|
+
dangerouslySkipPermissions: project.dangerouslySkipPermissions ?? settings.global.dangerouslySkipPermissions,
|
|
188
206
|
prPromptTemplate: project.prPromptTemplate || settings.global.prPromptTemplate,
|
|
189
207
|
gitConventions: project.gitConventions || settings.global.gitConventions,
|
|
190
208
|
sourceBranch: project.defaultSourceBranch,
|
|
@@ -193,7 +211,7 @@ export function getEffectiveSettings(projectPath) {
|
|
|
193
211
|
}
|
|
194
212
|
export function updateGlobalSettings(data) {
|
|
195
213
|
const settings = readSettings();
|
|
196
|
-
const allowedGlobalKeys = ['defaultModel', 'prPromptTemplate', 'gitConventions'];
|
|
214
|
+
const allowedGlobalKeys = ['defaultModel', 'dangerouslySkipPermissions', 'prPromptTemplate', 'gitConventions'];
|
|
197
215
|
const filtered = pickKnownKeys(data, allowedGlobalKeys);
|
|
198
216
|
settings.global = { ...settings.global, ...filtered };
|
|
199
217
|
writeSettings(settings);
|
|
@@ -204,6 +222,7 @@ export function upsertProject(projectPath, data) {
|
|
|
204
222
|
'displayName',
|
|
205
223
|
'defaultSourceBranch',
|
|
206
224
|
'defaultModel',
|
|
225
|
+
'dangerouslySkipPermissions',
|
|
207
226
|
'prPromptTemplate',
|
|
208
227
|
'gitConventions',
|
|
209
228
|
'devServer',
|
|
@@ -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
|
+
}
|
|
@@ -22,6 +22,7 @@ function mapWorkspace(row) {
|
|
|
22
22
|
notionUrl: row.notion_url,
|
|
23
23
|
notionPageId: row.notion_page_id,
|
|
24
24
|
model: row.model,
|
|
25
|
+
permissionMode: (row.permission_mode ?? 'auto-accept'),
|
|
25
26
|
devServerStatus: row.dev_server_status,
|
|
26
27
|
archivedAt: row.archived_at,
|
|
27
28
|
createdAt: row.created_at,
|
|
@@ -87,13 +88,30 @@ export function updateWorkspaceStatus(id, status) {
|
|
|
87
88
|
export function updateWorkspaceName(id, name) {
|
|
88
89
|
const db = getDb();
|
|
89
90
|
const now = new Date().toISOString();
|
|
90
|
-
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
|
+
}
|
|
91
95
|
return getWorkspace(id);
|
|
92
96
|
}
|
|
93
97
|
export function updateWorkspaceModel(id, model) {
|
|
94
98
|
const db = getDb();
|
|
95
99
|
const now = new Date().toISOString();
|
|
96
|
-
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
|
+
}
|
|
104
|
+
return getWorkspace(id);
|
|
105
|
+
}
|
|
106
|
+
export function updateWorkspacePermissionMode(id, permissionMode) {
|
|
107
|
+
const db = getDb();
|
|
108
|
+
const now = new Date().toISOString();
|
|
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
|
+
}
|
|
97
115
|
return getWorkspace(id);
|
|
98
116
|
}
|
|
99
117
|
export function updateDevServerStatus(id, status) {
|
|
@@ -120,6 +138,11 @@ export function createTask(workspaceId, data) {
|
|
|
120
138
|
const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
121
139
|
return mapTask(row);
|
|
122
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
|
+
}
|
|
123
146
|
export function listTasks(workspaceId) {
|
|
124
147
|
const db = getDb();
|
|
125
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 '';
|
|
@@ -136,6 +159,135 @@ export function getPrUrl(repoPath, branchName) {
|
|
|
136
159
|
return null;
|
|
137
160
|
}
|
|
138
161
|
}
|
|
162
|
+
export function getPrStatus(repoPath, branchName) {
|
|
163
|
+
try {
|
|
164
|
+
const raw = execFileSync('gh', ['pr', 'view', branchName, '--json', 'state,url'], {
|
|
165
|
+
cwd: repoPath,
|
|
166
|
+
encoding: 'utf-8',
|
|
167
|
+
}).trim();
|
|
168
|
+
if (!raw)
|
|
169
|
+
return null;
|
|
170
|
+
const parsed = JSON.parse(raw);
|
|
171
|
+
return { state: parsed.state, url: parsed.url };
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
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
|
+
}
|
|
278
|
+
/** Count commits ahead of upstream. Returns -1 if no upstream is set. */
|
|
279
|
+
export function getUnpushedCount(repoPath) {
|
|
280
|
+
try {
|
|
281
|
+
const output = execFileSync('git', ['rev-list', '@{u}..HEAD', '--count'], {
|
|
282
|
+
cwd: repoPath,
|
|
283
|
+
encoding: 'utf-8',
|
|
284
|
+
}).trim();
|
|
285
|
+
return parseInt(output, 10) || 0;
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return -1; // no upstream
|
|
289
|
+
}
|
|
290
|
+
}
|
|
139
291
|
export function getDiffStatsBetween(repoPath, base, head) {
|
|
140
292
|
try {
|
|
141
293
|
return git(repoPath, ['diff', '--shortstat', `${base}...${head}`]);
|
|
@@ -144,3 +296,47 @@ export function getDiffStatsBetween(repoPath, base, head) {
|
|
|
144
296
|
return '';
|
|
145
297
|
}
|
|
146
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
|
},
|