@loicngr/kobo 1.11.39 → 1.11.40
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 +12 -0
- package/CHANGELOG.md +4 -0
- package/dist/mcp-server/kobo-tasks-handlers.js +16 -9
- package/dist/mcp-server/kobo-tasks-server.js +13 -2
- package/dist/mcp-server/workspace-dialogue-client.js +28 -0
- package/dist/server/db/mcp-message-schema.js +16 -0
- package/dist/server/db/migrations.js +4 -0
- package/dist/server/db/schema.js +4 -0
- package/dist/server/db/search-schema.js +53 -0
- package/dist/server/index.js +17 -86
- package/dist/server/middleware/network-auth-middleware.js +5 -1
- package/dist/server/routes/changelog.js +4 -49
- package/dist/server/routes/git.js +2 -2
- package/dist/server/routes/mcp.js +47 -0
- package/dist/server/routes/pull-requests.js +1 -1
- package/dist/server/routes/search.js +13 -4
- package/dist/server/routes/settings.js +5 -0
- package/dist/server/routes/workspaces.js +225 -241
- package/dist/server/services/agent/engines/claude-code/engine.js +31 -33
- package/dist/server/services/agent/engines/codex/engine.js +37 -8
- package/dist/server/services/agent/engines/codex/event-mapper.js +3 -1
- package/dist/server/services/agent/engines/codex/options-builder.js +4 -3
- package/dist/server/services/agent/engines/codex/server-requests.js +8 -1
- package/dist/server/services/agent/orchestrator.js +20 -3
- package/dist/server/services/change-source-branch-service.js +10 -42
- package/dist/server/services/chat-receipt-service.js +33 -0
- package/dist/server/services/cron-service.js +27 -1
- package/dist/server/services/mcp-client-context.js +12 -0
- package/dist/server/services/mcp-connection-info-service.js +63 -0
- package/dist/server/services/mcp-message-delivery-service.js +59 -0
- package/dist/server/services/mcp-message-request-service.js +64 -0
- package/dist/server/services/pr-checkout-service.js +10 -10
- package/dist/server/services/pr-watcher-service.js +14 -9
- package/dist/server/services/search/indexer.js +232 -0
- package/dist/server/services/search/worker.js +76 -0
- package/dist/server/services/search-service.js +104 -110
- package/dist/server/services/update-check-service.js +119 -0
- package/dist/server/services/websocket-service.js +121 -35
- package/dist/server/services/workspace-dialogue-service.js +128 -0
- package/dist/server/services/workspace-message-service.js +58 -0
- package/dist/server/services/workspace-permission-policy-service.js +33 -3
- package/dist/server/services/worktree-restore-service.js +2 -2
- package/dist/server/services/worktree-service.js +197 -124
- package/dist/server/utils/bounded-process.js +137 -0
- package/dist/server/utils/git-ops.js +76 -128
- package/dist/server/utils/git-repo-lock.js +27 -13
- package/dist/shared/workspace-dialogue-tools.js +89 -0
- package/dist/shared/workspace-message-types.js +19 -0
- package/package.json +2 -1
- package/src/client/dist/pwa/assets/{ActivityFeed-Chsw1Mh7.css → ActivityFeed-BYR5x7aa.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-MqsC-95i.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-D8563008.js → ChangelogPage-BxURWYxa.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-Bd0X08sg.js → ComparisonPanel-DjiMDAUi.js} +2 -2
- package/src/client/dist/pwa/assets/CreatePage-B-rhBmqv.js +2 -0
- package/src/client/dist/pwa/assets/DashboardPage-mA5RZf1t.js +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-Ci7ucmy3.js +8 -0
- package/src/client/dist/pwa/assets/{DiffViewer-DnR79ycb.css → DiffViewer-DEPOa3D7.css} +1 -1
- package/src/client/dist/pwa/assets/{DocumentsPanel-BJsJ0fNs.js → DocumentsPanel-RZTvcJkT.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-BVLO5K5d.js → DrawerToggleButton-CTOc3yA0.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-_kL2gMoy.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-RFZS7Lao.js → HealthPage-D3__hKJ5.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-B3JKUm3n.js +2 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-X8oNU5Mb.js → QBtnDropdown-td0RAVPx.js} +1 -1
- package/src/client/dist/pwa/assets/{QDrawer-Cij17skX.js → QDrawer-BkyFPV7a.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-WspK3KC7.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-CwcCGSWq.js → QTooltip-7pw7iuV0.js} +1 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BAA4Aajn.css +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-CLNM13oo.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-BGc1oQSM.css +1 -0
- package/src/client/dist/pwa/assets/SearchPage-CmRgIvu4.js +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-Cj6Y_TlN.js +9 -0
- package/src/client/dist/pwa/assets/SettingsPage-sVlsJQ0b.css +1 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-D7lGFRKs.js → SplitWorkspacePage-CCHGEsKf.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DI8DHb9e.js → TerminalPanel-ijmsYk-2.js} +9 -9
- package/src/client/dist/pwa/assets/{TouchPan-B3nrkCY8.js → TouchPan-DuQFjlQF.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-DU1UShMY.js → TourReplayButton-C5RF0lOK.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-C-yN5IX8.js → WorkspaceAttentionLabels-BR_XElog.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-BGCildyP.js +4 -0
- package/src/client/dist/pwa/assets/{WorkspacePage-BAC6u9OP.css → WorkspacePage-CqcMweNL.css} +1 -1
- package/src/client/dist/pwa/assets/{browser-f9MHQjfi.js → browser-DUqzdkbg.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-BWBzP8os.js → build-path-tree-ImiSpzfI.js} +1 -1
- package/src/client/dist/pwa/assets/de-CaHID9mW.js +17 -0
- package/src/client/dist/pwa/assets/{documents-C2cLnEBQ.js → documents-2C1hn1pw.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-sw146MWN.js → editor.api-Bm1vVaBr.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-C5ZZgCH4.js → efforts-_D8DFYXo.js} +1 -1
- package/src/client/dist/pwa/assets/engineFeatures-CZ9qfBYq.js +1 -0
- package/src/client/dist/pwa/assets/es-ZhzlCmer.js +17 -0
- package/src/client/dist/pwa/assets/{expand-template-B6xAlAaM.js → expand-template-CbDNnTPP.js} +1 -1
- package/src/client/dist/pwa/assets/{format-BwWP6Aay.js → format-pPImdFUy.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-DyWtO-nR.js → formatters-dTX38ZsV.js} +1 -1
- package/src/client/dist/pwa/assets/fr-BfXw7L7J.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DZ5Hm8UH.js → freemarker2-DGluDnAm.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BRUGpTA7.js → handlebars-BgviTnpD.js} +1 -1
- package/src/client/dist/pwa/assets/{html-DNOayNNs.js → html-CV-MrunL.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-CmXBenUp.js +1 -0
- package/src/client/dist/pwa/assets/index-9tiw3NjK.js +20 -0
- package/src/client/dist/pwa/assets/it-CHAurcTF.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-C0tDQU3z.js → javascript-C2VFcHf2.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-CN46O6OF.js → liquid-Dg9ZhbP5.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-DM-lrr8Q.js → mdx-ImgrPYNl.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-CkN7y-8c.js → monaco-contributions-waWZXS1T.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-2BcAOLjc.js → monaco.contribution-BB2rtCHM.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-h6O0qCMA.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-a91Tcr6o.js → notify-theme-Dyg-FGI6.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-DOtELZfB.js → pending-diff-open-Dfpg92li.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-C-9Crj-c.js → permissionModes-DDWmNBW1.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-BRe4oyNx.js → project-color-XUfY_zbw.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BRpJG6sZ.js → python-DVLs-DBA.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-CehCmvHl.js → razor-D34BqMwB.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-DlbQLZ02.js → render-chat-markdown-DwUkPy-O.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-Dt_8mVuj.js → typescript-C7-UpNXx.js} +1 -1
- package/src/client/dist/pwa/assets/use-search-index-status-DtLzygcy.js +1 -0
- package/src/client/dist/pwa/assets/use-tours-DfE-kwrX.js +2 -0
- package/src/client/dist/pwa/assets/{workspace-templates-GCObcjO6.js → workspace-templates-DwwdOEAq.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-_gOwdzB4.js → xml-Cx1oQhEr.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-BxeBqpGf.js → yaml-BQOiJsdx.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +118 -2
- package/src/mcp-server/kobo-tasks-handlers.ts +14 -11
- package/src/mcp-server/kobo-tasks-server.ts +19 -2
- package/src/mcp-server/workspace-dialogue-client.ts +34 -0
- package/src/client/dist/pwa/assets/ActivityFeed-BzzvcOmq.js +0 -8
- package/src/client/dist/pwa/assets/CreatePage-Bon8qIka.js +0 -2
- package/src/client/dist/pwa/assets/DashboardPage-DYxdxEul.js +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-BpsLNxnj.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-Cal_wzC0.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-vvyiRcql.js +0 -2
- package/src/client/dist/pwa/assets/QSelect-xd-AlGo_.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BunlzsOY.css +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-CXRUsz6G.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-CVgRa025.css +0 -1
- package/src/client/dist/pwa/assets/SearchPage-DopbgLxh.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Do9d0Pov.css +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Jx6TnOzR.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-DKRpdpPg.js +0 -4
- package/src/client/dist/pwa/assets/de-QkDLrcGq.js +0 -17
- package/src/client/dist/pwa/assets/engineFeatures-CAVMDawq.js +0 -1
- package/src/client/dist/pwa/assets/es-BgQFKlo8.js +0 -17
- package/src/client/dist/pwa/assets/fr-DAdLWjNM.js +0 -17
- package/src/client/dist/pwa/assets/i18n-CBez_vip.js +0 -1
- package/src/client/dist/pwa/assets/index-VL3fI9a3.js +0 -20
- package/src/client/dist/pwa/assets/it-Bumaewp1.js +0 -17
- package/src/client/dist/pwa/assets/network-auth-KYS4snQH.js +0 -1
- package/src/client/dist/pwa/assets/use-tours-CSVM_54K.js +0 -2
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
/** A zombie cannot mutate a checkout, even before its parent reaps it. */
|
|
5
|
+
async function groupIsLive(pid) {
|
|
6
|
+
try {
|
|
7
|
+
process.kill(-pid, 0);
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
if (err.code === 'ESRCH')
|
|
11
|
+
return false;
|
|
12
|
+
throw err;
|
|
13
|
+
}
|
|
14
|
+
const { stdout } = await execFileAsync('ps', ['-eo', 'pgid=,stat='], { timeout: 1000, encoding: 'utf8' });
|
|
15
|
+
return stdout.split('\n').some((line) => {
|
|
16
|
+
const [group, state] = line.trim().split(/\s+/);
|
|
17
|
+
return Number(group) === pid && !!state && !/^[ZX]/.test(state);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Stop the entire group on timeout, and retain the caller's mutation ownership
|
|
22
|
+
* until exit is confirmed. Inspection failures never authorize another mutation.
|
|
23
|
+
*/
|
|
24
|
+
export function runBoundedProcess(command, args, options) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const child = spawn(command, args, {
|
|
27
|
+
cwd: options.cwd,
|
|
28
|
+
env: options.env,
|
|
29
|
+
detached: true,
|
|
30
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
31
|
+
});
|
|
32
|
+
const stdout = [];
|
|
33
|
+
let stdoutBytes = 0;
|
|
34
|
+
let stderr = Buffer.alloc(0);
|
|
35
|
+
let failure;
|
|
36
|
+
let closed = false;
|
|
37
|
+
let settled = false;
|
|
38
|
+
let code = null;
|
|
39
|
+
let signal = null;
|
|
40
|
+
let killTimer;
|
|
41
|
+
let pollTimer;
|
|
42
|
+
let checking = false;
|
|
43
|
+
let warned = false;
|
|
44
|
+
let escalatedAt;
|
|
45
|
+
const finish = () => {
|
|
46
|
+
if (settled)
|
|
47
|
+
return;
|
|
48
|
+
settled = true;
|
|
49
|
+
clearTimeout(deadline);
|
|
50
|
+
clearTimeout(killTimer);
|
|
51
|
+
clearTimeout(pollTimer);
|
|
52
|
+
if (failure)
|
|
53
|
+
reject(failure);
|
|
54
|
+
else if (code === 0)
|
|
55
|
+
resolve(Buffer.concat(stdout, stdoutBytes).toString('utf8'));
|
|
56
|
+
else
|
|
57
|
+
reject(new Error(`${command} failed: ${stderr.toString('utf8').trim() || `exit code ${code ?? signal ?? 'unknown'}`}`));
|
|
58
|
+
};
|
|
59
|
+
const confirm = async () => {
|
|
60
|
+
if (settled || checking)
|
|
61
|
+
return;
|
|
62
|
+
checking = true;
|
|
63
|
+
try {
|
|
64
|
+
if (closed && (!child.pid || !(await groupIsLive(child.pid)))) {
|
|
65
|
+
finish();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (escalatedAt !== undefined && Date.now() - escalatedAt >= 5000 && !warned) {
|
|
69
|
+
warned = true;
|
|
70
|
+
console.error(`[process] ${command} has not exited after SIGKILL; retaining mutation ownership until exit is confirmed`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (!warned) {
|
|
75
|
+
warned = true;
|
|
76
|
+
console.error(`[process] Cannot confirm ${command} process-group exit; retaining mutation ownership:`, err);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
checking = false;
|
|
81
|
+
}
|
|
82
|
+
if (!settled)
|
|
83
|
+
pollTimer = setTimeout(() => void confirm(), 250);
|
|
84
|
+
};
|
|
85
|
+
const kill = (requestedSignal) => {
|
|
86
|
+
try {
|
|
87
|
+
if (child.pid)
|
|
88
|
+
process.kill(-child.pid, requestedSignal);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (err.code !== 'ESRCH')
|
|
92
|
+
console.error(`[process] ${requestedSignal} failed for ${command}:`, err);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const stop = (error) => {
|
|
96
|
+
if (failure || settled)
|
|
97
|
+
return;
|
|
98
|
+
failure = error;
|
|
99
|
+
kill('SIGTERM');
|
|
100
|
+
killTimer = setTimeout(() => {
|
|
101
|
+
kill('SIGKILL');
|
|
102
|
+
escalatedAt = Date.now();
|
|
103
|
+
void confirm();
|
|
104
|
+
}, options.graceMs ?? 5000);
|
|
105
|
+
void confirm();
|
|
106
|
+
};
|
|
107
|
+
const deadline = setTimeout(() => stop(new Error(`${command} timed out after ${options.timeoutMs}ms`)), options.timeoutMs);
|
|
108
|
+
child.stdout?.on('data', (chunk) => {
|
|
109
|
+
if (options.stdoutLimit === undefined)
|
|
110
|
+
return;
|
|
111
|
+
if (stdoutBytes + chunk.length > options.stdoutLimit) {
|
|
112
|
+
stop(new Error(`${command} output exceeded ${options.stdoutLimit} bytes`));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
stdout.push(chunk);
|
|
116
|
+
stdoutBytes += chunk.length;
|
|
117
|
+
});
|
|
118
|
+
child.stderr?.on('data', (chunk) => {
|
|
119
|
+
const limit = options.stderrLimit ?? 8192;
|
|
120
|
+
stderr = Buffer.concat([stderr, chunk]).subarray(-limit);
|
|
121
|
+
});
|
|
122
|
+
child.once('error', (err) => {
|
|
123
|
+
if (!child.pid) {
|
|
124
|
+
failure = new Error(`${command} failed to spawn: ${err.message}`);
|
|
125
|
+
finish();
|
|
126
|
+
}
|
|
127
|
+
else
|
|
128
|
+
stop(err);
|
|
129
|
+
});
|
|
130
|
+
child.once('close', (exitCode, exitSignal) => {
|
|
131
|
+
closed = true;
|
|
132
|
+
code = exitCode;
|
|
133
|
+
signal = exitSignal;
|
|
134
|
+
void confirm();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile as execFileCb, execFileSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { join, relative, resolve } from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { resolvePathInside } from './safe-path.js';
|
|
6
6
|
const execFileAsync = promisify(execFileCb);
|
|
@@ -922,6 +922,43 @@ export function localBranchExists(repoPath, name) {
|
|
|
922
922
|
export function moveWorktree(projectPath, oldPath, newPath) {
|
|
923
923
|
git(projectPath, ['worktree', 'move', oldPath, newPath]);
|
|
924
924
|
}
|
|
925
|
+
/** Git -z never quotes filenames; renames have a second source record. */
|
|
926
|
+
function parsePorcelain(output) {
|
|
927
|
+
const records = output.split('\0');
|
|
928
|
+
const entries = [];
|
|
929
|
+
for (let i = 0; i < records.length; i++) {
|
|
930
|
+
const record = records[i];
|
|
931
|
+
if (!record)
|
|
932
|
+
continue;
|
|
933
|
+
const x = record[0];
|
|
934
|
+
const y = record[1];
|
|
935
|
+
entries.push({ path: record.slice(3), x, y });
|
|
936
|
+
if (x === 'R' || x === 'C' || y === 'R' || y === 'C')
|
|
937
|
+
i++;
|
|
938
|
+
}
|
|
939
|
+
return entries;
|
|
940
|
+
}
|
|
941
|
+
function parseNameStatus(output) {
|
|
942
|
+
const records = output.split('\0');
|
|
943
|
+
const files = [];
|
|
944
|
+
for (let i = 0; i < records.length - 1;) {
|
|
945
|
+
const code = records[i++];
|
|
946
|
+
let filePath = records[i++];
|
|
947
|
+
if (code.startsWith('R') || code.startsWith('C'))
|
|
948
|
+
filePath = records[i++];
|
|
949
|
+
if (!filePath)
|
|
950
|
+
continue;
|
|
951
|
+
const status = code.startsWith('R')
|
|
952
|
+
? 'renamed'
|
|
953
|
+
: code.startsWith('A')
|
|
954
|
+
? 'added'
|
|
955
|
+
: code.startsWith('D')
|
|
956
|
+
? 'deleted'
|
|
957
|
+
: 'modified';
|
|
958
|
+
files.push({ path: filePath, status });
|
|
959
|
+
}
|
|
960
|
+
return files;
|
|
961
|
+
}
|
|
925
962
|
/** List files changed between base and HEAD (committed), plus working tree changes. */
|
|
926
963
|
/**
|
|
927
964
|
* List the worktree's files — tracked plus untracked-but-not-git-ignored.
|
|
@@ -931,10 +968,10 @@ export function moveWorktree(projectPath, oldPath, newPath) {
|
|
|
931
968
|
*/
|
|
932
969
|
export function listWorktreeFiles(worktreePath, limit = 5000) {
|
|
933
970
|
try {
|
|
934
|
-
const out = git(worktreePath, ['ls-files', '--cached', '--others', '--exclude-standard']);
|
|
971
|
+
const out = git(worktreePath, ['ls-files', '-z', '--cached', '--others', '--exclude-standard']);
|
|
935
972
|
if (!out)
|
|
936
973
|
return [];
|
|
937
|
-
const files = out.split('\
|
|
974
|
+
const files = out.split('\0').filter((line) => line.length > 0);
|
|
938
975
|
return files.length > limit ? files.slice(0, limit) : files;
|
|
939
976
|
}
|
|
940
977
|
catch {
|
|
@@ -947,7 +984,7 @@ export function listWorktreeFiles(worktreePath, limit = 5000) {
|
|
|
947
984
|
*/
|
|
948
985
|
export function worktreeHasChanges(worktreePath) {
|
|
949
986
|
try {
|
|
950
|
-
return git(worktreePath, ['status', '--porcelain']).length > 0;
|
|
987
|
+
return git(worktreePath, ['status', '--porcelain', '-z']).length > 0;
|
|
951
988
|
}
|
|
952
989
|
catch {
|
|
953
990
|
return false;
|
|
@@ -958,7 +995,7 @@ export function worktreeHasChanges(worktreePath) {
|
|
|
958
995
|
* the worktree holds uncommitted work, "unknown" must never be read as
|
|
959
996
|
* "nothing to lose". */
|
|
960
997
|
export function worktreeHasChangesStrict(worktreePath) {
|
|
961
|
-
return git(worktreePath, ['status', '--porcelain']).length > 0;
|
|
998
|
+
return git(worktreePath, ['status', '--porcelain', '-z']).length > 0;
|
|
962
999
|
}
|
|
963
1000
|
export function getChangedFiles(repoPath, base, includeUntracked = false) {
|
|
964
1001
|
const ref = resolveBase(repoPath, base);
|
|
@@ -966,29 +1003,9 @@ export function getChangedFiles(repoPath, base, includeUntracked = false) {
|
|
|
966
1003
|
const seen = new Set();
|
|
967
1004
|
// Committed changes (base..HEAD)
|
|
968
1005
|
try {
|
|
969
|
-
|
|
970
|
-
for (const
|
|
971
|
-
|
|
972
|
-
continue;
|
|
973
|
-
const [statusCode, ...pathParts] = line.split('\t');
|
|
974
|
-
if (!statusCode || pathParts.length === 0)
|
|
975
|
-
continue;
|
|
976
|
-
// For renames/copies (R100 old new), use the new path (last element)
|
|
977
|
-
const filePath = (statusCode.startsWith('R') || statusCode.startsWith('C')
|
|
978
|
-
? pathParts[pathParts.length - 1]
|
|
979
|
-
: pathParts[0])?.replace(/\/$/, '') ?? '';
|
|
980
|
-
if (!filePath)
|
|
981
|
-
continue;
|
|
982
|
-
let status = 'modified';
|
|
983
|
-
if (statusCode.startsWith('A'))
|
|
984
|
-
status = 'added';
|
|
985
|
-
else if (statusCode.startsWith('D'))
|
|
986
|
-
status = 'deleted';
|
|
987
|
-
else if (statusCode.startsWith('R'))
|
|
988
|
-
status = 'renamed';
|
|
989
|
-
files.push({ path: filePath, status });
|
|
990
|
-
seen.add(filePath);
|
|
991
|
-
}
|
|
1006
|
+
files.push(...parseNameStatus(git(repoPath, ['diff', '--name-status', '-z', `${ref}...HEAD`])));
|
|
1007
|
+
for (const file of files)
|
|
1008
|
+
seen.add(file.path);
|
|
992
1009
|
}
|
|
993
1010
|
catch {
|
|
994
1011
|
// No commits yet
|
|
@@ -1000,23 +1017,21 @@ export function getChangedFiles(repoPath, base, includeUntracked = false) {
|
|
|
1000
1017
|
// we use `-uall` and surface them with status='added'.
|
|
1001
1018
|
try {
|
|
1002
1019
|
const flag = includeUntracked ? '-uall' : '-uno';
|
|
1003
|
-
const output = git(repoPath, ['status', '--porcelain', flag]);
|
|
1004
|
-
for (const
|
|
1005
|
-
if (
|
|
1020
|
+
const output = git(repoPath, ['status', '--porcelain', '-z', flag]);
|
|
1021
|
+
for (const { path: filePath, x, y } of parsePorcelain(output)) {
|
|
1022
|
+
if (seen.has(filePath))
|
|
1006
1023
|
continue;
|
|
1007
|
-
const filePath = line.substring(3).replace(/\/$/, '');
|
|
1008
|
-
if (!filePath || seen.has(filePath))
|
|
1009
|
-
continue;
|
|
1010
|
-
const x = line[0];
|
|
1011
|
-
const y = line[1];
|
|
1012
1024
|
let status = 'modified';
|
|
1013
1025
|
if (x === '?' && y === '?')
|
|
1014
1026
|
status = 'untracked';
|
|
1027
|
+
else if (x === 'R' || y === 'R')
|
|
1028
|
+
status = 'renamed';
|
|
1015
1029
|
else if (x === 'A' || y === 'A')
|
|
1016
1030
|
status = 'added';
|
|
1017
1031
|
else if (x === 'D' || y === 'D')
|
|
1018
1032
|
status = 'deleted';
|
|
1019
1033
|
files.push({ path: filePath, status });
|
|
1034
|
+
seen.add(filePath);
|
|
1020
1035
|
}
|
|
1021
1036
|
}
|
|
1022
1037
|
catch {
|
|
@@ -1041,27 +1056,7 @@ export function getUnpushedChangedFiles(repoPath, branchName, remote = 'origin')
|
|
|
1041
1056
|
}
|
|
1042
1057
|
const files = [];
|
|
1043
1058
|
try {
|
|
1044
|
-
|
|
1045
|
-
for (const line of output.split('\n')) {
|
|
1046
|
-
if (!line)
|
|
1047
|
-
continue;
|
|
1048
|
-
const [statusCode, ...pathParts] = line.split('\t');
|
|
1049
|
-
if (!statusCode || pathParts.length === 0)
|
|
1050
|
-
continue;
|
|
1051
|
-
const filePath = (statusCode.startsWith('R') || statusCode.startsWith('C')
|
|
1052
|
-
? pathParts[pathParts.length - 1]
|
|
1053
|
-
: pathParts[0])?.replace(/\/$/, '') ?? '';
|
|
1054
|
-
if (!filePath)
|
|
1055
|
-
continue;
|
|
1056
|
-
let status = 'modified';
|
|
1057
|
-
if (statusCode.startsWith('A'))
|
|
1058
|
-
status = 'added';
|
|
1059
|
-
else if (statusCode.startsWith('D'))
|
|
1060
|
-
status = 'deleted';
|
|
1061
|
-
else if (statusCode.startsWith('R'))
|
|
1062
|
-
status = 'renamed';
|
|
1063
|
-
files.push({ path: filePath, status });
|
|
1064
|
-
}
|
|
1059
|
+
files.push(...parseNameStatus(git(repoPath, ['diff', '--name-status', '-z', `${remoteRef}..HEAD`])));
|
|
1065
1060
|
}
|
|
1066
1061
|
catch {
|
|
1067
1062
|
// Unlikely after the rev-parse check, but keep the happy path robust.
|
|
@@ -1113,27 +1108,7 @@ export function commitExists(repoPath, ref) {
|
|
|
1113
1108
|
export function getChangedFilesBetween(repoPath, fromRef, toRef) {
|
|
1114
1109
|
const files = [];
|
|
1115
1110
|
try {
|
|
1116
|
-
|
|
1117
|
-
for (const line of output.split('\n')) {
|
|
1118
|
-
if (!line)
|
|
1119
|
-
continue;
|
|
1120
|
-
const [statusCode, ...pathParts] = line.split('\t');
|
|
1121
|
-
if (!statusCode || pathParts.length === 0)
|
|
1122
|
-
continue;
|
|
1123
|
-
const filePath = (statusCode.startsWith('R') || statusCode.startsWith('C')
|
|
1124
|
-
? pathParts[pathParts.length - 1]
|
|
1125
|
-
: pathParts[0])?.replace(/\/$/, '') ?? '';
|
|
1126
|
-
if (!filePath)
|
|
1127
|
-
continue;
|
|
1128
|
-
let status = 'modified';
|
|
1129
|
-
if (statusCode.startsWith('A'))
|
|
1130
|
-
status = 'added';
|
|
1131
|
-
else if (statusCode.startsWith('D'))
|
|
1132
|
-
status = 'deleted';
|
|
1133
|
-
else if (statusCode.startsWith('R'))
|
|
1134
|
-
status = 'renamed';
|
|
1135
|
-
files.push({ path: filePath, status });
|
|
1136
|
-
}
|
|
1111
|
+
files.push(...parseNameStatus(git(repoPath, ['diff', '--name-status', '-z', `${fromRef}..${toRef}`])));
|
|
1137
1112
|
}
|
|
1138
1113
|
catch {
|
|
1139
1114
|
// invalid refs / no diff → empty list
|
|
@@ -1158,42 +1133,27 @@ export function getChangedFilesBetween(repoPath, fromRef, toRef) {
|
|
|
1158
1133
|
* feedback in the UI.
|
|
1159
1134
|
*/
|
|
1160
1135
|
export function rollbackFile(repoPath, branchName, filePath, remote = 'origin') {
|
|
1161
|
-
|
|
1136
|
+
resolvePathInside(repoPath, filePath);
|
|
1137
|
+
const absPath = resolve(repoPath, filePath);
|
|
1138
|
+
filePath = relative(resolve(repoPath), absPath);
|
|
1162
1139
|
const remoteRef = `${remote}/${branchName}`;
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
return 'remote';
|
|
1176
|
-
}
|
|
1177
|
-
catch {
|
|
1178
|
-
// File doesn't exist at origin/<branch> (added locally) — fall through.
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
try {
|
|
1182
|
-
git(repoPath, ['cat-file', '-e', `HEAD:${filePath}`]);
|
|
1183
|
-
git(repoPath, ['checkout', 'HEAD', '--', filePath]);
|
|
1140
|
+
// show-ref distinguishes an absent ref from repository/IO errors. Tree
|
|
1141
|
+
// inspection must succeed before absence authorizes deletion.
|
|
1142
|
+
const refs = git(repoPath, ['for-each-ref', '--format=%(refname)', `refs/remotes/${remoteRef}`]).split('\n');
|
|
1143
|
+
const fileExistsAt = (ref) => git(repoPath, ['ls-tree', '-r', '-z', '--name-only', ref, '--', `:(literal)${filePath}`])
|
|
1144
|
+
.split('\0')
|
|
1145
|
+
.includes(filePath);
|
|
1146
|
+
if (refs.includes(`refs/remotes/${remoteRef}`) && fileExistsAt(remoteRef)) {
|
|
1147
|
+
git(repoPath, ['checkout', remoteRef, '--', `:(literal)${filePath}`]);
|
|
1148
|
+
return 'remote';
|
|
1149
|
+
}
|
|
1150
|
+
if (fileExistsAt('HEAD')) {
|
|
1151
|
+
git(repoPath, ['checkout', 'HEAD', '--', `:(literal)${filePath}`]);
|
|
1184
1152
|
return 'head';
|
|
1185
1153
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
// with a previous rollback, stale UI list, manual rm), we still return
|
|
1190
|
-
// 'deleted' since the end state matches the user's intent. `rmSync`
|
|
1191
|
-
// over `git clean -f` keeps the action narrow to one file.
|
|
1192
|
-
if (existsSync(absPath)) {
|
|
1193
|
-
rmSync(absPath, { force: true });
|
|
1194
|
-
}
|
|
1195
|
-
return 'deleted';
|
|
1196
|
-
}
|
|
1154
|
+
if (existsSync(absPath))
|
|
1155
|
+
rmSync(absPath, { force: true });
|
|
1156
|
+
return 'deleted';
|
|
1197
1157
|
}
|
|
1198
1158
|
/** @deprecated kept for backwards-compat with older imports — use `rollbackFile`. */
|
|
1199
1159
|
export const rollbackFileToRemote = rollbackFile;
|
|
@@ -1213,7 +1173,7 @@ export function writeFileInWorktree(absPath, content) {
|
|
|
1213
1173
|
/** Parse `git status --porcelain` into counts of staged, modified, and untracked files. */
|
|
1214
1174
|
export function getWorkingTreeStatus(repoPath) {
|
|
1215
1175
|
try {
|
|
1216
|
-
return parseWorkingTreeStatus(git(repoPath, ['status', '--porcelain']));
|
|
1176
|
+
return parseWorkingTreeStatus(git(repoPath, ['status', '--porcelain', '-z']));
|
|
1217
1177
|
}
|
|
1218
1178
|
catch {
|
|
1219
1179
|
return { staged: 0, modified: 0, untracked: 0 };
|
|
@@ -1223,11 +1183,7 @@ function parseWorkingTreeStatus(output) {
|
|
|
1223
1183
|
let staged = 0;
|
|
1224
1184
|
let modified = 0;
|
|
1225
1185
|
let untracked = 0;
|
|
1226
|
-
for (const
|
|
1227
|
-
if (!line)
|
|
1228
|
-
continue;
|
|
1229
|
-
const x = line[0];
|
|
1230
|
-
const y = line[1];
|
|
1186
|
+
for (const { x, y } of parsePorcelain(output)) {
|
|
1231
1187
|
if (x === '?' && y === '?') {
|
|
1232
1188
|
untracked++;
|
|
1233
1189
|
}
|
|
@@ -1247,17 +1203,9 @@ function parseWorkingTreeStatus(output) {
|
|
|
1247
1203
|
*/
|
|
1248
1204
|
export function getWorkingTreeFiles(repoPath) {
|
|
1249
1205
|
try {
|
|
1250
|
-
const output = git(repoPath, ['status', '--porcelain']);
|
|
1206
|
+
const output = git(repoPath, ['status', '--porcelain', '-z']);
|
|
1251
1207
|
const files = [];
|
|
1252
|
-
for (const
|
|
1253
|
-
if (!line)
|
|
1254
|
-
continue;
|
|
1255
|
-
const x = line[0];
|
|
1256
|
-
const y = line[1];
|
|
1257
|
-
let filePath = line.slice(3);
|
|
1258
|
-
const arrowIdx = filePath.indexOf(' -> ');
|
|
1259
|
-
if (arrowIdx !== -1)
|
|
1260
|
-
filePath = filePath.slice(arrowIdx + 4);
|
|
1208
|
+
for (const { path: filePath, x, y } of parsePorcelain(output)) {
|
|
1261
1209
|
const untracked = x === '?' && y === '?';
|
|
1262
1210
|
files.push({
|
|
1263
1211
|
path: filePath,
|
|
@@ -1436,7 +1384,7 @@ export async function getStructuredDiffStatsBetweenAsync(repoPath, base, head) {
|
|
|
1436
1384
|
/** Non-blocking working-tree summary for request and polling hot paths. */
|
|
1437
1385
|
export async function getWorkingTreeStatusAsync(repoPath) {
|
|
1438
1386
|
try {
|
|
1439
|
-
return parseWorkingTreeStatus(await gitAsync(repoPath, ['status', '--porcelain']));
|
|
1387
|
+
return parseWorkingTreeStatus(await gitAsync(repoPath, ['status', '--porcelain', '-z']));
|
|
1440
1388
|
}
|
|
1441
1389
|
catch {
|
|
1442
1390
|
return { staged: 0, modified: 0, untracked: 0 };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
3
4
|
/**
|
|
4
5
|
* One promise chain per shared git directory. Same mechanism as the per-worktree
|
|
5
6
|
* lock in `image-service.ts`, keyed differently: remote refs, packed-refs and
|
|
@@ -8,6 +9,10 @@ import path from 'node:path';
|
|
|
8
9
|
* are disjoint.
|
|
9
10
|
*/
|
|
10
11
|
const locks = new Map();
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
// Register operations in invocation order even when common-dir lookups finish
|
|
14
|
+
// out of order. This queue covers resolution only, never the locked operation.
|
|
15
|
+
let registrations = Promise.resolve();
|
|
11
16
|
/**
|
|
12
17
|
* Absolute path of the git directory shared by every worktree of a repository.
|
|
13
18
|
*
|
|
@@ -15,15 +20,16 @@ const locks = new Map();
|
|
|
15
20
|
* repository, git missing): a wrong key only costs parallelism, never
|
|
16
21
|
* correctness, whereas throwing here would break every caller.
|
|
17
22
|
*/
|
|
18
|
-
export function resolveGitCommonDir(repoPath) {
|
|
23
|
+
export async function resolveGitCommonDir(repoPath) {
|
|
19
24
|
try {
|
|
20
|
-
const
|
|
25
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', '--git-common-dir'], {
|
|
21
26
|
cwd: repoPath,
|
|
22
27
|
encoding: 'utf-8',
|
|
23
|
-
timeout:
|
|
28
|
+
timeout: 10_000,
|
|
29
|
+
killSignal: 'SIGKILL',
|
|
24
30
|
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
25
|
-
})
|
|
26
|
-
return path.resolve(repoPath,
|
|
31
|
+
});
|
|
32
|
+
return path.resolve(repoPath, stdout.replace(/\n$/, ''));
|
|
27
33
|
}
|
|
28
34
|
catch {
|
|
29
35
|
return path.resolve(repoPath);
|
|
@@ -31,15 +37,23 @@ export function resolveGitCommonDir(repoPath) {
|
|
|
31
37
|
}
|
|
32
38
|
/** Run `fn` with exclusive access to `repoPath`'s shared git directory. */
|
|
33
39
|
export function withGitRepoLock(repoPath, fn) {
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
let next;
|
|
41
|
+
const registered = registrations.then(async () => {
|
|
42
|
+
const key = await resolveGitCommonDir(repoPath);
|
|
43
|
+
const previous = locks.get(key) ?? Promise.resolve();
|
|
44
|
+
next = previous.then(fn, fn);
|
|
45
|
+
const settled = next.then(() => { }, () => { });
|
|
46
|
+
locks.set(key, settled);
|
|
47
|
+
void settled.then(() => {
|
|
48
|
+
if (locks.get(key) === settled)
|
|
49
|
+
locks.delete(key);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
registrations = registered.catch(() => { });
|
|
53
|
+
return registered.then(() => next);
|
|
41
54
|
}
|
|
42
55
|
/** Test-only: drop every queued chain so tests don't inherit each other's state. */
|
|
43
56
|
export function _resetGitRepoLocksForTest() {
|
|
44
57
|
locks.clear();
|
|
58
|
+
registrations = Promise.resolve();
|
|
45
59
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const workspace = { type: 'string', minLength: 1, maxLength: 200, description: 'Workspace ID from list_workspaces.' };
|
|
2
|
+
const session = {
|
|
3
|
+
type: 'string',
|
|
4
|
+
minLength: 1,
|
|
5
|
+
maxLength: 200,
|
|
6
|
+
description: 'Session ID from list_workspace_sessions.',
|
|
7
|
+
};
|
|
8
|
+
function tool(name, description, properties, required, readOnly = true) {
|
|
9
|
+
return {
|
|
10
|
+
name,
|
|
11
|
+
description,
|
|
12
|
+
inputSchema: { type: 'object', properties, required, additionalProperties: false },
|
|
13
|
+
annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, openWorldHint: !readOnly },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export const WORKSPACE_DISCOVERY_TOOL = tool('list_workspaces', 'Discover Kōbō workspaces and their current status.', {
|
|
17
|
+
include_archived: { type: 'boolean' },
|
|
18
|
+
}, []);
|
|
19
|
+
export const WORKSPACE_DIALOGUE_TOOLS = [
|
|
20
|
+
tool('get_workspace', 'Read workspace metadata, tasks and current agent session.', { workspace_id: workspace }, [
|
|
21
|
+
'workspace_id',
|
|
22
|
+
]),
|
|
23
|
+
tool('list_workspace_sessions', 'List the sessions of a workspace to select a conversation.', { workspace_id: workspace }, ['workspace_id']),
|
|
24
|
+
tool('read_workspace_messages', 'Read a bounded chronological page of conversation fragments. Start without after_cursor; continue with nextCursor. Reuse the final cursor to poll for new replies. Concatenate assistant fragments with the same messageId and sessionId.', {
|
|
25
|
+
workspace_id: workspace,
|
|
26
|
+
session_id: session,
|
|
27
|
+
after_cursor: { type: 'string', minLength: 1, maxLength: 200 },
|
|
28
|
+
limit: { type: 'integer', minimum: 1, maximum: 200, description: 'Maximum source events scanned (default 100).' },
|
|
29
|
+
}, ['workspace_id']),
|
|
30
|
+
tool('send_workspace_message', 'Send a message to an agent, resuming it if needed. Returns acceptance, not the agent reply; read_workspace_messages retrieves replies. Uses the configured permission mode and pauses active auto-loop. Do not retry blindly after an ambiguous connection failure. Do not send to your own workspace.', {
|
|
31
|
+
workspace_id: workspace,
|
|
32
|
+
session_id: session,
|
|
33
|
+
content: { type: 'string', minLength: 1, maxLength: 100_000 },
|
|
34
|
+
idempotency_key: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
minLength: 1,
|
|
37
|
+
maxLength: 200,
|
|
38
|
+
description: 'Generate once per intended message; reuse on retries to prevent duplicate dispatch, including after restart. Reuse with different content/session is rejected.',
|
|
39
|
+
},
|
|
40
|
+
}, ['workspace_id', 'content'], false),
|
|
41
|
+
tool('get_workspace_questions', 'Read pending questions and whether a human tool approval is blocking the workspace.', { workspace_id: workspace }, ['workspace_id']),
|
|
42
|
+
tool('answer_workspace_question', 'Answer the current pending question using its exact tool_call_id. Answer keys are question IDs for Codex, question text for Claude. Tool permission requests require human approval in Kōbō.', {
|
|
43
|
+
workspace_id: workspace,
|
|
44
|
+
tool_call_id: { type: 'string', minLength: 1, maxLength: 200 },
|
|
45
|
+
answers: { type: 'object', minProperties: 1, additionalProperties: { type: 'string' } },
|
|
46
|
+
}, ['workspace_id', 'tool_call_id', 'answers'], false),
|
|
47
|
+
];
|
|
48
|
+
export const EXTERNAL_MCP_TOOLS = [WORKSPACE_DISCOVERY_TOOL, ...WORKSPACE_DIALOGUE_TOOLS];
|
|
49
|
+
/** The low-level MCP server checks the envelope; validate tool arguments here. */
|
|
50
|
+
export function validateDialogueArguments(name, value) {
|
|
51
|
+
const definition = EXTERNAL_MCP_TOOLS.find((entry) => entry.name === name);
|
|
52
|
+
if (!definition)
|
|
53
|
+
throw new Error(`Unknown tool '${name}'`);
|
|
54
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
55
|
+
throw new Error('Tool arguments must be an object');
|
|
56
|
+
const args = value;
|
|
57
|
+
const properties = definition.inputSchema.properties;
|
|
58
|
+
for (const key of definition.inputSchema.required ?? [])
|
|
59
|
+
if (!Object.hasOwn(args, key))
|
|
60
|
+
throw new Error(`${key} is required`);
|
|
61
|
+
for (const [key, input] of Object.entries(args)) {
|
|
62
|
+
const property = properties[key];
|
|
63
|
+
if (!Object.hasOwn(properties, key))
|
|
64
|
+
throw new Error(`Unknown argument '${key}'`);
|
|
65
|
+
if (property.type === 'string') {
|
|
66
|
+
if (typeof input !== 'string' || !input.trim() || input.length > (property.maxLength ?? 100_000))
|
|
67
|
+
throw new Error(`${key} must be a non-empty string within its length limit`);
|
|
68
|
+
}
|
|
69
|
+
else if (property.type === 'boolean') {
|
|
70
|
+
if (typeof input !== 'boolean')
|
|
71
|
+
throw new Error(`${key} must be a boolean`);
|
|
72
|
+
}
|
|
73
|
+
else if (property.type === 'integer') {
|
|
74
|
+
if (typeof input !== 'number' ||
|
|
75
|
+
!Number.isInteger(input) ||
|
|
76
|
+
input < property.minimum ||
|
|
77
|
+
input > property.maximum)
|
|
78
|
+
throw new Error(`${key} is outside its allowed integer range`);
|
|
79
|
+
}
|
|
80
|
+
else if (!input ||
|
|
81
|
+
typeof input !== 'object' ||
|
|
82
|
+
Array.isArray(input) ||
|
|
83
|
+
Object.keys(input).length === 0 ||
|
|
84
|
+
Object.values(input).some((answer) => typeof answer !== 'string' || answer.length > 100_000)) {
|
|
85
|
+
throw new Error(`${key} must be a non-empty object of string answers`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return args;
|
|
89
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function normalizeClientName(value) {
|
|
2
|
+
return ((typeof value === 'string'
|
|
3
|
+
? Array.from(value
|
|
4
|
+
.replace(/[\p{Cc}\s]+/gu, ' ')
|
|
5
|
+
.replace(/\p{Cs}/gu, '�')
|
|
6
|
+
.trim())
|
|
7
|
+
.slice(0, 100)
|
|
8
|
+
.join('')
|
|
9
|
+
: '') || 'External MCP client');
|
|
10
|
+
}
|
|
11
|
+
/** Tolerant decoder for historical/untrusted payloads. Labels are not identities. */
|
|
12
|
+
export function parseMessageSource(value) {
|
|
13
|
+
if (!value || typeof value !== 'object')
|
|
14
|
+
return undefined;
|
|
15
|
+
const source = value;
|
|
16
|
+
if (source.kind !== 'mcp' || (source.transport !== 'http' && source.transport !== 'stdio'))
|
|
17
|
+
return undefined;
|
|
18
|
+
return { kind: 'mcp', clientName: normalizeClientName(source.clientName), transport: source.transport };
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loicngr/kobo",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.40",
|
|
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",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"install-all": "npm install && cd src/client && npm install && cd src-pwa && npm install",
|
|
58
58
|
"start": "node dist/server/index.js",
|
|
59
59
|
"test": "vitest run --config vitest.config.ts --root .",
|
|
60
|
+
"test:mcp:live": "vitest run --config vitest.live.config.ts",
|
|
60
61
|
"test:watch": "vitest --config vitest.config.ts --root .",
|
|
61
62
|
"test:client": "cd src/client && npm test",
|
|
62
63
|
"test:all": "npm test && npm run test:client",
|