@loicngr/kobo 0.1.1
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 +227 -0
- package/LICENSE +674 -0
- package/README.md +199 -0
- package/dist/mcp-server/kobo-tasks-handlers.js +27 -0
- package/dist/mcp-server/kobo-tasks-server.js +116 -0
- package/dist/server/db/index.js +22 -0
- package/dist/server/db/migrations.js +20 -0
- package/dist/server/db/schema.js +49 -0
- package/dist/server/index.js +178 -0
- package/dist/server/routes/dev-server.js +74 -0
- package/dist/server/routes/git.js +20 -0
- package/dist/server/routes/notion.js +24 -0
- package/dist/server/routes/settings.js +92 -0
- package/dist/server/routes/workspaces.js +730 -0
- package/dist/server/services/agent-manager.js +435 -0
- package/dist/server/services/dev-server-service.js +298 -0
- package/dist/server/services/notion-service.js +369 -0
- package/dist/server/services/pr-template-service.js +38 -0
- package/dist/server/services/settings-service.js +205 -0
- package/dist/server/services/websocket-service.js +212 -0
- package/dist/server/services/workspace-service.js +208 -0
- package/dist/server/services/worktree-service.js +117 -0
- package/dist/server/utils/git-ops.js +117 -0
- package/dist/server/utils/paths.js +95 -0
- package/dist/server/utils/process-tracker.js +46 -0
- package/package.json +84 -0
- package/src/client/dist/spa/assets/ActivityFeed-BveJRagX.js +60 -0
- package/src/client/dist/spa/assets/ActivityFeed-DBNn62g_.css +1 -0
- package/src/client/dist/spa/assets/CreatePage-BlgXsrJO.css +1 -0
- package/src/client/dist/spa/assets/CreatePage-wbOkBwYU.js +2 -0
- 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-6hzaLlYO.js +1 -0
- package/src/client/dist/spa/assets/MainLayout-D0OU6djX.css +1 -0
- package/src/client/dist/spa/assets/QBadge-Cb92Ia8-.js +1 -0
- package/src/client/dist/spa/assets/QDialog-B5H6ayTp.js +1 -0
- package/src/client/dist/spa/assets/QExpansionItem-DJgnAZg_.js +1 -0
- package/src/client/dist/spa/assets/QPage-CLk9i9z8.js +1 -0
- package/src/client/dist/spa/assets/QSpinnerDots-DcaNq8uL.js +1 -0
- package/src/client/dist/spa/assets/QTabPanels-DlG5TZhP.js +1 -0
- package/src/client/dist/spa/assets/QTooltip-637ruGFc.js +1 -0
- package/src/client/dist/spa/assets/SettingsPage-B9VYIQs-.css +1 -0
- package/src/client/dist/spa/assets/SettingsPage-KEqbLZUA.js +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-BFuHLjou.css +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-D0Hm21LY.js +2 -0
- package/src/client/dist/spa/assets/_plugin-vue_export-helper-CHpmshS7.js +1 -0
- package/src/client/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
- package/src/client/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2 +0 -0
- package/src/client/dist/spa/assets/index-BThMCiY7.css +1 -0
- package/src/client/dist/spa/assets/index-CMvo3OTb.js +5 -0
- package/src/client/dist/spa/assets/nodes-DeIen-kp.js +1 -0
- package/src/client/dist/spa/assets/use-quasar-Dq-Vjx_2.js +1 -0
- package/src/client/dist/spa/index.html +4 -0
- package/src/mcp-server/kobo-tasks-handlers.ts +54 -0
- package/src/mcp-server/kobo-tasks-server.ts +128 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { isGitBranchExistsError } from '../utils/git-ops.js';
|
|
5
|
+
function git(repoPath, args) {
|
|
6
|
+
return execFileSync('git', args, { cwd: repoPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
7
|
+
}
|
|
8
|
+
function getExcludeFilePath(projectPath) {
|
|
9
|
+
return path.join(projectPath, '.git', 'info', 'exclude');
|
|
10
|
+
}
|
|
11
|
+
function addToExclude(projectPath, worktreePath) {
|
|
12
|
+
const excludeFile = getExcludeFilePath(projectPath);
|
|
13
|
+
// Ensure the .git/info directory exists
|
|
14
|
+
const infoDir = path.dirname(excludeFile);
|
|
15
|
+
if (!fs.existsSync(infoDir)) {
|
|
16
|
+
fs.mkdirSync(infoDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
// Make the path relative to projectPath for cleaner exclude entries
|
|
19
|
+
const relativePath = path.relative(projectPath, worktreePath);
|
|
20
|
+
const entry = `/${relativePath}`;
|
|
21
|
+
let current = '';
|
|
22
|
+
if (fs.existsSync(excludeFile)) {
|
|
23
|
+
current = fs.readFileSync(excludeFile, 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
if (!current.split('\n').includes(entry)) {
|
|
26
|
+
const newContent = current.endsWith('\n') || current === '' ? `${current}${entry}\n` : `${current}\n${entry}\n`;
|
|
27
|
+
fs.writeFileSync(excludeFile, newContent, 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function removeFromExclude(projectPath, worktreePath) {
|
|
31
|
+
const excludeFile = getExcludeFilePath(projectPath);
|
|
32
|
+
if (!fs.existsSync(excludeFile))
|
|
33
|
+
return;
|
|
34
|
+
const relativePath = path.relative(projectPath, worktreePath);
|
|
35
|
+
const entry = `/${relativePath}`;
|
|
36
|
+
const lines = fs.readFileSync(excludeFile, 'utf-8').split('\n');
|
|
37
|
+
const filtered = lines.filter((line) => line !== entry);
|
|
38
|
+
// I3: ensure the file ends with exactly one newline and has no trailing empty lines
|
|
39
|
+
const trimmed = filtered.join('\n').replace(/\n+$/, '');
|
|
40
|
+
fs.writeFileSync(excludeFile, trimmed ? `${trimmed}\n` : '', 'utf-8');
|
|
41
|
+
}
|
|
42
|
+
export function createWorktree(projectPath, branchName, sourceBranch) {
|
|
43
|
+
const worktreesDir = path.join(projectPath, '.worktrees');
|
|
44
|
+
if (!fs.existsSync(worktreesDir)) {
|
|
45
|
+
fs.mkdirSync(worktreesDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
const worktreePath = path.join(worktreesDir, branchName);
|
|
48
|
+
try {
|
|
49
|
+
// Try creating a new branch + worktree
|
|
50
|
+
git(projectPath, ['worktree', 'add', '-b', branchName, worktreePath, sourceBranch]);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
54
|
+
// M3: use shared utility for branch-exists detection
|
|
55
|
+
// If branch already exists, add worktree without creating the branch
|
|
56
|
+
if (isGitBranchExistsError(message)) {
|
|
57
|
+
git(projectPath, ['worktree', 'add', worktreePath, branchName]);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
throw new Error(`Failed to create worktree for branch '${branchName}': ${message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
addToExclude(projectPath, worktreePath);
|
|
64
|
+
return worktreePath;
|
|
65
|
+
}
|
|
66
|
+
export function removeWorktree(projectPath, worktreePath) {
|
|
67
|
+
try {
|
|
68
|
+
git(projectPath, ['worktree', 'remove', worktreePath, '--force']);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
+
throw new Error(`Failed to remove worktree '${worktreePath}': ${message}`);
|
|
73
|
+
}
|
|
74
|
+
removeFromExclude(projectPath, worktreePath);
|
|
75
|
+
}
|
|
76
|
+
export function listWorktrees(projectPath) {
|
|
77
|
+
const output = git(projectPath, ['worktree', 'list', '--porcelain']);
|
|
78
|
+
const worktrees = [];
|
|
79
|
+
const blocks = output.split('\n\n').filter(Boolean);
|
|
80
|
+
for (const block of blocks) {
|
|
81
|
+
const lines = block.split('\n');
|
|
82
|
+
const worktree = {};
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
if (line.startsWith('worktree ')) {
|
|
85
|
+
worktree.path = line.slice('worktree '.length).trim();
|
|
86
|
+
}
|
|
87
|
+
else if (line.startsWith('HEAD ')) {
|
|
88
|
+
worktree.head = line.slice('HEAD '.length).trim();
|
|
89
|
+
}
|
|
90
|
+
else if (line.startsWith('branch ')) {
|
|
91
|
+
// branch refs/heads/<name>
|
|
92
|
+
const ref = line.slice('branch '.length).trim();
|
|
93
|
+
worktree.branch = ref.replace(/^refs\/heads\//, '');
|
|
94
|
+
}
|
|
95
|
+
else if (line === 'detached') {
|
|
96
|
+
worktree.branch = '(detached HEAD)';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (worktree.path) {
|
|
100
|
+
worktrees.push({
|
|
101
|
+
path: worktree.path,
|
|
102
|
+
branch: worktree.branch ?? '',
|
|
103
|
+
head: worktree.head ?? '',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return worktrees;
|
|
108
|
+
}
|
|
109
|
+
export function worktreeExists(projectPath, branchName) {
|
|
110
|
+
try {
|
|
111
|
+
const worktrees = listWorktrees(projectPath);
|
|
112
|
+
return worktrees.some((wt) => wt.branch === branchName);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
function git(repoPath, args) {
|
|
3
|
+
return execFileSync('git', args, { cwd: repoPath, encoding: 'utf-8' }).trim();
|
|
4
|
+
}
|
|
5
|
+
export function getCurrentBranch(repoPath) {
|
|
6
|
+
return git(repoPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
7
|
+
}
|
|
8
|
+
export function listBranches(repoPath) {
|
|
9
|
+
const output = git(repoPath, ['branch', '--format=%(refname:short)']);
|
|
10
|
+
return output
|
|
11
|
+
.split('\n')
|
|
12
|
+
.map((b) => b.trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
export class BranchAlreadyExistsError extends Error {
|
|
16
|
+
constructor(branchName) {
|
|
17
|
+
super(`Branch '${branchName}' already exists`);
|
|
18
|
+
this.name = 'BranchAlreadyExistsError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* M3: Shared utility to detect "branch already exists" git error messages
|
|
23
|
+
* across different locales (English, French, Russian).
|
|
24
|
+
*/
|
|
25
|
+
export function isGitBranchExistsError(message) {
|
|
26
|
+
const lower = message.toLowerCase();
|
|
27
|
+
return lower.includes('already exists') || lower.includes('existe') || lower.includes('существует');
|
|
28
|
+
}
|
|
29
|
+
export function createBranch(repoPath, branchName, sourceBranch) {
|
|
30
|
+
try {
|
|
31
|
+
git(repoPath, ['branch', branchName, sourceBranch]);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
35
|
+
if (isGitBranchExistsError(message)) {
|
|
36
|
+
throw new BranchAlreadyExistsError(branchName);
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Failed to create branch '${branchName}' from '${sourceBranch}': ${message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function getDiffStats(repoPath) {
|
|
42
|
+
try {
|
|
43
|
+
const output = git(repoPath, ['diff', '--cached', '--shortstat']);
|
|
44
|
+
return parseDiffShortstat(output);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function parseDiffShortstat(output) {
|
|
51
|
+
if (!output.trim()) {
|
|
52
|
+
return { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
53
|
+
}
|
|
54
|
+
const filesMatch = output.match(/(\d+) file/);
|
|
55
|
+
const insertMatch = output.match(/(\d+) insertion/);
|
|
56
|
+
const deleteMatch = output.match(/(\d+) deletion/);
|
|
57
|
+
return {
|
|
58
|
+
filesChanged: filesMatch ? parseInt(filesMatch[1], 10) : 0,
|
|
59
|
+
insertions: insertMatch ? parseInt(insertMatch[1], 10) : 0,
|
|
60
|
+
deletions: deleteMatch ? parseInt(deleteMatch[1], 10) : 0,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function listRemoteBranches(repoPath) {
|
|
64
|
+
try {
|
|
65
|
+
const output = git(repoPath, ['branch', '-r', '--format=%(refname:short)']);
|
|
66
|
+
return output
|
|
67
|
+
.split('\n')
|
|
68
|
+
.map((b) => b.trim())
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function deleteLocalBranch(repoPath, branchName) {
|
|
76
|
+
try {
|
|
77
|
+
git(repoPath, ['branch', '-D', branchName]);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
81
|
+
throw new Error(`Failed to delete local branch '${branchName}': ${message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function deleteRemoteBranch(repoPath, branchName, remote = 'origin') {
|
|
85
|
+
try {
|
|
86
|
+
git(repoPath, ['push', remote, '--delete', branchName]);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
throw new Error(`Failed to delete remote branch '${remote}/${branchName}': ${message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function pushBranch(repoPath, branchName, remote = 'origin') {
|
|
94
|
+
try {
|
|
95
|
+
git(repoPath, ['push', '-u', remote, branchName]);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
99
|
+
throw new Error(`Failed to push branch '${branchName}' to '${remote}': ${message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function getCommitsBetween(repoPath, base, head) {
|
|
103
|
+
try {
|
|
104
|
+
return git(repoPath, ['log', `${base}..${head}`, '--pretty=format:- %s (%h)', '--no-merges']);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return '';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function getDiffStatsBetween(repoPath, base, head) {
|
|
111
|
+
try {
|
|
112
|
+
return git(repoPath, ['diff', '--shortstat', `${base}...${head}`]);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
// Package root resolved from this file's location via pure path arithmetic
|
|
6
|
+
// (no filesystem calls, so this stays robust when tests mock `node:fs`).
|
|
7
|
+
// This file lives at `<root>/src/server/utils/paths.ts` in dev (tsx) and at
|
|
8
|
+
// `<root>/dist/server/utils/paths.js` in production (node) — both are exactly
|
|
9
|
+
// three directories deep from the package root.
|
|
10
|
+
//
|
|
11
|
+
// Do NOT move this file without updating the parent count. A unit test in
|
|
12
|
+
// src/__tests__/paths.test.ts verifies the result points at a directory that
|
|
13
|
+
// contains package.json.
|
|
14
|
+
const selfDir = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const packageRoot = path.resolve(selfDir, '..', '..', '..');
|
|
16
|
+
/**
|
|
17
|
+
* Resolves a path inside the Kōbō package (e.g. compiled MCP server, built SPA).
|
|
18
|
+
* Never use this for user data — those go through getKoboHome() / getDataDir().
|
|
19
|
+
*/
|
|
20
|
+
export function getPackageAssetPath(...parts) {
|
|
21
|
+
return path.join(packageRoot, ...parts);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolves the Kōbō home directory for user data (DB, settings). Respects
|
|
25
|
+
* KOBO_HOME when set, otherwise defaults to an XDG-compliant location under
|
|
26
|
+
* ~/.config/kobo. The directory is NOT created here — callers are responsible
|
|
27
|
+
* for mkdir before writing.
|
|
28
|
+
*
|
|
29
|
+
* Dev workflow: `npm run dev` sets KOBO_HOME=./data so local development uses
|
|
30
|
+
* the repo-relative data/ directory and never touches the user's real home.
|
|
31
|
+
*/
|
|
32
|
+
export function getKoboHome() {
|
|
33
|
+
if (process.env.KOBO_HOME) {
|
|
34
|
+
return path.resolve(process.env.KOBO_HOME);
|
|
35
|
+
}
|
|
36
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
37
|
+
if (xdgConfigHome) {
|
|
38
|
+
return path.join(xdgConfigHome, 'kobo');
|
|
39
|
+
}
|
|
40
|
+
return path.join(os.homedir(), '.config', 'kobo');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Same as getKoboHome(), but guarantees the directory exists on disk. Call this
|
|
44
|
+
* before any filesystem write into the Kōbō home.
|
|
45
|
+
*/
|
|
46
|
+
export function ensureKoboHome() {
|
|
47
|
+
const dir = getKoboHome();
|
|
48
|
+
if (!fs.existsSync(dir)) {
|
|
49
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
return dir;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Absolute path to the SQLite database file under the Kōbō home.
|
|
55
|
+
*/
|
|
56
|
+
export function getDbPath() {
|
|
57
|
+
return path.join(getKoboHome(), 'kobo.db');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Absolute path to settings.json under the Kōbō home.
|
|
61
|
+
*/
|
|
62
|
+
export function getSettingsPath() {
|
|
63
|
+
return path.join(getKoboHome(), 'settings.json');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Absolute path to skills.json under the Kōbō home — cached list of Claude
|
|
67
|
+
* Code slash commands discovered from system init events.
|
|
68
|
+
*/
|
|
69
|
+
export function getSkillsPath() {
|
|
70
|
+
return path.join(getKoboHome(), 'skills.json');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Absolute path to the compiled MCP server entry (shipped in the published
|
|
74
|
+
* package as dist/mcp-server/kobo-tasks-server.js). Returns null if not
|
|
75
|
+
* present — callers (agent-manager) then fall back to the TS source for dev.
|
|
76
|
+
*/
|
|
77
|
+
export function getCompiledMcpServerPath() {
|
|
78
|
+
const compiled = getPackageAssetPath('dist', 'mcp-server', 'kobo-tasks-server.js');
|
|
79
|
+
return fs.existsSync(compiled) ? compiled : null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Absolute path to the MCP server TypeScript source (used in dev when the
|
|
83
|
+
* compiled version is absent).
|
|
84
|
+
*/
|
|
85
|
+
export function getMcpServerSourcePath() {
|
|
86
|
+
return getPackageAssetPath('src', 'mcp-server', 'kobo-tasks-server.ts');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Absolute path to the built Quasar SPA (src/client/dist/spa). Returns null
|
|
90
|
+
* if the SPA has not been built yet.
|
|
91
|
+
*/
|
|
92
|
+
export function getClientSpaPath() {
|
|
93
|
+
const spa = getPackageAssetPath('src', 'client', 'dist', 'spa');
|
|
94
|
+
return fs.existsSync(spa) ? spa : null;
|
|
95
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const trackedProcesses = new Map();
|
|
2
|
+
export function registerProcess(id, proc) {
|
|
3
|
+
trackedProcesses.set(id, proc);
|
|
4
|
+
}
|
|
5
|
+
export function unregisterProcess(id) {
|
|
6
|
+
trackedProcesses.delete(id);
|
|
7
|
+
}
|
|
8
|
+
export function getProcess(id) {
|
|
9
|
+
return trackedProcesses.get(id);
|
|
10
|
+
}
|
|
11
|
+
export function getTrackedCount() {
|
|
12
|
+
return trackedProcesses.size;
|
|
13
|
+
}
|
|
14
|
+
export function killAll() {
|
|
15
|
+
const procs = [...trackedProcesses.values()];
|
|
16
|
+
trackedProcesses.clear();
|
|
17
|
+
const killTimers = procs.map((proc) => {
|
|
18
|
+
try {
|
|
19
|
+
proc.kill('SIGTERM');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Process may already be dead
|
|
23
|
+
}
|
|
24
|
+
return setTimeout(() => {
|
|
25
|
+
try {
|
|
26
|
+
if (!proc.killed) {
|
|
27
|
+
proc.kill('SIGKILL');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Ignore
|
|
32
|
+
}
|
|
33
|
+
}, 5000);
|
|
34
|
+
});
|
|
35
|
+
// Unref timers so they don't keep the process alive
|
|
36
|
+
killTimers.forEach((t) => {
|
|
37
|
+
t.unref?.();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function initProcessCleanup() {
|
|
41
|
+
process.on('exit', killAll);
|
|
42
|
+
process.on('SIGINT', () => {
|
|
43
|
+
killAll();
|
|
44
|
+
process.exit(0);
|
|
45
|
+
});
|
|
46
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@loicngr/kobo",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "GPL-3.0-or-later",
|
|
7
|
+
"bin": {
|
|
8
|
+
"kobo": "./dist/server/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"src/client/dist/spa/",
|
|
13
|
+
"src/mcp-server/",
|
|
14
|
+
"AGENTS.md",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public",
|
|
20
|
+
"provenance": true
|
|
21
|
+
},
|
|
22
|
+
"author": {
|
|
23
|
+
"name": "loicngr",
|
|
24
|
+
"email": "loicnogier@ik.me",
|
|
25
|
+
"url": "https://github.com/loicngr"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/loicngr/kobo.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/loicngr/kobo/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/loicngr/kobo#readme",
|
|
35
|
+
"keywords": [
|
|
36
|
+
"claude",
|
|
37
|
+
"claude-code",
|
|
38
|
+
"agent",
|
|
39
|
+
"mcp",
|
|
40
|
+
"workspace",
|
|
41
|
+
"worktree",
|
|
42
|
+
"hono",
|
|
43
|
+
"vue",
|
|
44
|
+
"quasar"
|
|
45
|
+
],
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"dev": "KOBO_HOME=./data tsx watch src/server/index.ts",
|
|
51
|
+
"dev:client": "cd src/client && npx quasar dev",
|
|
52
|
+
"dev:all": "concurrently \"npm run dev\" \"npm run dev:client\"",
|
|
53
|
+
"build:client": "cd src/client && npx quasar build",
|
|
54
|
+
"build:server": "npx tsc && chmod +x dist/server/index.js",
|
|
55
|
+
"build": "npm run build:client && npm run build:server",
|
|
56
|
+
"start": "node dist/server/index.js",
|
|
57
|
+
"test": "vitest run --config vitest.config.ts --root .",
|
|
58
|
+
"test:watch": "vitest --config vitest.config.ts --root .",
|
|
59
|
+
"lint": "biome check .",
|
|
60
|
+
"lint:fix": "biome check --write .",
|
|
61
|
+
"format": "biome format --write .",
|
|
62
|
+
"format:check": "biome format .",
|
|
63
|
+
"prepublishOnly": "npm run build"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@hono/node-server": "^1.19.12",
|
|
67
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
68
|
+
"better-sqlite3": "^12.8.0",
|
|
69
|
+
"hono": "^4.12.10",
|
|
70
|
+
"nanoid": "^5.1.7",
|
|
71
|
+
"ws": "^8.20.0"
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"@biomejs/biome": "2.4.10",
|
|
75
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
76
|
+
"@types/node": "^25.5.2",
|
|
77
|
+
"@types/ws": "^8.18.1",
|
|
78
|
+
"@vitest/runner": "^4.1.2",
|
|
79
|
+
"concurrently": "^9.2.1",
|
|
80
|
+
"tsx": "^4.21.0",
|
|
81
|
+
"typescript": "^6.0.2",
|
|
82
|
+
"vitest": "^3.2.4"
|
|
83
|
+
}
|
|
84
|
+
}
|