@nogataka/smart-edit 1.0.1 → 1.0.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.
@@ -20,7 +20,7 @@ import { ActivateProjectTool, GetCurrentConfigTool, RemoveProjectTool, SwitchMod
20
20
  import { ReadFileTool, CreateTextFileTool, ListDirTool, FindFileTool, ReplaceRegexTool, DeleteLinesTool, ReplaceLinesTool, InsertAtLineTool, SearchForPatternTool } from './tools/file_tools.js';
21
21
  import { WriteMemoryTool, ReadMemoryTool, ListMemoriesTool, DeleteMemoryTool } from './tools/memory_tools.js';
22
22
  import { RestartLanguageServerTool, GetSymbolsOverviewTool, FindSymbolTool, FindReferencingSymbolsTool, ReplaceSymbolBodyTool, InsertAfterSymbolTool, InsertBeforeSymbolTool } from './tools/symbol_tools.js';
23
- import { CheckOnboardingPerformedTool, OnboardingTool, ThinkAboutCollectedInformationTool, ThinkAboutTaskAdherenceTool, ThinkAboutWhetherYouAreDoneTool, SummarizeChangesTool, PrepareForNewConversationTool, InitialInstructionsTool } from './tools/workflow_tools.js';
23
+ import { CheckOnboardingPerformedTool, OnboardingTool, CollectProjectSymbolsTool, ThinkAboutCollectedInformationTool, ThinkAboutTaskAdherenceTool, ThinkAboutWhetherYouAreDoneTool, SummarizeChangesTool, PrepareForNewConversationTool, InitialInstructionsTool } from './tools/workflow_tools.js';
24
24
  const { logger: log, memoryHandler: defaultMemoryHandler } = createSmartEditLogger({
25
25
  name: 'smart-edit.agent',
26
26
  emitToConsole: true,
@@ -62,6 +62,7 @@ const DEFAULT_TOOL_CLASSES = [
62
62
  InsertBeforeSymbolTool,
63
63
  CheckOnboardingPerformedTool,
64
64
  OnboardingTool,
65
+ CollectProjectSymbolsTool,
65
66
  ThinkAboutCollectedInformationTool,
66
67
  ThinkAboutTaskAdherenceTool,
67
68
  ThinkAboutWhetherYouAreDoneTool,
@@ -9,6 +9,7 @@ import { ProjectConfig, SmartEditConfig, SmartEditPaths } from './config/smart_e
9
9
  import { createSmartEditLogger, setConsoleLoggingEnabled } from './util/logging.js';
10
10
  import { SmartEditAgent } from './agent.js';
11
11
  import { SmartEditMCPFactorySingleProcess, createSmartEditHttpServer, createSmartEditStdioServer } from './mcp.js';
12
+ import { DEFAULT_DASHBOARD_PORT, registerInstance, unregisterInstance, findAvailablePort } from './instance-registry.js';
12
13
  import { ToolRegistry } from './tools/tools_base.js';
13
14
  import { coerceLanguage } from '../smart-lsp/ls_config.js';
14
15
  import { ensureDefaultSubprocessOptions } from '../smart-lsp/util/subprocess_util.js';
@@ -96,8 +97,25 @@ function normalizeStartMcpServerOptions(raw) {
96
97
  }
97
98
  return undefined;
98
99
  };
100
+ // プロジェクトの決定ロジック:
101
+ // 1. --no-project が指定された場合: null(プロジェクトなしで起動)
102
+ // 2. --project が指定された場合: 指定されたパス
103
+ // 3. --project-file が指定された場合: 指定されたパス(後方互換)
104
+ // 4. どれも指定されていない場合: カレントディレクトリを使用
105
+ const resolveProject = () => {
106
+ if (raw.noProject === true) {
107
+ return null;
108
+ }
109
+ if (isNonEmptyString(raw.project)) {
110
+ return raw.project;
111
+ }
112
+ if (isNonEmptyString(raw.projectFile)) {
113
+ return raw.projectFile;
114
+ }
115
+ return process.cwd();
116
+ };
99
117
  return {
100
- project: isNonEmptyString(raw.project) ? raw.project : null,
118
+ project: resolveProject(),
101
119
  projectFile: isNonEmptyString(raw.projectFile) ? raw.projectFile : null,
102
120
  context,
103
121
  modes: normalizedModes.slice(),
@@ -222,6 +240,8 @@ async function handleStartMcpServer(options, projectArg) {
222
240
  memoryLogHandler: agentOptions.memoryLogHandler ?? undefined
223
241
  })
224
242
  });
243
+ // Track registered instance for cleanup
244
+ let registeredInstance = null;
225
245
  try {
226
246
  const serverOptions = {
227
247
  host: options.host,
@@ -237,11 +257,26 @@ async function handleStartMcpServer(options, projectArg) {
237
257
  switch (options.transport) {
238
258
  case 'streamable-http': {
239
259
  const server = await createSmartEditHttpServer(factory, serverOptions);
260
+ const serverPort = server.url.port ? Number.parseInt(server.url.port, 10) : options.port;
240
261
  logger.info(`Streamable HTTP MCP server started: ${server.url.href}`);
241
262
  logger.info('Press Ctrl+C to exit.');
263
+ // Register instance (skip in test environment)
264
+ if (!process.env.SMART_EDIT_SKIP_EDITOR) {
265
+ registeredInstance = registerInstance({
266
+ port: serverPort,
267
+ project,
268
+ pid: process.pid,
269
+ transport: 'streamable-http'
270
+ });
271
+ logger.info(`Registered instance ${registeredInstance.id} in registry`);
272
+ }
242
273
  await new Promise((resolve) => {
243
274
  const shutdown = async () => {
244
275
  logger.info('Stopping HTTP MCP server...');
276
+ if (registeredInstance) {
277
+ unregisterInstance(registeredInstance.id);
278
+ logger.info(`Unregistered instance ${registeredInstance.id} from registry`);
279
+ }
245
280
  await server.close();
246
281
  resolve();
247
282
  };
@@ -257,6 +292,17 @@ async function handleStartMcpServer(options, projectArg) {
257
292
  case 'stdio': {
258
293
  const server = await createSmartEditStdioServer(factory, serverOptions);
259
294
  logger.info('STDIO MCP server started. Press Ctrl+C to exit.');
295
+ // For stdio transport, find an available port for the dashboard API (skip in test environment)
296
+ if (!process.env.SMART_EDIT_SKIP_EDITOR) {
297
+ const dashboardPort = findAvailablePort();
298
+ registeredInstance = registerInstance({
299
+ port: dashboardPort,
300
+ project,
301
+ pid: process.pid,
302
+ transport: 'stdio'
303
+ });
304
+ logger.info(`Registered instance ${registeredInstance.id} in registry (dashboard port: ${dashboardPort})`);
305
+ }
260
306
  await new Promise((resolve) => {
261
307
  let settled = false;
262
308
  const finalize = async (reason) => {
@@ -265,6 +311,10 @@ async function handleStartMcpServer(options, projectArg) {
265
311
  }
266
312
  settled = true;
267
313
  logger.info('Stopping STDIO MCP server...');
314
+ if (registeredInstance) {
315
+ unregisterInstance(registeredInstance.id);
316
+ logger.info(`Unregistered instance ${registeredInstance.id} from registry`);
317
+ }
268
318
  if (reason === 'signal') {
269
319
  await server.close();
270
320
  }
@@ -295,6 +345,11 @@ async function handleStartMcpServer(options, projectArg) {
295
345
  catch (error) {
296
346
  const message = error instanceof Error ? error.message : String(error);
297
347
  logger.error(`Smart-Edit MCP サーバーの起動に失敗しました: ${message}`);
348
+ // Unregister instance on error
349
+ if (registeredInstance) {
350
+ unregisterInstance(registeredInstance.id);
351
+ logger.info(`Unregistered instance ${registeredInstance.id} from registry (error cleanup)`);
352
+ }
298
353
  throw error;
299
354
  }
300
355
  finally {
@@ -379,7 +434,8 @@ export function createSmartEditCli(options = {}) {
379
434
  }
380
435
  const startMcpServerCommand = new Command('start-mcp-server')
381
436
  .description('Smart-Edit MCP サーバーを起動します。')
382
- .option('--project [project]', '起動時にアクティブ化するプロジェクト名またはパス。')
437
+ .option('--project [project]', '起動時にアクティブ化するプロジェクトパス。省略時はカレントディレクトリを使用。')
438
+ .option('--no-project', 'プロジェクトなしで起動。後から activate_project ツールで指定可能。')
383
439
  .option('--project-file [project]', '[非推奨] --project の旧名称。')
384
440
  .argument('[project]', '[非推奨] プロジェクトの位置引数。')
385
441
  .option('--context <context>', 'ビルトインコンテキスト名またはカスタム YAML へのパス。')
@@ -404,11 +460,32 @@ export function createSmartEditCli(options = {}) {
404
460
  await handleStartMcpServer(opts, normalizedProjectArg);
405
461
  }
406
462
  catch (error) {
463
+ // In test environment, rethrow the error so vitest can display it properly
464
+ // (this.error() calls process.exit which vitest intercepts, hiding the actual error)
465
+ if (process.env.SMART_EDIT_SKIP_EDITOR) {
466
+ throw error;
467
+ }
407
468
  const message = error instanceof Error ? error.message : String(error);
408
469
  this.error(`${message}\n`, { exitCode: 1 });
409
470
  }
410
471
  });
411
472
  program.addCommand(startMcpServerCommand);
473
+ const startDashboardCommand = new Command('start-dashboard')
474
+ .description('統合ダッシュボードを起動します(MCPサーバーなし)。複数のMCPインスタンスを一括管理できます。')
475
+ .option('--port <port>', 'ダッシュボードのポート番号。', (value) => parseInteger(value, '--port'), DEFAULT_DASHBOARD_PORT)
476
+ .action(async function () {
477
+ const opts = this.optsWithGlobals();
478
+ const port = typeof opts.port === 'number' ? opts.port : DEFAULT_DASHBOARD_PORT;
479
+ try {
480
+ const { runStandaloneDashboard } = await import('./standalone-dashboard.js');
481
+ await runStandaloneDashboard({ port });
482
+ }
483
+ catch (error) {
484
+ const message = error instanceof Error ? error.message : String(error);
485
+ this.error(`${message}\n`, { exitCode: 1 });
486
+ }
487
+ });
488
+ program.addCommand(startDashboardCommand);
412
489
  const modeCommand = new Command('mode')
413
490
  .description('Smart-Edit モードを管理します。');
414
491
  modeCommand
@@ -7,6 +7,7 @@ import path from 'node:path';
7
7
  import process from 'node:process';
8
8
  import { createSmartEditLogger } from './util/logging.js';
9
9
  import { SMART_EDIT_DASHBOARD_DIR } from './constants.js';
10
+ import { getInstances } from './instance-registry.js';
10
11
  const { logger } = createSmartEditLogger({ name: 'smart-edit.dashboard', emitToConsole: false, level: 'info' });
11
12
  const DEFAULT_DASHBOARD_PORT = 0x5eda;
12
13
  const DASHBOARD_HOST = '127.0.0.1';
@@ -292,6 +293,16 @@ export class SmartEditDashboardAPI {
292
293
  const method = req.method?.toUpperCase() ?? 'GET';
293
294
  const url = parseUrl(req.url ?? '/', true);
294
295
  const pathname = url.pathname ?? '/';
296
+ // Add CORS headers for cross-origin requests from multi-instance dashboard
297
+ res.setHeader('Access-Control-Allow-Origin', '*');
298
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
299
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
300
+ // Handle CORS preflight
301
+ if (method === 'OPTIONS') {
302
+ res.statusCode = 204;
303
+ res.end();
304
+ return;
305
+ }
295
306
  try {
296
307
  if (pathname.startsWith('/dashboard')) {
297
308
  if (method !== 'GET') {
@@ -356,6 +367,24 @@ export class SmartEditDashboardAPI {
356
367
  this.shutdown();
357
368
  this.sendJson(res, 200, { status: 'shutting down' });
358
369
  return;
370
+ // Multi-instance dashboard APIs
371
+ case '/api/instances':
372
+ if (method !== 'GET') {
373
+ this.respondMethodNotAllowed(res);
374
+ return;
375
+ }
376
+ this.sendJson(res, 200, { instances: getInstances() });
377
+ return;
378
+ case '/api/instance-info':
379
+ if (method !== 'GET') {
380
+ this.respondMethodNotAllowed(res);
381
+ return;
382
+ }
383
+ this.sendJson(res, 200, {
384
+ port: this.listeningPort,
385
+ project: this.agent.getActiveProject()?.projectName ?? null
386
+ });
387
+ return;
359
388
  default:
360
389
  this.respondNotFound(res);
361
390
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Instance Registry for Multi-Instance Dashboard
3
+ *
4
+ * Manages registration of multiple Smart Edit MCP server instances.
5
+ * Each instance registers itself when starting and unregisters when stopping.
6
+ * The dashboard reads this registry to discover and connect to all running instances.
7
+ */
8
+ export declare const DEFAULT_DASHBOARD_PORT = 24282;
9
+ export interface InstanceInfo {
10
+ id: string;
11
+ port: number;
12
+ project: string | null;
13
+ pid: number;
14
+ startedAt: string;
15
+ transport: 'stdio' | 'sse' | 'streamable-http';
16
+ }
17
+ export declare function generateInstanceId(): string;
18
+ /**
19
+ * Register a new MCP server instance in the registry.
20
+ * This function is designed to be non-fatal - if registration fails, the instance
21
+ * will still work, just won't be visible in the multi-instance dashboard.
22
+ */
23
+ export declare function registerInstance(info: Omit<InstanceInfo, 'id' | 'startedAt'>): InstanceInfo;
24
+ /**
25
+ * Unregister an MCP server instance from the registry.
26
+ */
27
+ export declare function unregisterInstance(instanceId: string): void;
28
+ /**
29
+ * Get all registered instances (with cleanup of dead processes).
30
+ */
31
+ export declare function getInstances(): InstanceInfo[];
32
+ /**
33
+ * Get a specific instance by ID.
34
+ */
35
+ export declare function getInstance(instanceId: string): InstanceInfo | null;
36
+ /**
37
+ * Find an available port for the dashboard, starting from the default port.
38
+ */
39
+ export declare function findAvailablePort(startPort?: number): number;
40
+ /**
41
+ * Update instance info (e.g., when project changes).
42
+ */
43
+ export declare function updateInstance(instanceId: string, updates: Partial<Pick<InstanceInfo, 'project'>>): void;
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Instance Registry for Multi-Instance Dashboard
3
+ *
4
+ * Manages registration of multiple Smart Edit MCP server instances.
5
+ * Each instance registers itself when starting and unregisters when stopping.
6
+ * The dashboard reads this registry to discover and connect to all running instances.
7
+ */
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import process from 'node:process';
12
+ import crypto from 'node:crypto';
13
+ import { SMART_EDIT_MANAGED_DIR_NAME } from './constants.js';
14
+ import { createSmartEditLogger } from './util/logging.js';
15
+ const { logger } = createSmartEditLogger({ name: 'smart-edit.instance-registry', emitToConsole: false, level: 'info' });
16
+ export const DEFAULT_DASHBOARD_PORT = 0x5eda; // 24282
17
+ // Compute paths dynamically to respect runtime HOME changes (important for testing)
18
+ // Use process.env.HOME/USERPROFILE first as os.homedir() may cache the value
19
+ function getSmartEditDir() {
20
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
21
+ return path.join(home, SMART_EDIT_MANAGED_DIR_NAME);
22
+ }
23
+ function getInstancesFilePath() {
24
+ return path.join(getSmartEditDir(), 'instances.json');
25
+ }
26
+ function getLockFilePath() {
27
+ return path.join(getSmartEditDir(), 'instances.lock');
28
+ }
29
+ const LOCK_TIMEOUT_MS = 5000;
30
+ const LOCK_RETRY_INTERVAL_MS = 50;
31
+ function ensureDirectoryExists(filePath) {
32
+ const dir = path.dirname(filePath);
33
+ if (!fs.existsSync(dir)) {
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ }
36
+ }
37
+ function acquireLock() {
38
+ try {
39
+ const lockFile = getLockFilePath();
40
+ ensureDirectoryExists(lockFile);
41
+ const startTime = Date.now();
42
+ while (Date.now() - startTime < LOCK_TIMEOUT_MS) {
43
+ try {
44
+ fs.writeFileSync(lockFile, String(process.pid), { flag: 'wx' });
45
+ return true;
46
+ }
47
+ catch (error) {
48
+ const err = error;
49
+ if (err.code === 'EEXIST') {
50
+ // Lock file exists, check if the process is still alive
51
+ try {
52
+ const lockPid = Number.parseInt(fs.readFileSync(lockFile, 'utf-8').trim(), 10);
53
+ if (!Number.isNaN(lockPid)) {
54
+ try {
55
+ // Check if process is alive (signal 0 doesn't kill, just checks)
56
+ process.kill(lockPid, 0);
57
+ }
58
+ catch {
59
+ // Process is dead, remove stale lock
60
+ try {
61
+ fs.unlinkSync(lockFile);
62
+ continue;
63
+ }
64
+ catch {
65
+ // Ignore unlink errors
66
+ }
67
+ }
68
+ }
69
+ }
70
+ catch {
71
+ // Can't read lock file, try to remove it
72
+ try {
73
+ fs.unlinkSync(lockFile);
74
+ continue;
75
+ }
76
+ catch {
77
+ // Ignore unlink errors
78
+ }
79
+ }
80
+ // Brief busy-wait before retry
81
+ const waitUntil = Date.now() + Math.min(LOCK_RETRY_INTERVAL_MS, LOCK_TIMEOUT_MS - (Date.now() - startTime));
82
+ while (Date.now() < waitUntil) {
83
+ // Busy wait - acceptable since lock contention should be rare
84
+ }
85
+ continue;
86
+ }
87
+ // For other errors (EACCES, ENOENT, etc.), give up on locking
88
+ logger.warn(`Lock acquisition failed with error: ${err.code}`, err);
89
+ return false;
90
+ }
91
+ }
92
+ logger.warn('Failed to acquire lock for instance registry (timeout)');
93
+ return false;
94
+ }
95
+ catch (error) {
96
+ // Catch any unexpected errors (e.g., from ensureDirectoryExists)
97
+ logger.warn('Unexpected error during lock acquisition', error instanceof Error ? error : undefined);
98
+ return false;
99
+ }
100
+ }
101
+ function releaseLock() {
102
+ try {
103
+ fs.unlinkSync(getLockFilePath());
104
+ }
105
+ catch {
106
+ // Ignore errors when releasing lock
107
+ }
108
+ }
109
+ function readRegistry() {
110
+ const instancesFile = getInstancesFilePath();
111
+ ensureDirectoryExists(instancesFile);
112
+ try {
113
+ if (fs.existsSync(instancesFile)) {
114
+ const content = fs.readFileSync(instancesFile, 'utf-8');
115
+ const data = JSON.parse(content);
116
+ if (data && typeof data === 'object' && Array.isArray(data.instances)) {
117
+ return data;
118
+ }
119
+ }
120
+ }
121
+ catch (error) {
122
+ logger.warn('Failed to read instance registry, starting fresh', error instanceof Error ? error : undefined);
123
+ }
124
+ return { instances: [] };
125
+ }
126
+ function writeRegistry(data) {
127
+ const instancesFile = getInstancesFilePath();
128
+ ensureDirectoryExists(instancesFile);
129
+ fs.writeFileSync(instancesFile, JSON.stringify(data, null, 2), 'utf-8');
130
+ }
131
+ function isProcessAlive(pid) {
132
+ try {
133
+ process.kill(pid, 0);
134
+ return true;
135
+ }
136
+ catch {
137
+ return false;
138
+ }
139
+ }
140
+ function cleanupDeadInstances(data) {
141
+ const aliveInstances = data.instances.filter((instance) => isProcessAlive(instance.pid));
142
+ if (aliveInstances.length !== data.instances.length) {
143
+ logger.info(`Cleaned up ${data.instances.length - aliveInstances.length} dead instance(s) from registry`);
144
+ }
145
+ return { instances: aliveInstances };
146
+ }
147
+ export function generateInstanceId() {
148
+ return crypto.randomBytes(6).toString('hex');
149
+ }
150
+ /**
151
+ * Register a new MCP server instance in the registry.
152
+ * This function is designed to be non-fatal - if registration fails, the instance
153
+ * will still work, just won't be visible in the multi-instance dashboard.
154
+ */
155
+ export function registerInstance(info) {
156
+ const id = generateInstanceId();
157
+ const instance = {
158
+ ...info,
159
+ id,
160
+ startedAt: new Date().toISOString()
161
+ };
162
+ try {
163
+ if (!acquireLock()) {
164
+ logger.warn('Failed to acquire lock for registering instance, continuing without registration');
165
+ return instance;
166
+ }
167
+ try {
168
+ let data = readRegistry();
169
+ data = cleanupDeadInstances(data);
170
+ // Check for duplicate port (shouldn't happen, but just in case)
171
+ const existingIndex = data.instances.findIndex((i) => i.port === info.port);
172
+ if (existingIndex !== -1) {
173
+ data.instances.splice(existingIndex, 1);
174
+ }
175
+ data.instances.push(instance);
176
+ writeRegistry(data);
177
+ logger.info(`Registered instance ${id} on port ${info.port} for project: ${info.project ?? '(none)'}`);
178
+ }
179
+ finally {
180
+ releaseLock();
181
+ }
182
+ }
183
+ catch (error) {
184
+ logger.warn('Failed to register instance in registry', error instanceof Error ? error : undefined);
185
+ }
186
+ return instance;
187
+ }
188
+ /**
189
+ * Unregister an MCP server instance from the registry.
190
+ */
191
+ export function unregisterInstance(instanceId) {
192
+ if (!acquireLock()) {
193
+ logger.error('Failed to acquire lock for unregistering instance');
194
+ return;
195
+ }
196
+ try {
197
+ const data = readRegistry();
198
+ const index = data.instances.findIndex((i) => i.id === instanceId);
199
+ if (index !== -1) {
200
+ const removed = data.instances.splice(index, 1)[0];
201
+ writeRegistry(data);
202
+ logger.info(`Unregistered instance ${instanceId} (port: ${removed.port})`);
203
+ }
204
+ }
205
+ finally {
206
+ releaseLock();
207
+ }
208
+ }
209
+ /**
210
+ * Get all registered instances (with cleanup of dead processes).
211
+ */
212
+ export function getInstances() {
213
+ if (!acquireLock()) {
214
+ // Even if we can't acquire lock, try to read
215
+ const data = readRegistry();
216
+ return data.instances.filter((instance) => isProcessAlive(instance.pid));
217
+ }
218
+ try {
219
+ let data = readRegistry();
220
+ data = cleanupDeadInstances(data);
221
+ writeRegistry(data);
222
+ return data.instances;
223
+ }
224
+ finally {
225
+ releaseLock();
226
+ }
227
+ }
228
+ /**
229
+ * Get a specific instance by ID.
230
+ */
231
+ export function getInstance(instanceId) {
232
+ const instances = getInstances();
233
+ return instances.find((i) => i.id === instanceId) ?? null;
234
+ }
235
+ /**
236
+ * Find an available port for the dashboard, starting from the default port.
237
+ */
238
+ export function findAvailablePort(startPort = DEFAULT_DASHBOARD_PORT) {
239
+ const instances = getInstances();
240
+ const usedPorts = new Set(instances.map((i) => i.port));
241
+ let port = startPort;
242
+ while (usedPorts.has(port) && port <= 65535) {
243
+ port++;
244
+ }
245
+ return port;
246
+ }
247
+ /**
248
+ * Update instance info (e.g., when project changes).
249
+ */
250
+ export function updateInstance(instanceId, updates) {
251
+ if (!acquireLock()) {
252
+ logger.error('Failed to acquire lock for updating instance');
253
+ return;
254
+ }
255
+ try {
256
+ const data = readRegistry();
257
+ const instance = data.instances.find((i) => i.id === instanceId);
258
+ if (instance) {
259
+ if (updates.project !== undefined) {
260
+ instance.project = updates.project;
261
+ }
262
+ writeRegistry(data);
263
+ logger.info(`Updated instance ${instanceId}`);
264
+ }
265
+ }
266
+ finally {
267
+ releaseLock();
268
+ }
269
+ }
@@ -0,0 +1,47 @@
1
+ description: Careful editing mode that respects existing code and prevents duplicate implementations
2
+ prompt: |
3
+ You are operating in careful-editor mode. This mode emphasizes avoiding duplicate implementations
4
+ and respecting existing code patterns in the project.
5
+
6
+ ## Duplicate Prevention Rules
7
+
8
+ Before implementing anything, you MUST check for existing implementations:
9
+
10
+ 1. **Read the "project-symbols" memory first**
11
+ - Check existing utility functions
12
+ - Check existing dependencies and their purposes
13
+ - Review common components and patterns
14
+
15
+ 2. **Search for similar functionality**
16
+ - Before creating a new function, use `find_symbol` to search for similar names
17
+ - Before adding a new library, check package.json for existing dependencies
18
+ - Use `get_symbols_overview` to understand available utilities in relevant files
19
+
20
+ 3. **Prefer existing code over new code**
21
+ - If an existing function can accomplish the task, use it instead of creating a new one
22
+ - If an existing library provides the functionality, use it instead of adding a new dependency
23
+ - If a similar pattern exists elsewhere in the codebase, follow that pattern
24
+
25
+ 4. **Check onboarding status**
26
+ - If `check_onboarding_performed` returns `significantChanges: true`,
27
+ you should run onboarding again to refresh the project-symbols memory
28
+
29
+ ## When Creating New Code
30
+
31
+ If you determine that new code is truly necessary:
32
+
33
+ 1. Place it in the appropriate existing file when possible
34
+ 2. Follow existing naming conventions in the project
35
+ 3. Document why existing utilities couldn't be used (if applicable)
36
+ 4. Consider whether this new code should be added to the project-symbols memory
37
+
38
+ ## Examples of What to Avoid
39
+
40
+ - Creating `formatDate()` when the project already uses date-fns
41
+ - Adding lodash when the project already has similar utilities
42
+ - Creating a new `Button` component when one exists in `components/common/`
43
+ - Implementing custom validation when zod is already used
44
+
45
+ You have access to all editing tools, but use them thoughtfully.
46
+ excluded_tools: []
47
+ included_optional_tools: []