commandmate 0.1.12 → 0.2.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.
Files changed (163) hide show
  1. package/.env.example +4 -9
  2. package/.next/BUILD_ID +1 -1
  3. package/.next/app-build-manifest.json +24 -24
  4. package/.next/app-path-routes-manifest.json +1 -1
  5. package/.next/build-manifest.json +7 -7
  6. package/.next/cache/.tsbuildinfo +1 -1
  7. package/.next/cache/config.json +3 -3
  8. package/.next/cache/webpack/client-production/0.pack +0 -0
  9. package/.next/cache/webpack/client-production/1.pack +0 -0
  10. package/.next/cache/webpack/client-production/2.pack +0 -0
  11. package/.next/cache/webpack/client-production/index.pack +0 -0
  12. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  13. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  14. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  15. package/.next/cache/webpack/server-production/0.pack +0 -0
  16. package/.next/cache/webpack/server-production/index.pack +0 -0
  17. package/.next/next-server.js.nft.json +1 -1
  18. package/.next/prerender-manifest.json +1 -1
  19. package/.next/react-loadable-manifest.json +7 -7
  20. package/.next/required-server-files.json +1 -1
  21. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/server/app/_not-found.html +1 -1
  23. package/.next/server/app/_not-found.rsc +2 -2
  24. package/.next/server/app/api/hooks/claude-done/route.js +1 -19
  25. package/.next/server/app/api/hooks/claude-done/route.js.nft.json +1 -1
  26. package/.next/server/app/api/repositories/clone/[jobId]/route.js +1 -1
  27. package/.next/server/app/api/repositories/clone/[jobId]/route.js.nft.json +1 -1
  28. package/.next/server/app/api/repositories/clone/route.js +1 -1
  29. package/.next/server/app/api/repositories/clone/route.js.nft.json +1 -1
  30. package/.next/server/app/api/repositories/excluded/route.js +36 -0
  31. package/.next/server/app/api/repositories/excluded/route.js.nft.json +1 -0
  32. package/.next/server/app/api/repositories/excluded.body +1 -0
  33. package/.next/server/app/api/repositories/excluded.meta +1 -0
  34. package/.next/server/app/api/repositories/restore/route.js +36 -0
  35. package/.next/server/app/api/repositories/restore/route.js.nft.json +1 -0
  36. package/.next/server/app/api/repositories/route.js +36 -1
  37. package/.next/server/app/api/repositories/route.js.nft.json +1 -1
  38. package/.next/server/app/api/repositories/scan/route.js +1 -1
  39. package/.next/server/app/api/repositories/sync/route.js +36 -1
  40. package/.next/server/app/api/slash-commands/route.js +1 -1
  41. package/.next/server/app/api/slash-commands.body +1 -1
  42. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js +1 -1
  43. package/.next/server/app/api/worktrees/[id]/auto-yes/route.js.nft.json +1 -1
  44. package/.next/server/app/api/worktrees/[id]/capture/route.js +2 -2
  45. package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
  46. package/.next/server/app/api/worktrees/[id]/current-output/route.js.nft.json +1 -1
  47. package/.next/server/app/api/worktrees/[id]/interrupt/route.js +1 -1
  48. package/.next/server/app/api/worktrees/[id]/interrupt/route.js.nft.json +1 -1
  49. package/.next/server/app/api/worktrees/[id]/kill-session/route.js +1 -1
  50. package/.next/server/app/api/worktrees/[id]/kill-session/route.js.nft.json +1 -1
  51. package/.next/server/app/api/worktrees/[id]/logs/[filename]/route.js +1 -1
  52. package/.next/server/app/api/worktrees/[id]/logs/route.js +1 -1
  53. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js +1 -1
  54. package/.next/server/app/api/worktrees/[id]/prompt-response/route.js.nft.json +1 -1
  55. package/.next/server/app/api/worktrees/[id]/respond/route.js +1 -1
  56. package/.next/server/app/api/worktrees/[id]/respond/route.js.nft.json +1 -1
  57. package/.next/server/app/api/worktrees/[id]/route.js +1 -1
  58. package/.next/server/app/api/worktrees/[id]/route.js.nft.json +1 -1
  59. package/.next/server/app/api/worktrees/[id]/search/route.js +1 -1
  60. package/.next/server/app/api/worktrees/[id]/send/route.js +1 -1
  61. package/.next/server/app/api/worktrees/[id]/send/route.js.nft.json +1 -1
  62. package/.next/server/app/api/worktrees/[id]/slash-commands/route.js +1 -1
  63. package/.next/server/app/api/worktrees/[id]/start-polling/route.js +1 -1
  64. package/.next/server/app/api/worktrees/[id]/start-polling/route.js.nft.json +1 -1
  65. package/.next/server/app/api/worktrees/[id]/terminal/route.js +1 -1
  66. package/.next/server/app/api/worktrees/route.js +1 -1
  67. package/.next/server/app/api/worktrees/route.js.nft.json +1 -1
  68. package/.next/server/app/index.html +2 -2
  69. package/.next/server/app/index.rsc +3 -3
  70. package/.next/server/app/page.js +7 -7
  71. package/.next/server/app/page.js.nft.json +1 -1
  72. package/.next/server/app/page_client-reference-manifest.js +1 -1
  73. package/.next/server/app/proxy/[...path]/route.js +2 -2
  74. package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/worktrees/[id]/page.js +4 -4
  76. package/.next/server/app/worktrees/[id]/page.js.nft.json +1 -1
  77. package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
  78. package/.next/server/app/worktrees/[id]/simple-terminal/page_client-reference-manifest.js +1 -1
  79. package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
  80. package/.next/server/app-paths-manifest.json +10 -8
  81. package/.next/server/chunks/5488.js +36 -0
  82. package/.next/server/chunks/6550.js +1 -1
  83. package/.next/server/chunks/7425.js +53 -50
  84. package/.next/server/chunks/7536.js +1 -0
  85. package/.next/server/chunks/8174.js +23 -0
  86. package/.next/server/chunks/9367.js +19 -0
  87. package/.next/server/middleware-build-manifest.js +1 -1
  88. package/.next/server/middleware-manifest.json +2 -28
  89. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  90. package/.next/server/pages/404.html +1 -1
  91. package/.next/server/pages/500.html +1 -1
  92. package/.next/server/server-reference-manifest.json +1 -1
  93. package/.next/static/chunks/4327.740cc7fe2d0b5049.js +60 -0
  94. package/.next/static/chunks/4343-ebe884a2a80eb033.js +1 -0
  95. package/.next/static/chunks/6568-38a33aa67d82e12b.js +1 -0
  96. package/.next/static/chunks/816-c254f4e2406e696a.js +1 -0
  97. package/.next/static/chunks/app/layout-4804cfba519283cf.js +1 -0
  98. package/.next/static/chunks/app/page-3926224c4cdf315b.js +1 -0
  99. package/.next/static/chunks/app/worktrees/[id]/page-8bd88bdc29607413.js +1 -0
  100. package/.next/static/chunks/main-b6d727aa9248d4f2.js +1 -0
  101. package/.next/static/chunks/{webpack-3fc79fab9bb738d7.js → webpack-4f85dcef6279c6ee.js} +1 -1
  102. package/.next/static/css/28be35e4727ae7ef.css +3 -0
  103. package/.next/trace +5 -5
  104. package/.next/types/app/api/repositories/excluded/route.ts +343 -0
  105. package/.next/types/app/api/repositories/restore/route.ts +343 -0
  106. package/README.md +2 -2
  107. package/dist/cli/commands/init.d.ts.map +1 -1
  108. package/dist/cli/commands/init.js +2 -13
  109. package/dist/cli/commands/start.d.ts.map +1 -1
  110. package/dist/cli/commands/start.js +3 -7
  111. package/dist/cli/config/security-messages.d.ts +11 -0
  112. package/dist/cli/config/security-messages.d.ts.map +1 -0
  113. package/dist/cli/config/security-messages.js +29 -0
  114. package/dist/cli/types/index.d.ts +0 -1
  115. package/dist/cli/types/index.d.ts.map +1 -1
  116. package/dist/cli/utils/daemon.d.ts.map +1 -1
  117. package/dist/cli/utils/daemon.js +3 -7
  118. package/dist/cli/utils/env-setup.d.ts +0 -4
  119. package/dist/cli/utils/env-setup.d.ts.map +1 -1
  120. package/dist/cli/utils/env-setup.js +0 -14
  121. package/dist/cli/utils/security-logger.d.ts.map +1 -1
  122. package/dist/cli/utils/security-logger.js +1 -2
  123. package/dist/server/server.js +25 -2
  124. package/dist/server/src/lib/auto-yes-manager.js +100 -11
  125. package/dist/server/src/lib/claude-poller.js +341 -0
  126. package/dist/server/src/lib/claude-session.js +48 -19
  127. package/dist/server/src/lib/cli-patterns.js +69 -6
  128. package/dist/server/src/lib/cli-tools/base.js +7 -1
  129. package/dist/server/src/lib/cli-tools/codex.js +14 -2
  130. package/dist/server/src/lib/cli-tools/manager.js +27 -0
  131. package/dist/server/src/lib/cli-tools/types.js +7 -0
  132. package/dist/server/src/lib/cli-tools/validation.js +41 -0
  133. package/dist/server/src/lib/db-repository.js +482 -0
  134. package/dist/server/src/lib/db.js +23 -0
  135. package/dist/server/src/lib/env.js +0 -17
  136. package/dist/server/src/lib/logger.js +0 -4
  137. package/dist/server/src/lib/prompt-detector.js +297 -109
  138. package/dist/server/src/lib/response-poller.js +73 -27
  139. package/dist/server/src/lib/tmux.js +48 -0
  140. package/dist/server/src/lib/ws-server.js +12 -1
  141. package/dist/server/src/types/sidebar.js +16 -31
  142. package/dist/server/src/types/slash-commands.js +2 -0
  143. package/package.json +1 -1
  144. package/.next/server/chunks/1318.js +0 -29
  145. package/.next/server/chunks/2597.js +0 -1
  146. package/.next/server/chunks/2648.js +0 -1
  147. package/.next/server/chunks/9703.js +0 -31
  148. package/.next/server/chunks/9723.js +0 -19
  149. package/.next/server/edge-runtime-webpack.js +0 -2
  150. package/.next/server/edge-runtime-webpack.js.map +0 -1
  151. package/.next/server/src/middleware.js +0 -14
  152. package/.next/server/src/middleware.js.map +0 -1
  153. package/.next/static/chunks/2853-d11a80b03c9a1640.js +0 -1
  154. package/.next/static/chunks/4327.3b84aa049900fdeb.js +0 -60
  155. package/.next/static/chunks/816-7e340dad784be28c.js +0 -1
  156. package/.next/static/chunks/9365-733d8c05712d2888.js +0 -1
  157. package/.next/static/chunks/app/layout-37e55f11dcc8b1bf.js +0 -1
  158. package/.next/static/chunks/app/page-fe35d61f14b90a51.js +0 -1
  159. package/.next/static/chunks/app/worktrees/[id]/page-58fcf2e63c056743.js +0 -1
  160. package/.next/static/chunks/main-a960f4a5e1a2f598.js +0 -1
  161. package/.next/static/css/376b339640084689.css +0 -3
  162. /package/.next/static/{564GHwluX5xIv9qpqLJV2 → oUD-A998xeBoez6zsrTH3}/_buildManifest.js +0 -0
  163. /package/.next/static/{564GHwluX5xIv9qpqLJV2 → oUD-A998xeBoez6zsrTH3}/_ssgManifest.js +0 -0
