commandmate 0.3.1 → 0.3.3
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/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +11 -11
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +4 -4
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/1.pack +0 -0
- package/.next/cache/webpack/client-production/2.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/routes-manifest.json +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update-check/route.js +1 -1
- package/.next/server/app/api/external-apps/[id]/health/route.js +1 -1
- package/.next/server/app/api/external-apps/[id]/route.js +1 -1
- package/.next/server/app/api/external-apps/route.js +1 -1
- package/.next/server/app/api/hooks/claude-done/route.js +1 -1
- package/.next/server/app/api/ollama/models/route.js +1 -0
- package/.next/server/app/api/ollama/models/route.js.nft.json +1 -0
- package/.next/server/app/api/ollama/models.body +1 -0
- package/.next/server/app/api/ollama/models.meta +1 -0
- package/.next/server/app/api/repositories/clone/[jobId]/route.js +1 -1
- package/.next/server/app/api/repositories/clone/route.js +1 -1
- package/.next/server/app/api/repositories/excluded/route.js +7 -7
- package/.next/server/app/api/repositories/restore/route.js +3 -3
- package/.next/server/app/api/repositories/route.js +13 -11
- package/.next/server/app/api/repositories/route.js.nft.json +1 -1
- package/.next/server/app/api/repositories/scan/route.js +1 -1
- package/.next/server/app/api/repositories/sync/route.js +3 -3
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/cli-tool/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js +1 -0
- package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route.js.nft.json +1 -0
- package/.next/server/app/api/worktrees/[id]/execution-logs/route.js +9 -0
- package/.next/server/app/api/worktrees/[id]/execution-logs/route.js.nft.json +1 -0
- package/.next/server/app/api/worktrees/[id]/files/[...path]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/interrupt/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/kill-session/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/[filename]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/route.js +2 -2
- package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/memos/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/messages/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js +1 -0
- package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route.js.nft.json +1 -0
- package/.next/server/app/api/worktrees/[id]/schedules/route.js +4 -0
- package/.next/server/app/api/worktrees/[id]/schedules/route.js.nft.json +1 -0
- package/.next/server/app/api/worktrees/[id]/search/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/start-polling/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/start-polling/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/[...path]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/upload/[...path]/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/viewed/route.js +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
- package/.next/server/app/login/page.js.nft.json +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/proxy/[...path]/route.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page.js.nft.json +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +8 -3
- package/.next/server/app/worktrees/[id]/page.js.nft.json +1 -1
- package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page.js.nft.json +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +10 -5
- package/.next/server/chunks/2314.js +1 -0
- package/.next/server/chunks/3860.js +1 -1
- package/.next/server/chunks/4559.js +1 -1
- package/.next/server/chunks/539.js +10 -10
- package/.next/server/chunks/5853.js +1 -1
- package/.next/server/chunks/6228.js +1 -0
- package/.next/server/chunks/7425.js +112 -37
- package/.next/server/chunks/7566.js +1 -1
- package/.next/server/chunks/8693.js +1 -1
- package/.next/server/chunks/9446.js +1 -0
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-manifest.json +5 -5
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/8091-274bc0716106e7fc.js +1 -0
- package/.next/static/chunks/app/page-060057e02b841125.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/page-78580947c201d698.js +1 -0
- package/.next/static/chunks/{main-db79434ee4a6c931.js → main-2feda12a4d321111.js} +1 -1
- package/.next/static/css/e85de230ef5ddc40.css +3 -0
- package/.next/trace +5 -5
- package/.next/types/app/api/ollama/models/route.ts +343 -0
- package/.next/types/app/api/worktrees/[id]/execution-logs/[logId]/route.ts +343 -0
- package/.next/types/app/api/worktrees/[id]/execution-logs/route.ts +343 -0
- package/.next/types/app/api/worktrees/[id]/schedules/[scheduleId]/route.ts +343 -0
- package/.next/types/app/api/worktrees/[id]/schedules/route.ts +343 -0
- package/README.md +74 -76
- package/dist/cli/utils/docs-reader.d.ts.map +1 -1
- package/dist/cli/utils/docs-reader.js +1 -0
- package/dist/server/server.js +5 -0
- package/dist/server/src/config/cmate-constants.js +79 -0
- package/dist/server/src/config/schedule-config.js +60 -0
- package/dist/server/src/lib/auto-yes-manager.js +2 -2
- package/dist/server/src/lib/claude-executor.js +158 -0
- package/dist/server/src/lib/cli-patterns.js +73 -9
- package/dist/server/src/lib/cli-tools/gemini.js +81 -22
- package/dist/server/src/lib/cli-tools/manager.js +4 -2
- package/dist/server/src/lib/cli-tools/types.js +64 -2
- package/dist/server/src/lib/cli-tools/vibe-local.js +163 -0
- package/dist/server/src/lib/cmate-parser.js +262 -0
- package/dist/server/src/lib/db-instance.js +3 -0
- package/dist/server/src/lib/db-migrations.js +145 -2
- package/dist/server/src/lib/db.js +51 -1
- package/dist/server/src/lib/env-sanitizer.js +57 -0
- package/dist/server/src/lib/prompt-detector.js +4 -3
- package/dist/server/src/lib/response-poller.js +22 -11
- package/dist/server/src/lib/schedule-manager.js +401 -0
- package/dist/server/src/lib/selected-agents-validator.js +99 -0
- package/dist/server/src/types/cmate.js +6 -0
- package/dist/server/src/types/sidebar.js +9 -4
- package/package.json +2 -1
- package/.next/server/chunks/7536.js +0 -1
- package/.next/static/chunks/8091-925542bdfc843dce.js +0 -1
- package/.next/static/chunks/app/page-238b5a70d8c101e9.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/page-a556551ce5c69dec.js +0 -1
- package/.next/static/css/b9ea6a4fad17dc32.css +0 -3
- /package/.next/static/{hmAjbCPjxX_C0Os7rphI1 → O7EDFfAYQNe_HRbORxQAC}/_buildManifest.js +0 -0
- /package/.next/static/{hmAjbCPjxX_C0Os7rphI1 → O7EDFfAYQNe_HRbORxQAC}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Vibe Local CLI tool implementation
|
|
4
|
+
* Provides integration with vibe-local (vibe-coder) in interactive mode
|
|
5
|
+
*
|
|
6
|
+
* @remarks Issue #368: Rewritten from non-interactive pipe mode to interactive REPL mode.
|
|
7
|
+
* Previous implementation used `echo 'msg' | vibe-local` which caused the process to exit
|
|
8
|
+
* immediately with "(Cancelled)" + "Goodbye!", making response polling impossible.
|
|
9
|
+
* Now launches `vibe-local -y` in interactive mode within tmux (same approach as Claude/Codex/Gemini).
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.VibeLocalTool = void 0;
|
|
13
|
+
const base_1 = require("./base");
|
|
14
|
+
const types_1 = require("./types");
|
|
15
|
+
const tmux_1 = require("../tmux");
|
|
16
|
+
const pasted_text_helper_1 = require("../pasted-text-helper");
|
|
17
|
+
const db_instance_1 = require("../db-instance");
|
|
18
|
+
const db_1 = require("../db");
|
|
19
|
+
/**
|
|
20
|
+
* Extract error message from unknown error type (DRY)
|
|
21
|
+
*/
|
|
22
|
+
function getErrorMessage(error) {
|
|
23
|
+
return error instanceof Error ? error.message : String(error);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Wait for vibe-local to initialize after launch.
|
|
27
|
+
* vibe-local shows a permission check prompt, banner, and model loading.
|
|
28
|
+
*/
|
|
29
|
+
const VIBE_LOCAL_INIT_WAIT_MS = 5000;
|
|
30
|
+
/**
|
|
31
|
+
* Vibe Local CLI tool implementation
|
|
32
|
+
* Manages vibe-local interactive sessions using tmux
|
|
33
|
+
*/
|
|
34
|
+
class VibeLocalTool extends base_1.BaseCLITool {
|
|
35
|
+
id = 'vibe-local';
|
|
36
|
+
name = 'Vibe Local';
|
|
37
|
+
command = 'vibe-local';
|
|
38
|
+
/**
|
|
39
|
+
* Check if vibe-local session is running for a worktree
|
|
40
|
+
*/
|
|
41
|
+
async isRunning(worktreeId) {
|
|
42
|
+
const sessionName = this.getSessionName(worktreeId);
|
|
43
|
+
return await (0, tmux_1.hasSession)(sessionName);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Start a new vibe-local session for a worktree
|
|
47
|
+
* Launches `vibe-local -y` in interactive mode within tmux
|
|
48
|
+
*
|
|
49
|
+
* @param worktreeId - Worktree ID
|
|
50
|
+
* @param worktreePath - Worktree path
|
|
51
|
+
*/
|
|
52
|
+
async startSession(worktreeId, worktreePath) {
|
|
53
|
+
const vibeLocalAvailable = await this.isInstalled();
|
|
54
|
+
if (!vibeLocalAvailable) {
|
|
55
|
+
throw new Error('vibe-local is not installed or not in PATH');
|
|
56
|
+
}
|
|
57
|
+
const sessionName = this.getSessionName(worktreeId);
|
|
58
|
+
const exists = await (0, tmux_1.hasSession)(sessionName);
|
|
59
|
+
if (exists) {
|
|
60
|
+
console.log(`Vibe Local session ${sessionName} already exists`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
// Create tmux session with large history buffer
|
|
65
|
+
await (0, tmux_1.createSession)({
|
|
66
|
+
sessionName,
|
|
67
|
+
workingDirectory: worktreePath,
|
|
68
|
+
historyLimit: 50000,
|
|
69
|
+
});
|
|
70
|
+
// Wait a moment for the session to be created
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
72
|
+
// Read Ollama model preference from DB
|
|
73
|
+
// [SEC-001] Re-validate model name at point of use (defense-in-depth)
|
|
74
|
+
let vibeLocalCommand = 'vibe-local -y';
|
|
75
|
+
try {
|
|
76
|
+
const db = (0, db_instance_1.getDbInstance)();
|
|
77
|
+
const wt = (0, db_1.getWorktreeById)(db, worktreeId);
|
|
78
|
+
if (wt?.vibeLocalModel && types_1.OLLAMA_MODEL_PATTERN.test(wt.vibeLocalModel)) {
|
|
79
|
+
vibeLocalCommand = `vibe-local -y -m ${wt.vibeLocalModel}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// DB read failure is non-fatal; use default model
|
|
84
|
+
}
|
|
85
|
+
// Start vibe-local in interactive mode with auto-approve (-y)
|
|
86
|
+
// -y flag skips the permission confirmation prompt
|
|
87
|
+
await (0, tmux_1.sendKeys)(sessionName, vibeLocalCommand, true);
|
|
88
|
+
// Wait for vibe-local to initialize (banner + model loading)
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, VIBE_LOCAL_INIT_WAIT_MS));
|
|
90
|
+
console.log(`✓ Started Vibe Local session: ${sessionName}`);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const errorMessage = getErrorMessage(error);
|
|
94
|
+
throw new Error(`Failed to start Vibe Local session: ${errorMessage}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Send a message to vibe-local interactive session
|
|
99
|
+
*
|
|
100
|
+
* @param worktreeId - Worktree ID
|
|
101
|
+
* @param message - Message to send
|
|
102
|
+
*/
|
|
103
|
+
async sendMessage(worktreeId, message) {
|
|
104
|
+
const sessionName = this.getSessionName(worktreeId);
|
|
105
|
+
const exists = await (0, tmux_1.hasSession)(sessionName);
|
|
106
|
+
if (!exists) {
|
|
107
|
+
throw new Error(`Vibe Local session ${sessionName} does not exist. Start the session first.`);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
// Send message to vibe-local (without Enter)
|
|
111
|
+
await (0, tmux_1.sendKeys)(sessionName, message, false);
|
|
112
|
+
// Wait a moment for the text to be typed
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
114
|
+
// vibe-local uses IME mode: first Enter creates a new line,
|
|
115
|
+
// second Enter on empty line submits the message.
|
|
116
|
+
// Send Enter twice with a short delay between.
|
|
117
|
+
await (0, tmux_1.sendSpecialKey)(sessionName, 'C-m');
|
|
118
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
119
|
+
await (0, tmux_1.sendSpecialKey)(sessionName, 'C-m');
|
|
120
|
+
// Wait a moment for the message to be processed
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
122
|
+
// Detect [Pasted text] and resend Enter for multi-line messages
|
|
123
|
+
if (message.includes('\n')) {
|
|
124
|
+
await (0, pasted_text_helper_1.detectAndResendIfPastedText)(sessionName);
|
|
125
|
+
}
|
|
126
|
+
console.log(`✓ Sent message to Vibe Local session: ${sessionName}`);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const errorMessage = getErrorMessage(error);
|
|
130
|
+
throw new Error(`Failed to send message to Vibe Local: ${errorMessage}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Kill vibe-local session
|
|
135
|
+
*
|
|
136
|
+
* @param worktreeId - Worktree ID
|
|
137
|
+
*/
|
|
138
|
+
async killSession(worktreeId) {
|
|
139
|
+
const sessionName = this.getSessionName(worktreeId);
|
|
140
|
+
try {
|
|
141
|
+
const exists = await (0, tmux_1.hasSession)(sessionName);
|
|
142
|
+
if (exists) {
|
|
143
|
+
// Send Ctrl+C to interrupt any running operation
|
|
144
|
+
await (0, tmux_1.sendSpecialKey)(sessionName, 'C-c');
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
146
|
+
// Send Ctrl+C again to ensure exit
|
|
147
|
+
await (0, tmux_1.sendSpecialKey)(sessionName, 'C-c');
|
|
148
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
149
|
+
}
|
|
150
|
+
// Kill the tmux session
|
|
151
|
+
const killed = await (0, tmux_1.killSession)(sessionName);
|
|
152
|
+
if (killed) {
|
|
153
|
+
console.log(`✓ Stopped Vibe Local session: ${sessionName}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const errorMessage = getErrorMessage(error);
|
|
158
|
+
console.error(`Error stopping Vibe Local session: ${errorMessage}`);
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
exports.VibeLocalTool = VibeLocalTool;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CMATE.md Parser
|
|
4
|
+
* Issue #294: Schedule execution feature
|
|
5
|
+
*
|
|
6
|
+
* Parses CMATE.md files (Markdown table format) from worktree root directories.
|
|
7
|
+
* Provides a generic table parser and a specialized schedule section parser.
|
|
8
|
+
*
|
|
9
|
+
* Security:
|
|
10
|
+
* - Path traversal prevention (realpath + worktree directory validation)
|
|
11
|
+
* - Unicode control character sanitization
|
|
12
|
+
* - Name validation with strict pattern matching
|
|
13
|
+
* - Cron expression validation
|
|
14
|
+
*/
|
|
15
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
16
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
17
|
+
};
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.MIN_CRON_INTERVAL = exports.CONTROL_CHAR_REGEX = exports.isValidCronExpression = exports.MAX_SCHEDULE_ENTRIES = exports.MAX_CRON_EXPRESSION_LENGTH = exports.NAME_PATTERN = exports.CMATE_FILENAME = void 0;
|
|
20
|
+
exports.sanitizeMessageContent = sanitizeMessageContent;
|
|
21
|
+
exports.validateCmatePath = validateCmatePath;
|
|
22
|
+
exports.parseCmateFile = parseCmateFile;
|
|
23
|
+
exports.parseSchedulesSection = parseSchedulesSection;
|
|
24
|
+
exports.readCmateFile = readCmateFile;
|
|
25
|
+
const fs_1 = require("fs");
|
|
26
|
+
const path_1 = __importDefault(require("path"));
|
|
27
|
+
const types_1 = require("../lib/cli-tools/types");
|
|
28
|
+
const schedule_config_1 = require("../config/schedule-config");
|
|
29
|
+
const cmate_constants_1 = require("../config/cmate-constants");
|
|
30
|
+
Object.defineProperty(exports, "CMATE_FILENAME", { enumerable: true, get: function () { return cmate_constants_1.CMATE_FILENAME; } });
|
|
31
|
+
Object.defineProperty(exports, "NAME_PATTERN", { enumerable: true, get: function () { return cmate_constants_1.NAME_PATTERN; } });
|
|
32
|
+
Object.defineProperty(exports, "MAX_CRON_EXPRESSION_LENGTH", { enumerable: true, get: function () { return cmate_constants_1.MAX_CRON_EXPRESSION_LENGTH; } });
|
|
33
|
+
Object.defineProperty(exports, "MAX_SCHEDULE_ENTRIES", { enumerable: true, get: function () { return cmate_constants_1.MAX_SCHEDULE_ENTRIES; } });
|
|
34
|
+
Object.defineProperty(exports, "isValidCronExpression", { enumerable: true, get: function () { return cmate_constants_1.isValidCronExpression; } });
|
|
35
|
+
/**
|
|
36
|
+
* @deprecated Use CONTROL_CHAR_PATTERN from '../config/cmate-constants' instead.
|
|
37
|
+
* Kept for backward compatibility with existing tests.
|
|
38
|
+
*/
|
|
39
|
+
exports.CONTROL_CHAR_REGEX = new RegExp(cmate_constants_1.CONTROL_CHAR_PATTERN.source, 'g');
|
|
40
|
+
/** Minimum cron interval pattern (every minute) */
|
|
41
|
+
exports.MIN_CRON_INTERVAL = '* * * * *';
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// Sanitization
|
|
44
|
+
// =============================================================================
|
|
45
|
+
/**
|
|
46
|
+
* Remove Unicode control characters from a string.
|
|
47
|
+
* Preserves tabs (\t), newlines (\n), and carriage returns (\r).
|
|
48
|
+
*
|
|
49
|
+
* @param content - Raw string to sanitize
|
|
50
|
+
* @returns Sanitized string with control characters removed
|
|
51
|
+
*/
|
|
52
|
+
function sanitizeMessageContent(content) {
|
|
53
|
+
return (0, cmate_constants_1.sanitizeContent)(content);
|
|
54
|
+
}
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// Path Validation
|
|
57
|
+
// =============================================================================
|
|
58
|
+
/**
|
|
59
|
+
* Validate that a CMATE.md file path is within the expected worktree directory.
|
|
60
|
+
* Prevents path traversal attacks by resolving symlinks and verifying containment.
|
|
61
|
+
*
|
|
62
|
+
* @param filePath - Path to CMATE.md file
|
|
63
|
+
* @param worktreeDir - Expected worktree directory
|
|
64
|
+
* @returns true if path is valid and within worktree directory
|
|
65
|
+
* @throws Error if path traversal is detected
|
|
66
|
+
*/
|
|
67
|
+
function validateCmatePath(filePath, worktreeDir) {
|
|
68
|
+
const realFilePath = (0, fs_1.realpathSync)(filePath);
|
|
69
|
+
const realWorktreeDir = (0, fs_1.realpathSync)(worktreeDir);
|
|
70
|
+
// Ensure the file is within the worktree directory
|
|
71
|
+
if (!realFilePath.startsWith(realWorktreeDir + path_1.default.sep) &&
|
|
72
|
+
realFilePath !== path_1.default.join(realWorktreeDir, cmate_constants_1.CMATE_FILENAME)) {
|
|
73
|
+
throw new Error(`Path traversal detected: ${filePath} is not within ${worktreeDir}`);
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// Generic Markdown Table Parser
|
|
79
|
+
// =============================================================================
|
|
80
|
+
/**
|
|
81
|
+
* Parse a CMATE.md file into a generic structure.
|
|
82
|
+
* Returns a Map where keys are section names (from ## headers)
|
|
83
|
+
* and values are arrays of row data (each row is an array of cell values).
|
|
84
|
+
*
|
|
85
|
+
* [S1-010] Generic design: returns Map<string, string[][]>
|
|
86
|
+
*
|
|
87
|
+
* @param content - Raw CMATE.md file content
|
|
88
|
+
* @returns Map of section name to table rows
|
|
89
|
+
*/
|
|
90
|
+
function parseCmateFile(content) {
|
|
91
|
+
const result = new Map();
|
|
92
|
+
const lines = content.split('\n');
|
|
93
|
+
let currentSection = null;
|
|
94
|
+
let headerParsed = false;
|
|
95
|
+
let separatorParsed = false;
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const trimmed = line.trim();
|
|
98
|
+
// Detect section headers (## SectionName)
|
|
99
|
+
const headerMatch = trimmed.match(/^##\s+(.+)$/);
|
|
100
|
+
if (headerMatch) {
|
|
101
|
+
currentSection = headerMatch[1].trim();
|
|
102
|
+
headerParsed = false;
|
|
103
|
+
separatorParsed = false;
|
|
104
|
+
if (!result.has(currentSection)) {
|
|
105
|
+
result.set(currentSection, []);
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// Skip empty lines
|
|
110
|
+
if (!trimmed) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// Skip non-table lines
|
|
114
|
+
if (!trimmed.startsWith('|')) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!currentSection) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
// Parse table row
|
|
121
|
+
if (!headerParsed) {
|
|
122
|
+
// First row is header - skip it
|
|
123
|
+
headerParsed = true;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!separatorParsed) {
|
|
127
|
+
// Second row is separator (|---|---|) - skip it
|
|
128
|
+
if (trimmed.match(/^\|[\s-:|]+\|$/)) {
|
|
129
|
+
separatorParsed = true;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// If it's not a separator, treat it as data
|
|
133
|
+
separatorParsed = true;
|
|
134
|
+
}
|
|
135
|
+
// Parse data row
|
|
136
|
+
const cells = trimmed
|
|
137
|
+
.split('|')
|
|
138
|
+
.slice(1, -1) // Remove leading and trailing empty strings from split
|
|
139
|
+
.map((cell) => cell.trim());
|
|
140
|
+
if (cells.length > 0) {
|
|
141
|
+
result.get(currentSection).push(cells);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
// =============================================================================
|
|
147
|
+
// Schedule Section Parser
|
|
148
|
+
// =============================================================================
|
|
149
|
+
/**
|
|
150
|
+
* Parse the Schedules section of a CMATE.md file into typed ScheduleEntry objects.
|
|
151
|
+
*
|
|
152
|
+
* Expected table format:
|
|
153
|
+
* | Name | Cron | Message | CLI Tool | Enabled |
|
|
154
|
+
* |------|------|---------|----------|---------|
|
|
155
|
+
* | daily-review | 0 9 * * * | Review code changes | claude | true |
|
|
156
|
+
*
|
|
157
|
+
* Entries with invalid names, cron expressions, or missing required fields
|
|
158
|
+
* are silently skipped with a console.warn.
|
|
159
|
+
*
|
|
160
|
+
* @param rows - Raw table rows from parseCmateFile() for the Schedules section
|
|
161
|
+
* @returns Array of validated ScheduleEntry objects
|
|
162
|
+
*/
|
|
163
|
+
function parseSchedulesSection(rows) {
|
|
164
|
+
const entries = [];
|
|
165
|
+
for (const row of rows) {
|
|
166
|
+
if (entries.length >= cmate_constants_1.MAX_SCHEDULE_ENTRIES) {
|
|
167
|
+
console.warn(`[cmate-parser] Maximum schedule entries (${cmate_constants_1.MAX_SCHEDULE_ENTRIES}) reached, skipping remaining`);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
// Minimum required columns: Name, Cron, Message
|
|
171
|
+
if (row.length < 3) {
|
|
172
|
+
console.warn('[cmate-parser] Skipping row with insufficient columns:', row);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const [name, cronExpression, message, cliToolId, enabledStr, permissionStr] = row;
|
|
176
|
+
// Validate name
|
|
177
|
+
const sanitizedName = sanitizeMessageContent(name);
|
|
178
|
+
if (!cmate_constants_1.NAME_PATTERN.test(sanitizedName)) {
|
|
179
|
+
console.warn(`[cmate-parser] Skipping entry with invalid name: "${sanitizedName}"`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// Validate cron expression
|
|
183
|
+
if (!(0, cmate_constants_1.isValidCronExpression)(cronExpression)) {
|
|
184
|
+
console.warn(`[cmate-parser] Skipping entry "${sanitizedName}" with invalid cron: "${cronExpression}"`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
// Sanitize message
|
|
188
|
+
const sanitizedMessage = sanitizeMessageContent(message);
|
|
189
|
+
if (!sanitizedMessage) {
|
|
190
|
+
console.warn(`[cmate-parser] Skipping entry "${sanitizedName}" with empty message`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Parse enabled (default: true)
|
|
194
|
+
const enabled = enabledStr === undefined ||
|
|
195
|
+
enabledStr === '' ||
|
|
196
|
+
enabledStr.toLowerCase() === 'true';
|
|
197
|
+
// Parse and validate CLI tool ID [SEC-002]
|
|
198
|
+
const resolvedCliToolId = cliToolId?.trim() || 'claude';
|
|
199
|
+
if (!(0, types_1.isCliToolType)(resolvedCliToolId)) {
|
|
200
|
+
console.warn(`[cmate-parser] Skipping entry "${sanitizedName}" with invalid CLI tool: "${resolvedCliToolId}"`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const defaultPermission = schedule_config_1.DEFAULT_PERMISSIONS[resolvedCliToolId] ?? '';
|
|
204
|
+
let permission = permissionStr?.trim() || defaultPermission;
|
|
205
|
+
// Validate permission against allowed values
|
|
206
|
+
let allowedValues;
|
|
207
|
+
switch (resolvedCliToolId) {
|
|
208
|
+
case 'codex':
|
|
209
|
+
allowedValues = schedule_config_1.CODEX_SANDBOXES;
|
|
210
|
+
break;
|
|
211
|
+
case 'gemini':
|
|
212
|
+
case 'vibe-local':
|
|
213
|
+
// No permission flags for gemini/vibe-local; only empty string is valid
|
|
214
|
+
allowedValues = [];
|
|
215
|
+
if (permission) {
|
|
216
|
+
console.warn(`[cmate-parser] Permission "${permission}" ignored for ${resolvedCliToolId} in entry "${sanitizedName}" (no permission flags supported)`);
|
|
217
|
+
permission = '';
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
default:
|
|
221
|
+
allowedValues = schedule_config_1.CLAUDE_PERMISSIONS;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
if (allowedValues.length > 0 && permission && !allowedValues.includes(permission)) {
|
|
225
|
+
console.warn(`[cmate-parser] Invalid permission "${permission}" for ${resolvedCliToolId} in entry "${sanitizedName}", using default "${defaultPermission}"`);
|
|
226
|
+
permission = defaultPermission;
|
|
227
|
+
}
|
|
228
|
+
entries.push({
|
|
229
|
+
name: sanitizedName,
|
|
230
|
+
cronExpression: cronExpression.trim(),
|
|
231
|
+
message: sanitizedMessage,
|
|
232
|
+
cliToolId: resolvedCliToolId,
|
|
233
|
+
enabled,
|
|
234
|
+
permission,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return entries;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Read and parse a CMATE.md file from a worktree directory.
|
|
241
|
+
*
|
|
242
|
+
* @param worktreeDir - Path to the worktree directory
|
|
243
|
+
* @returns Parsed CmateConfig, or null if the file doesn't exist
|
|
244
|
+
* @throws Error if path traversal is detected
|
|
245
|
+
*/
|
|
246
|
+
function readCmateFile(worktreeDir) {
|
|
247
|
+
const filePath = path_1.default.join(worktreeDir, cmate_constants_1.CMATE_FILENAME);
|
|
248
|
+
try {
|
|
249
|
+
// Validate path before reading
|
|
250
|
+
validateCmatePath(filePath, worktreeDir);
|
|
251
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
252
|
+
return parseCmateFile(content);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (error instanceof Error &&
|
|
256
|
+
'code' in error &&
|
|
257
|
+
error.code === 'ENOENT') {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -45,6 +45,9 @@ function getDbInstance() {
|
|
|
45
45
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
46
46
|
}
|
|
47
47
|
dbInstance = new better_sqlite3_1.default(dbPath);
|
|
48
|
+
// Issue #294: Enable foreign key enforcement BEFORE migrations
|
|
49
|
+
// This ensures ON DELETE CASCADE works correctly for all tables
|
|
50
|
+
dbInstance.pragma('foreign_keys = ON');
|
|
48
51
|
(0, db_migrations_1.runMigrations)(dbInstance);
|
|
49
52
|
}
|
|
50
53
|
return dbInstance;
|
|
@@ -19,7 +19,7 @@ const db_1 = require("./db");
|
|
|
19
19
|
* Current schema version
|
|
20
20
|
* Increment this when adding new migrations
|
|
21
21
|
*/
|
|
22
|
-
exports.CURRENT_SCHEMA_VERSION =
|
|
22
|
+
exports.CURRENT_SCHEMA_VERSION = 19;
|
|
23
23
|
/**
|
|
24
24
|
* Migration registry
|
|
25
25
|
* All migrations should be added to this array in order
|
|
@@ -717,6 +717,149 @@ const migrations = [
|
|
|
717
717
|
`);
|
|
718
718
|
console.log('✓ Removed issue_no column from external_apps table');
|
|
719
719
|
}
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
version: 17,
|
|
723
|
+
name: 'add-scheduled-executions-and-execution-logs',
|
|
724
|
+
up: (db) => {
|
|
725
|
+
// Issue #294: Schedule execution feature
|
|
726
|
+
// [S3-002] Clean up orphan records BEFORE creating new tables with FK constraints
|
|
727
|
+
// These records may exist if worktrees/repositories were deleted while FK was disabled
|
|
728
|
+
db.exec(`
|
|
729
|
+
DELETE FROM chat_messages WHERE worktree_id NOT IN (SELECT id FROM worktrees);
|
|
730
|
+
`);
|
|
731
|
+
db.exec(`
|
|
732
|
+
DELETE FROM session_states WHERE worktree_id NOT IN (SELECT id FROM worktrees);
|
|
733
|
+
`);
|
|
734
|
+
db.exec(`
|
|
735
|
+
DELETE FROM worktree_memos WHERE worktree_id NOT IN (SELECT id FROM worktrees);
|
|
736
|
+
`);
|
|
737
|
+
db.exec(`
|
|
738
|
+
UPDATE clone_jobs SET repository_id = NULL
|
|
739
|
+
WHERE repository_id IS NOT NULL AND repository_id NOT IN (SELECT id FROM repositories);
|
|
740
|
+
`);
|
|
741
|
+
// Create scheduled_executions table
|
|
742
|
+
db.exec(`
|
|
743
|
+
CREATE TABLE scheduled_executions (
|
|
744
|
+
id TEXT PRIMARY KEY,
|
|
745
|
+
worktree_id TEXT NOT NULL,
|
|
746
|
+
cli_tool_id TEXT DEFAULT 'claude',
|
|
747
|
+
name TEXT NOT NULL,
|
|
748
|
+
message TEXT NOT NULL,
|
|
749
|
+
cron_expression TEXT,
|
|
750
|
+
enabled INTEGER DEFAULT 1,
|
|
751
|
+
last_executed_at INTEGER,
|
|
752
|
+
next_execute_at INTEGER,
|
|
753
|
+
created_at INTEGER NOT NULL,
|
|
754
|
+
updated_at INTEGER NOT NULL,
|
|
755
|
+
UNIQUE(worktree_id, name),
|
|
756
|
+
FOREIGN KEY (worktree_id) REFERENCES worktrees(id) ON DELETE CASCADE
|
|
757
|
+
);
|
|
758
|
+
`);
|
|
759
|
+
// Create index on worktree_id for scheduled_executions
|
|
760
|
+
db.exec(`
|
|
761
|
+
CREATE INDEX idx_scheduled_executions_worktree
|
|
762
|
+
ON scheduled_executions(worktree_id);
|
|
763
|
+
`);
|
|
764
|
+
// Create index on enabled for filtering active schedules
|
|
765
|
+
db.exec(`
|
|
766
|
+
CREATE INDEX idx_scheduled_executions_enabled
|
|
767
|
+
ON scheduled_executions(enabled);
|
|
768
|
+
`);
|
|
769
|
+
// Create execution_logs table
|
|
770
|
+
db.exec(`
|
|
771
|
+
CREATE TABLE execution_logs (
|
|
772
|
+
id TEXT PRIMARY KEY,
|
|
773
|
+
schedule_id TEXT NOT NULL,
|
|
774
|
+
worktree_id TEXT NOT NULL,
|
|
775
|
+
message TEXT NOT NULL,
|
|
776
|
+
result TEXT,
|
|
777
|
+
exit_code INTEGER,
|
|
778
|
+
status TEXT DEFAULT 'running' CHECK(status IN ('running', 'completed', 'failed', 'timeout', 'cancelled')),
|
|
779
|
+
started_at INTEGER NOT NULL,
|
|
780
|
+
completed_at INTEGER,
|
|
781
|
+
created_at INTEGER NOT NULL,
|
|
782
|
+
FOREIGN KEY (schedule_id) REFERENCES scheduled_executions(id) ON DELETE CASCADE,
|
|
783
|
+
FOREIGN KEY (worktree_id) REFERENCES worktrees(id) ON DELETE CASCADE
|
|
784
|
+
);
|
|
785
|
+
`);
|
|
786
|
+
// Create indexes for execution_logs
|
|
787
|
+
db.exec(`
|
|
788
|
+
CREATE INDEX idx_execution_logs_schedule
|
|
789
|
+
ON execution_logs(schedule_id);
|
|
790
|
+
`);
|
|
791
|
+
db.exec(`
|
|
792
|
+
CREATE INDEX idx_execution_logs_worktree
|
|
793
|
+
ON execution_logs(worktree_id);
|
|
794
|
+
`);
|
|
795
|
+
db.exec(`
|
|
796
|
+
CREATE INDEX idx_execution_logs_status
|
|
797
|
+
ON execution_logs(status);
|
|
798
|
+
`);
|
|
799
|
+
console.log('✓ Cleaned up orphan records');
|
|
800
|
+
console.log('✓ Created scheduled_executions table');
|
|
801
|
+
console.log('✓ Created execution_logs table');
|
|
802
|
+
console.log('✓ Created indexes for schedule tables');
|
|
803
|
+
},
|
|
804
|
+
down: (db) => {
|
|
805
|
+
db.exec('DROP INDEX IF EXISTS idx_execution_logs_status');
|
|
806
|
+
db.exec('DROP INDEX IF EXISTS idx_execution_logs_worktree');
|
|
807
|
+
db.exec('DROP INDEX IF EXISTS idx_execution_logs_schedule');
|
|
808
|
+
db.exec('DROP TABLE IF EXISTS execution_logs');
|
|
809
|
+
db.exec('DROP INDEX IF EXISTS idx_scheduled_executions_enabled');
|
|
810
|
+
db.exec('DROP INDEX IF EXISTS idx_scheduled_executions_worktree');
|
|
811
|
+
db.exec('DROP TABLE IF EXISTS scheduled_executions');
|
|
812
|
+
console.log('✓ Dropped scheduled_executions and execution_logs tables');
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
version: 18,
|
|
817
|
+
name: 'add-selected-agents-column',
|
|
818
|
+
up: (db) => {
|
|
819
|
+
// Issue #368: Add selected_agents column for agent selection persistence
|
|
820
|
+
// NOTE (R1-010): The literal values 'claude', 'codex' in the SQL CASE below
|
|
821
|
+
// are fixed at migration time and do NOT sync with TypeScript CLI_TOOL_IDS.
|
|
822
|
+
// Changes to CLI_TOOL_IDS will not retroactively affect already-migrated data.
|
|
823
|
+
// Migration tests cover all CLIToolType values to catch sync issues.
|
|
824
|
+
// Step 1: Add column
|
|
825
|
+
db.exec(`
|
|
826
|
+
ALTER TABLE worktrees ADD COLUMN selected_agents TEXT;
|
|
827
|
+
`);
|
|
828
|
+
// Step 2: Initialize existing data based on cli_tool_id
|
|
829
|
+
// - If cli_tool_id is 'claude' or 'codex' -> default ["claude","codex"]
|
|
830
|
+
// - Otherwise (e.g. 'gemini', 'vibe-local') -> [cli_tool_id, "claude"]
|
|
831
|
+
db.exec(`
|
|
832
|
+
UPDATE worktrees SET selected_agents =
|
|
833
|
+
CASE
|
|
834
|
+
WHEN cli_tool_id NOT IN ('claude', 'codex')
|
|
835
|
+
THEN json_array(cli_tool_id, 'claude')
|
|
836
|
+
ELSE '["claude","codex"]'
|
|
837
|
+
END;
|
|
838
|
+
`);
|
|
839
|
+
console.log('✓ Added selected_agents column to worktrees table');
|
|
840
|
+
console.log('✓ Initialized selected_agents based on cli_tool_id');
|
|
841
|
+
},
|
|
842
|
+
down: () => {
|
|
843
|
+
// selected_agents is a nullable TEXT column; dropping it requires table recreation
|
|
844
|
+
// which is disproportionate for a rollback. The column is harmless if unused.
|
|
845
|
+
console.log('No rollback for selected_agents column (SQLite limitation)');
|
|
846
|
+
}
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
version: 19,
|
|
850
|
+
name: 'add-vibe-local-model-column',
|
|
851
|
+
up: (db) => {
|
|
852
|
+
// Issue #368: Add vibe_local_model column for Ollama model selection
|
|
853
|
+
// NULL means use the default model (vibe-local decides)
|
|
854
|
+
db.exec(`
|
|
855
|
+
ALTER TABLE worktrees ADD COLUMN vibe_local_model TEXT DEFAULT NULL;
|
|
856
|
+
`);
|
|
857
|
+
console.log('✓ Added vibe_local_model column to worktrees table');
|
|
858
|
+
},
|
|
859
|
+
down: () => {
|
|
860
|
+
// vibe_local_model is a nullable TEXT column; harmless if unused
|
|
861
|
+
console.log('No rollback for vibe_local_model column (SQLite limitation)');
|
|
862
|
+
}
|
|
720
863
|
}
|
|
721
864
|
];
|
|
722
865
|
/**
|
|
@@ -904,7 +1047,7 @@ function validateSchema(db) {
|
|
|
904
1047
|
ORDER BY name
|
|
905
1048
|
`).all();
|
|
906
1049
|
const tableNames = tables.map(t => t.name);
|
|
907
|
-
const requiredTables = ['worktrees', 'chat_messages', 'session_states', 'schema_version', 'worktree_memos', 'external_apps', 'repositories', 'clone_jobs'];
|
|
1050
|
+
const requiredTables = ['worktrees', 'chat_messages', 'session_states', 'schema_version', 'worktree_memos', 'external_apps', 'repositories', 'clone_jobs', 'scheduled_executions', 'execution_logs'];
|
|
908
1051
|
const missingTables = requiredTables.filter(t => !tableNames.includes(t));
|
|
909
1052
|
if (missingTables.length > 0) {
|
|
910
1053
|
console.error('Missing required tables:', missingTables.join(', '));
|