@@ -8,6 +8,8 @@ exports.CLIToolManager = void 0;
8
8
  const claude_1 = require("./claude");
9
9
  const codex_1 = require("./codex");
10
10
  const gemini_1 = require("./gemini");
11
+ const response_poller_1 = require("../response-poller");
12
+ const claude_poller_1 = require("../claude-poller");
11
13
  /**
12
14
  * CLI Tool Manager (Singleton)
13
15
  * Provides centralized access to all CLI tools
@@ -139,5 +141,30 @@ class CLIToolManager {
139
141
  const allInfo = await this.getAllToolsInfo();
140
142
  return allInfo.filter(info => info.installed);
141
143
  }
144
+ /**
145
+ * Stop pollers for a specific worktree and CLI tool
146
+ * T2.4: Abstraction for poller stopping (MF1-001 DIP compliance)
147
+ *
148
+ * This method abstracts the poller stopping logic so API layer
149
+ * doesn't need to know about specific poller implementations.
150
+ *
151
+ * @param worktreeId - Worktree ID
152
+ * @param cliToolId - CLI tool ID
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * const manager = CLIToolManager.getInstance();
157
+ * manager.stopPollers('my-worktree', 'claude');
158
+ * ```
159
+ */
160
+ stopPollers(worktreeId, cliToolId) {
161
+ // Stop response-poller for all tools
162
+ (0, response_poller_1.stopPolling)(worktreeId, cliToolId);
163
+ // claude-poller is Claude-specific
164
+ if (cliToolId === 'claude') {
165
+ (0, claude_poller_1.stopPolling)(worktreeId);
166
+ }
167
+ // Future: Add other tool-specific pollers here if needed
168
+ }
142
169
  }
143
170
  exports.CLIToolManager = CLIToolManager;
@@ -3,3 +3,10 @@
3
3
  * Type definitions and interfaces for CLI tools
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CLI_TOOL_IDS = void 0;
7
+ /**
8
+ * CLI Tool IDs constant array
9
+ * T2.1: Single source of truth for CLI tool IDs
10
+ * CLIToolType is derived from this constant (DRY principle)
11
+ */
12
+ exports.CLI_TOOL_IDS = ['claude', 'codex', 'gemini'];
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * Session name validation module
4
+ * Issue #4: T2.2 - Security validation for session names (MF4-001)
5
+ *
6
+ * This module provides validation functions to prevent command injection
7
+ * attacks through session names used in tmux commands.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.SESSION_NAME_PATTERN = void 0;
11
+ exports.validateSessionName = validateSessionName;
12
+ /**
13
+ * Session name pattern
14
+ * Only allows alphanumeric characters, underscores, and hyphens
15
+ * This prevents command injection through shell special characters
16
+ *
17
+ * Pattern breakdown:
18
+ * - ^ : Start of string
19
+ * - [a-zA-Z0-9_-] : Allowed characters (alphanumeric, underscore, hyphen)
20
+ * - + : One or more characters (empty strings not allowed)
21
+ * - $ : End of string
22
+ */
23
+ exports.SESSION_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
24
+ /**
25
+ * Validate session name format
26
+ * Throws an error if the session name contains invalid characters
27
+ *
28
+ * @param sessionName - Session name to validate
29
+ * @throws Error if session name is invalid
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * validateSessionName('mcbd-claude-test'); // OK
34
+ * validateSessionName('test;rm -rf'); // Throws Error
35
+ * ```
36
+ */
37
+ function validateSessionName(sessionName) {
38
+ if (!exports.SESSION_NAME_PATTERN.test(sessionName)) {
39
+ throw new Error(`Invalid session name format: ${sessionName}`);
40
+ }
41
+ }
@@ -0,0 +1,482 @@
1
+ "use strict";
2
+ /**
3
+ * Repository and Clone Job Database Operations
4
+ * Issue #71: Clone URL registration feature
5
+ * Issue #190: Repository exclusion on sync
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.MAX_DISABLED_REPOSITORIES = void 0;
12
+ exports.createRepository = createRepository;
13
+ exports.getRepositoryByNormalizedUrl = getRepositoryByNormalizedUrl;
14
+ exports.getRepositoryById = getRepositoryById;
15
+ exports.getRepositoryByPath = getRepositoryByPath;
16
+ exports.updateRepository = updateRepository;
17
+ exports.getAllRepositories = getAllRepositories;
18
+ exports.resolveRepositoryPath = resolveRepositoryPath;
19
+ exports.validateRepositoryPath = validateRepositoryPath;
20
+ exports.ensureEnvRepositoriesRegistered = ensureEnvRepositoriesRegistered;
21
+ exports.filterExcludedPaths = filterExcludedPaths;
22
+ exports.registerAndFilterRepositories = registerAndFilterRepositories;
23
+ exports.disableRepository = disableRepository;
24
+ exports.getExcludedRepositoryPaths = getExcludedRepositoryPaths;
25
+ exports.getExcludedRepositories = getExcludedRepositories;
26
+ exports.restoreRepository = restoreRepository;
27
+ exports.createCloneJob = createCloneJob;
28
+ exports.getCloneJob = getCloneJob;
29
+ exports.updateCloneJob = updateCloneJob;
30
+ exports.getActiveCloneJobByUrl = getActiveCloneJobByUrl;
31
+ exports.getCloneJobsByStatus = getCloneJobsByStatus;
32
+ const crypto_1 = require("crypto");
33
+ const path_1 = __importDefault(require("path"));
34
+ const system_directories_1 = require("../config/system-directories");
35
+ /**
36
+ * Map repository row to Repository model
37
+ */
38
+ function mapRepositoryRow(row) {
39
+ return {
40
+ id: row.id,
41
+ name: row.name,
42
+ path: row.path,
43
+ enabled: row.enabled === 1,
44
+ cloneUrl: row.clone_url || undefined,
45
+ normalizedCloneUrl: row.normalized_clone_url || undefined,
46
+ cloneSource: row.clone_source,
47
+ isEnvManaged: row.is_env_managed === 1,
48
+ createdAt: new Date(row.created_at),
49
+ updatedAt: new Date(row.updated_at),
50
+ };
51
+ }
52
+ /**
53
+ * Map clone job row to CloneJobDB model
54
+ */
55
+ function mapCloneJobRow(row) {
56
+ return {
57
+ id: row.id,
58
+ cloneUrl: row.clone_url,
59
+ normalizedCloneUrl: row.normalized_clone_url,
60
+ targetPath: row.target_path,
61
+ repositoryId: row.repository_id || undefined,
62
+ status: row.status,
63
+ pid: row.pid || undefined,
64
+ progress: row.progress,
65
+ errorCategory: row.error_category || undefined,
66
+ errorCode: row.error_code || undefined,
67
+ errorMessage: row.error_message || undefined,
68
+ startedAt: row.started_at ? new Date(row.started_at) : undefined,
69
+ completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
70
+ createdAt: new Date(row.created_at),
71
+ };
72
+ }
73
+ // ============================================================
74
+ // Repository Operations
75
+ // ============================================================
76
+ /**
77
+ * Create a new repository
78
+ */
79
+ function createRepository(db, data) {
80
+ const id = (0, crypto_1.randomUUID)();
81
+ const now = Date.now();
82
+ const stmt = db.prepare(`
83
+ INSERT INTO repositories (
84
+ id, name, path, enabled, clone_url, normalized_clone_url,
85
+ clone_source, is_env_managed, created_at, updated_at
86
+ )
87
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
88
+ `);
89
+ stmt.run(id, data.name, data.path, data.enabled !== false ? 1 : 0, data.cloneUrl || null, data.normalizedCloneUrl || null, data.cloneSource, data.isEnvManaged ? 1 : 0, now, now);
90
+ return {
91
+ id,
92
+ name: data.name,
93
+ path: data.path,
94
+ enabled: data.enabled !== false,
95
+ cloneUrl: data.cloneUrl,
96
+ normalizedCloneUrl: data.normalizedCloneUrl,
97
+ cloneSource: data.cloneSource,
98
+ isEnvManaged: data.isEnvManaged || false,
99
+ createdAt: new Date(now),
100
+ updatedAt: new Date(now),
101
+ };
102
+ }
103
+ /**
104
+ * Get repository by normalized clone URL
105
+ */
106
+ function getRepositoryByNormalizedUrl(db, normalizedCloneUrl) {
107
+ const stmt = db.prepare(`
108
+ SELECT * FROM repositories
109
+ WHERE normalized_clone_url = ?
110
+ `);
111
+ const row = stmt.get(normalizedCloneUrl);
112
+ return row ? mapRepositoryRow(row) : null;
113
+ }
114
+ /**
115
+ * Get repository by ID
116
+ */
117
+ function getRepositoryById(db, id) {
118
+ const stmt = db.prepare(`
119
+ SELECT * FROM repositories
120
+ WHERE id = ?
121
+ `);
122
+ const row = stmt.get(id);
123
+ return row ? mapRepositoryRow(row) : null;
124
+ }
125
+ /**
126
+ * Get repository by path
127
+ */
128
+ function getRepositoryByPath(db, path) {
129
+ const stmt = db.prepare(`
130
+ SELECT * FROM repositories
131
+ WHERE path = ?
132
+ `);
133
+ const row = stmt.get(path);
134
+ return row ? mapRepositoryRow(row) : null;
135
+ }
136
+ /**
137
+ * Update repository
138
+ */
139
+ function updateRepository(db, id, updates) {
140
+ const now = Date.now();
141
+ const assignments = ['updated_at = ?'];
142
+ const params = [now];
143
+ if (updates.name !== undefined) {
144
+ assignments.push('name = ?');
145
+ params.push(updates.name);
146
+ }
147
+ if (updates.enabled !== undefined) {
148
+ assignments.push('enabled = ?');
149
+ params.push(updates.enabled ? 1 : 0);
150
+ }
151
+ if (updates.cloneUrl !== undefined) {
152
+ assignments.push('clone_url = ?');
153
+ params.push(updates.cloneUrl || null);
154
+ }
155
+ if (updates.normalizedCloneUrl !== undefined) {
156
+ assignments.push('normalized_clone_url = ?');
157
+ params.push(updates.normalizedCloneUrl || null);
158
+ }
159
+ params.push(id);
160
+ const stmt = db.prepare(`
161
+ UPDATE repositories
162
+ SET ${assignments.join(', ')}
163
+ WHERE id = ?
164
+ `);
165
+ stmt.run(...params);
166
+ }
167
+ /**
168
+ * Get all repositories
169
+ */
170
+ function getAllRepositories(db) {
171
+ const stmt = db.prepare(`
172
+ SELECT * FROM repositories
173
+ ORDER BY name ASC
174
+ `);
175
+ const rows = stmt.all();
176
+ return rows.map(mapRepositoryRow);
177
+ }
178
+ // ============================================================
179
+ // Repository Exclusion Operations (Issue #190)
180
+ // ============================================================
181
+ /**
182
+ * Maximum number of disabled repositories allowed.
183
+ * Prevents unlimited record accumulation from malicious or buggy DELETE requests.
184
+ * SEC-SF-004
185
+ */
186
+ exports.MAX_DISABLED_REPOSITORIES = 1000;
187
+ /**
188
+ * Resolve and normalize a repository path.
189
+ * All path normalization is centralized here to prevent inconsistencies.
190
+ *
191
+ * NOTE: path.resolve() removes trailing slashes and resolves relative paths
192
+ * but does NOT resolve symlinks. For symlink resolution, use fs.realpathSync().
193
+ * See design policy Section 7 for the symlink handling policy.
194
+ *
195
+ * SF-001: DRY - centralized path normalization
196
+ */
197
+ function resolveRepositoryPath(repoPath) {
198
+ return path_1.default.resolve(repoPath);
199
+ }
200
+ /**
201
+ * Validate and resolve a repository path for API requests.
202
+ * Centralizes all validation checks to avoid duplication across route handlers.
203
+ *
204
+ * Checks performed:
205
+ * 1. Presence and type check (must be non-empty string)
206
+ * 2. Null byte check (path traversal prevention, SEC-MF-001)
207
+ * 3. System directory check (prevents operations on /etc, /usr, etc.)
208
+ *
209
+ * DRY: Used by DELETE /api/repositories and PUT /api/repositories/restore
210
+ */
211
+ function validateRepositoryPath(repositoryPath) {
212
+ if (!repositoryPath || typeof repositoryPath !== 'string') {
213
+ return { valid: false, error: 'repositoryPath is required' };
214
+ }
215
+ if (repositoryPath.includes('\0')) {
216
+ return { valid: false, error: 'Invalid repository path' };
217
+ }
218
+ const resolvedPath = resolveRepositoryPath(repositoryPath);
219
+ if ((0, system_directories_1.isSystemDirectory)(resolvedPath)) {
220
+ return { valid: false, error: 'Invalid repository path' };
221
+ }
222
+ return { valid: true, resolvedPath };
223
+ }
224
+ /**
225
+ * Register environment variable repositories to the repositories table.
226
+ * Idempotent: already registered repositories are skipped (regardless of enabled status).
227
+ *
228
+ * NOTE (MF-C01): createRepository() treats enabled as follows:
229
+ * - true -> SQLite 1 (enabled)
230
+ * - false -> SQLite 0 (disabled)
231
+ * - undefined -> SQLite 1 (enabled, due to `data.enabled !== false ? 1 : 0` logic)
232
+ * We explicitly pass enabled: true to avoid relying on the implicit default.
233
+ *
234
+ * MF-001: SRP - registration logic separated from sync route
235
+ */
236
+ function ensureEnvRepositoriesRegistered(db, repositoryPaths) {
237
+ for (const repoPath of repositoryPaths) {
238
+ const resolvedPath = resolveRepositoryPath(repoPath);
239
+ const existing = getRepositoryByPath(db, resolvedPath);
240
+ if (!existing) {
241
+ createRepository(db, {
242
+ name: path_1.default.basename(resolvedPath),
243
+ path: resolvedPath,
244
+ cloneSource: 'local',
245
+ isEnvManaged: true,
246
+ enabled: true, // Explicit: do not rely on undefined -> 1 default
247
+ });
248
+ }
249
+ }
250
+ }
251
+ /**
252
+ * Filter out excluded repository paths (enabled=0).
253
+ * Exclusion logic is encapsulated here, so changes to exclusion criteria
254
+ * (e.g., pattern-based exclusion, temporary exclusion) only affect this function.
255
+ *
256
+ * @requires ensureEnvRepositoriesRegistered() must be called before this function
257
+ * to ensure all paths exist in the repositories table.
258
+ * Without prior registration, unregistered paths will not be filtered correctly.
259
+ *
260
+ * NOTE (SEC-SF-002): Array.includes() performs case-sensitive string comparison.
261
+ * On macOS (case-insensitive filesystem), paths with different casing would not match.
262
+ * resolveRepositoryPath() normalization on both sides mitigates most cases.
263
+ * On Linux (case-sensitive filesystem), the behavior is consistent.
264
+ *
265
+ * @param db - Database instance
266
+ * @param repositoryPaths - Array of repository paths to filter
267
+ * @returns Filtered array excluding disabled repositories
268
+ *
269
+ * SF-003: OCP - exclusion logic encapsulated
270
+ */
271
+ function filterExcludedPaths(db, repositoryPaths) {
272
+ const excludedPaths = getExcludedRepositoryPaths(db);
273
+ return repositoryPaths.filter(p => !excludedPaths.includes(resolveRepositoryPath(p)));
274
+ }
275
+ /**
276
+ * Register environment variable repositories and filter out excluded ones.
277
+ * Encapsulates the ordering constraint: registration MUST happen before filtering.
278
+ *
279
+ * This function combines ensureEnvRepositoriesRegistered() and filterExcludedPaths()
280
+ * into a single atomic operation to prevent callers from accidentally reversing the order.
281
+ *
282
+ * DRY: Used by server.ts initializeWorktrees() and POST /api/repositories/sync
283
+ *
284
+ * @param db - Database instance
285
+ * @param repositoryPaths - Array of repository paths from environment variables
286
+ * @returns ExclusionSummary with filtered paths, excluded paths, and count
287
+ */
288
+ function registerAndFilterRepositories(db, repositoryPaths) {
289
+ // Step 1: Register (must be before filter - see design policy Section 4)
290
+ ensureEnvRepositoriesRegistered(db, repositoryPaths);
291
+ // Step 2: Filter
292
+ const filteredPaths = filterExcludedPaths(db, repositoryPaths);
293
+ const excludedPaths = repositoryPaths.filter(p => !filteredPaths.includes(p));
294
+ return {
295
+ filteredPaths,
296
+ excludedPaths,
297
+ excludedCount: excludedPaths.length,
298
+ };
299
+ }
300
+ /**
301
+ * Disable a repository by setting enabled=0.
302
+ * If the repository is not registered, create it with enabled=0.
303
+ * All internal logic (lookup + update/create) is encapsulated.
304
+ *
305
+ * NOTE (MF-C01): Explicitly passes enabled: false to createRepository().
306
+ * The internal mapping `data.enabled !== false ? 1 : 0` will correctly
307
+ * store 0 in SQLite. Do NOT pass undefined for enabled.
308
+ *
309
+ * NOTE (SEC-SF-004): When creating a new record, checks the count of
310
+ * disabled repositories against MAX_DISABLED_REPOSITORIES to prevent
311
+ * unlimited record accumulation from malicious or buggy DELETE requests.
312
+ *
313
+ * SF-002: SRP - disable logic encapsulated
314
+ */
315
+ function disableRepository(db, repositoryPath) {
316
+ const resolvedPath = resolveRepositoryPath(repositoryPath);
317
+ const repo = getRepositoryByPath(db, resolvedPath);
318
+ if (repo) {
319
+ updateRepository(db, repo.id, { enabled: false });
320
+ }
321
+ else {
322
+ // SEC-SF-004: Check disabled repository count limit before creating new record
323
+ const disabledCount = db.prepare('SELECT COUNT(*) as count FROM repositories WHERE enabled = 0').get();
324
+ if (disabledCount.count >= exports.MAX_DISABLED_REPOSITORIES) {
325
+ throw new Error('Disabled repository limit exceeded');
326
+ }
327
+ createRepository(db, {
328
+ name: path_1.default.basename(resolvedPath),
329
+ path: resolvedPath,
330
+ cloneSource: 'local',
331
+ isEnvManaged: false,
332
+ enabled: false, // Explicit: do not rely on undefined -> 1 default
333
+ });
334
+ }
335
+ }
336
+ /**
337
+ * Get paths of excluded (enabled=0) repositories
338
+ */
339
+ function getExcludedRepositoryPaths(db) {
340
+ const stmt = db.prepare('SELECT path FROM repositories WHERE enabled = 0');
341
+ const rows = stmt.all();
342
+ return rows.map(r => r.path);
343
+ }
344
+ /**
345
+ * Get excluded repositories with full details
346
+ */
347
+ function getExcludedRepositories(db) {
348
+ const stmt = db.prepare('SELECT * FROM repositories WHERE enabled = 0 ORDER BY name ASC');
349
+ const rows = stmt.all();
350
+ return rows.map(mapRepositoryRow);
351
+ }
352
+ /**
353
+ * Restore an excluded repository by setting enabled=1
354
+ *
355
+ * @returns Restored Repository object, or null if not found
356
+ */
357
+ function restoreRepository(db, repoPath) {
358
+ const resolvedPath = resolveRepositoryPath(repoPath);
359
+ const repo = getRepositoryByPath(db, resolvedPath);
360
+ if (!repo)
361
+ return null;
362
+ updateRepository(db, repo.id, { enabled: true });
363
+ return { ...repo, enabled: true };
364
+ }
365
+ // ============================================================
366
+ // Clone Job Operations
367
+ // ============================================================
368
+ /**
369
+ * Create a new clone job
370
+ */
371
+ function createCloneJob(db, data) {
372
+ const id = (0, crypto_1.randomUUID)();
373
+ const now = Date.now();
374
+ const stmt = db.prepare(`
375
+ INSERT INTO clone_jobs (
376
+ id, clone_url, normalized_clone_url, target_path,
377
+ status, progress, created_at
378
+ )
379
+ VALUES (?, ?, ?, ?, 'pending', 0, ?)
380
+ `);
381
+ stmt.run(id, data.cloneUrl, data.normalizedCloneUrl, data.targetPath, now);
382
+ return {
383
+ id,
384
+ cloneUrl: data.cloneUrl,
385
+ normalizedCloneUrl: data.normalizedCloneUrl,
386
+ targetPath: data.targetPath,
387
+ status: 'pending',
388
+ progress: 0,
389
+ createdAt: new Date(now),
390
+ };
391
+ }
392
+ /**
393
+ * Get clone job by ID
394
+ */
395
+ function getCloneJob(db, id) {
396
+ const stmt = db.prepare(`
397
+ SELECT * FROM clone_jobs
398
+ WHERE id = ?
399
+ `);
400
+ const row = stmt.get(id);
401
+ return row ? mapCloneJobRow(row) : null;
402
+ }
403
+ /**
404
+ * Update clone job
405
+ */
406
+ function updateCloneJob(db, id, updates) {
407
+ const assignments = [];
408
+ const params = [];
409
+ if (updates.status !== undefined) {
410
+ assignments.push('status = ?');
411
+ params.push(updates.status);
412
+ }
413
+ if (updates.pid !== undefined) {
414
+ assignments.push('pid = ?');
415
+ params.push(updates.pid);
416
+ }
417
+ if (updates.progress !== undefined) {
418
+ assignments.push('progress = ?');
419
+ params.push(updates.progress);
420
+ }
421
+ if (updates.repositoryId !== undefined) {
422
+ assignments.push('repository_id = ?');
423
+ params.push(updates.repositoryId);
424
+ }
425
+ if (updates.errorCategory !== undefined) {
426
+ assignments.push('error_category = ?');
427
+ params.push(updates.errorCategory);
428
+ }
429
+ if (updates.errorCode !== undefined) {
430
+ assignments.push('error_code = ?');
431
+ params.push(updates.errorCode);
432
+ }
433
+ if (updates.errorMessage !== undefined) {
434
+ assignments.push('error_message = ?');
435
+ params.push(updates.errorMessage);
436
+ }
437
+ if (updates.startedAt !== undefined) {
438
+ assignments.push('started_at = ?');
439
+ params.push(updates.startedAt.getTime());
440
+ }
441
+ if (updates.completedAt !== undefined) {
442
+ assignments.push('completed_at = ?');
443
+ params.push(updates.completedAt.getTime());
444
+ }
445
+ if (assignments.length === 0) {
446
+ return;
447
+ }
448
+ params.push(id);
449
+ const stmt = db.prepare(`
450
+ UPDATE clone_jobs
451
+ SET ${assignments.join(', ')}
452
+ WHERE id = ?
453
+ `);
454
+ stmt.run(...params);
455
+ }
456
+ /**
457
+ * Get active clone job by normalized URL
458
+ * Active jobs are those with status 'pending' or 'running'
459
+ */
460
+ function getActiveCloneJobByUrl(db, normalizedCloneUrl) {
461
+ const stmt = db.prepare(`
462
+ SELECT * FROM clone_jobs
463
+ WHERE normalized_clone_url = ?
464
+ AND status IN ('pending', 'running')
465
+ ORDER BY created_at DESC
466
+ LIMIT 1
467
+ `);
468
+ const row = stmt.get(normalizedCloneUrl);
469
+ return row ? mapCloneJobRow(row) : null;
470
+ }
471
+ /**
472
+ * Get clone jobs by status
473
+ */
474
+ function getCloneJobsByStatus(db, status) {
475
+ const stmt = db.prepare(`
476
+ SELECT * FROM clone_jobs
477
+ WHERE status = ?
478
+ ORDER BY created_at DESC
479
+ `);
480
+ const rows = stmt.all(status);
481
+ return rows.map(mapCloneJobRow);
482
+ }
@@ -19,6 +19,7 @@ exports.getMessages = getMessages;
19
19
  exports.getLastUserMessage = getLastUserMessage;
20
20
  exports.getLastMessage = getLastMessage;
21
21
  exports.deleteAllMessages = deleteAllMessages;
22
+ exports.deleteMessagesByCliTool = deleteMessagesByCliTool;
22
23
  exports.getSessionState = getSessionState;
23
24
  exports.updateSessionState = updateSessionState;
24
25
  exports.setInProgressMessageId = setInProgressMessageId;
@@ -458,6 +459,28 @@ function deleteAllMessages(db, worktreeId) {
458
459
  stmt.run(worktreeId);
459
460
  console.log(`[deleteAllMessages] Deleted all messages for worktree: ${worktreeId}`);
460
461
  }
462
+ /**
463
+ * Delete messages for a specific CLI tool in a worktree
464
+ * Issue #4: T4.2 - Individual CLI tool session termination (MF3-001)
465
+ *
466
+ * Used when killing only a specific CLI tool's session to clear its message history
467
+ * while preserving messages from other CLI tools.
468
+ * Note: Log files are preserved for historical reference
469
+ *
470
+ * @param db - Database instance
471
+ * @param worktreeId - Worktree ID
472
+ * @param cliTool - CLI tool ID to delete messages for
473
+ * @returns Number of deleted messages
474
+ */
475
+ function deleteMessagesByCliTool(db, worktreeId, cliTool) {
476
+ const stmt = db.prepare(`
477
+ DELETE FROM chat_messages
478
+ WHERE worktree_id = ? AND cli_tool_id = ?
479
+ `);
480
+ const result = stmt.run(worktreeId, cliTool);
481
+ console.log(`[deleteMessagesByCliTool] Deleted ${result.changes} messages for worktree: ${worktreeId}, cliTool: ${cliTool}`);
482
+ return result.changes;
483
+ }
461
484
  /**
462
485
  * Get session state for a worktree
463
486
  */
@@ -22,7 +22,6 @@ exports.getDatabasePathWithDeprecationWarning = getDatabasePathWithDeprecationWa
22
22
  exports.getLogConfig = getLogConfig;
23
23
  exports.getEnv = getEnv;
24
24
  exports.validateEnv = validateEnv;
25
- exports.isAuthRequired = isAuthRequired;
26
25
  const path_1 = __importDefault(require("path"));
27
26
  const db_path_resolver_1 = require("./db-path-resolver");
28
27
  // ============================================================
@@ -37,7 +36,6 @@ exports.ENV_MAPPING = {
37
36
  CM_ROOT_DIR: 'MCBD_ROOT_DIR',
38
37
  CM_PORT: 'MCBD_PORT',
39
38
  CM_BIND: 'MCBD_BIND',
40
- CM_AUTH_TOKEN: 'MCBD_AUTH_TOKEN',
41
39
  CM_LOG_LEVEL: 'MCBD_LOG_LEVEL',
42
40
  CM_LOG_FORMAT: 'MCBD_LOG_FORMAT',
43
41
  CM_LOG_DIR: 'MCBD_LOG_DIR',
@@ -160,7 +158,6 @@ function getEnv() {
160
158
  const rootDir = getEnvByKey('CM_ROOT_DIR') || process.cwd();
161
159
  const port = parseInt(getEnvByKey('CM_PORT') || '3000', 10);
162
160
  const bind = getEnvByKey('CM_BIND') || '127.0.0.1';
163
- const authToken = getEnvByKey('CM_AUTH_TOKEN');
164
161
  // Issue #135: DB path resolution with proper fallback chain
165
162
  // Priority: CM_DB_PATH > DATABASE_PATH (deprecated) > getDefaultDbPath()
166
163
  const databasePath = getEnvByKey('CM_DB_PATH')
@@ -176,10 +173,6 @@ function getEnv() {
176
173
  if (bind !== '127.0.0.1' && bind !== '0.0.0.0' && bind !== 'localhost') {
177
174
  throw new Error(`Invalid CM_BIND: ${bind}. Must be '127.0.0.1', '0.0.0.0', or 'localhost'.`);
178
175
  }
179
- // Require auth token for public binding
180
- if (bind === '0.0.0.0' && !authToken) {
181
- throw new Error('CM_AUTH_TOKEN (or MCBD_AUTH_TOKEN) is required when CM_BIND=0.0.0.0');
182
- }
183
176
  // Issue #135: Validate DB path for security (SEC-001)
184
177
  let validatedDbPath;
185
178
  try {
@@ -195,7 +188,6 @@ function getEnv() {
195
188
  CM_ROOT_DIR: path_1.default.resolve(rootDir),
196
189
  CM_PORT: port,
197
190
  CM_BIND: bind,
198
- CM_AUTH_TOKEN: authToken,
199
191
  CM_DB_PATH: validatedDbPath,
200
192
  };
201
193
  }
@@ -217,12 +209,3 @@ function validateEnv() {
217
209
  return { valid: false, errors };
218
210
  }
219
211
  }
220
- /**
221
- * Check if authentication is required based on bind address
222
- *
223
- * @returns True if authentication is required
224
- */
225
- function isAuthRequired() {
226
- const env = getEnv();
227
- return env.CM_BIND === '0.0.0.0';
228
- }
@@ -35,10 +35,6 @@ const SENSITIVE_PATTERNS = [
35
35
  { pattern: /(password|passwd|pwd)[=:]\s*\S+/gi, replacement: '$1=[REDACTED]' },
36
36
  // Token/secret related
37
37
  { pattern: /(token|secret|api_key|apikey|auth)[=:]\s*\S+/gi, replacement: '$1=[REDACTED]' },
38
- // CM_AUTH_TOKEN (new name - Issue #76)
39
- { pattern: /CM_AUTH_TOKEN=\S+/gi, replacement: 'CM_AUTH_TOKEN=[REDACTED]' },
40
- // MCBD_AUTH_TOKEN (legacy name)
41
- { pattern: /MCBD_AUTH_TOKEN=\S+/gi, replacement: 'MCBD_AUTH_TOKEN=[REDACTED]' },
42
38
  // Authorization header
43
39
  { pattern: /Authorization:\s*\S+/gi, replacement: 'Authorization: [REDACTED]' },
44
40
  // SSH key