@sov3rain/nota 0.1.0 → 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.
package/bin/nota.mjs CHANGED
@@ -1,126 +1,150 @@
1
- #!/usr/bin/env node
2
-
3
- import { createServer } from 'node:http';
4
- import { randomBytes } from 'node:crypto';
5
- import { createReadStream } from 'node:fs';
6
- import { access, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
7
- import { dirname, extname, join, resolve } from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
- import { spawn } from 'node:child_process';
10
-
1
+ #!/usr/bin/env node
2
+
3
+ import { createServer } from 'node:http';
4
+ import { randomBytes } from 'node:crypto';
5
+ import { createReadStream } from 'node:fs';
6
+ import { access, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
7
+ import { dirname, extname, join, resolve } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { spawn } from 'node:child_process';
10
+ import { getAgentInstructionBlock } from './agentInstructions.mjs';
11
+
11
12
  const MAX_BODY_BYTES = 5 * 1024 * 1024;
13
+ const HEARTBEAT_TIMEOUT_MS = 15_000;
14
+ const HEARTBEAT_CHECK_MS = 5_000;
12
15
  const MIME_TYPES = {
13
- '.css': 'text/css; charset=utf-8',
14
- '.html': 'text/html; charset=utf-8',
15
- '.js': 'text/javascript; charset=utf-8',
16
- '.json': 'application/json; charset=utf-8',
17
- '.svg': 'image/svg+xml',
18
- };
19
-
20
- const args = process.argv.slice(2);
21
- const command = args[0];
22
- const wantsHelp = args.includes('--help') || args.includes('-h');
23
- const shouldOpenBrowser = !args.includes('--no-open');
24
- const targetFile = args.find((arg) => !arg.startsWith('-'));
25
-
26
- if (command === 'init') {
27
- if (wantsHelp) {
28
- printUsage();
29
- process.exit(0);
30
- }
31
-
32
- await initAgentInstructions(args[1] ?? 'all');
33
- process.exit(0);
34
- }
35
-
36
- if (wantsHelp || !targetFile) {
37
- printUsage();
38
- process.exit(wantsHelp ? 0 : 1);
39
- }
40
-
16
+ '.css': 'text/css; charset=utf-8',
17
+ '.html': 'text/html; charset=utf-8',
18
+ '.js': 'text/javascript; charset=utf-8',
19
+ '.json': 'application/json; charset=utf-8',
20
+ '.svg': 'image/svg+xml',
21
+ };
22
+ const AGENT_TARGETS = ['codex', 'claude', 'cursor'];
23
+ const AGENT_LABELS = {
24
+ codex: 'Codex',
25
+ claude: 'Claude',
26
+ cursor: 'Cursor',
27
+ };
28
+
29
+ const args = process.argv.slice(2);
30
+ const command = args[0];
31
+ const wantsHelp = args.includes('--help') || args.includes('-h');
32
+ const shouldOpenBrowser = !args.includes('--no-open');
33
+ const targetFile = args.find((arg) => !arg.startsWith('-'));
34
+
35
+ if (command === 'init') {
36
+ if (wantsHelp) {
37
+ printUsage();
38
+ process.exit(0);
39
+ }
40
+
41
+ const targets = args[1] ? getAgentTargets(args[1]) : await promptForAgentTargets();
42
+
43
+ if (!targets) {
44
+ console.error('Unknown agent target. Expected codex, claude, cursor, or all.');
45
+ process.exit(1);
46
+ }
47
+
48
+ if (targets.length === 0) {
49
+ console.log('No agents selected. Nothing to install.');
50
+ process.exit(0);
51
+ }
52
+
53
+ await initAgentInstructions(targets);
54
+ process.exit(0);
55
+ }
56
+
57
+ if (wantsHelp || !targetFile) {
58
+ printUsage();
59
+ process.exit(wantsHelp ? 0 : 1);
60
+ }
61
+
41
62
  const documentPath = resolve(process.cwd(), targetFile);
42
63
  const distDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'dist');
43
64
  const session = randomBytes(24).toString('hex');
44
-
45
- await ensureReadableFile(documentPath);
46
- await ensureBuildExists(distDir);
47
-
65
+ let lastHeartbeatAt = null;
66
+
67
+ await ensureReadableFile(documentPath);
68
+ await ensureBuildExists(distDir);
69
+
48
70
  const server = createServer((request, response) => {
49
71
  handleRequest(request, response).catch((error) => {
50
- console.error(error);
51
- sendJson(response, 500, { error: 'Internal server error' });
72
+ console.error(error);
73
+ sendJson(response, 500, { error: 'Internal server error' });
52
74
  });
53
75
  });
54
-
55
- server.listen(0, '127.0.0.1', () => {
56
- const address = server.address();
57
- const port = typeof address === 'object' && address ? address.port : 0;
58
- const url = `http://127.0.0.1:${port}/?session=${session}`;
59
-
60
- console.log(`Plan Annotation: ${documentPath}`);
61
- console.log(`Open: ${url}`);
62
- if (shouldOpenBrowser) {
63
- openBrowser(url);
64
- }
65
- });
66
-
76
+ const heartbeatTimeout = setInterval(closeInactiveServer, HEARTBEAT_CHECK_MS);
77
+ heartbeatTimeout.unref();
78
+
79
+ server.listen(0, '127.0.0.1', () => {
80
+ const address = server.address();
81
+ const port = typeof address === 'object' && address ? address.port : 0;
82
+ const url = `http://127.0.0.1:${port}/?session=${session}`;
83
+
84
+ console.log(`Plan Annotation: ${documentPath}`);
85
+ console.log(`Open: ${url}`);
86
+ if (shouldOpenBrowser) {
87
+ openBrowser(url);
88
+ }
89
+ });
90
+
67
91
  process.on('SIGINT', () => {
68
92
  server.close(() => process.exit(0));
69
93
  });
70
-
71
- async function handleRequest(request, response) {
72
- const url = new URL(request.url ?? '/', 'http://127.0.0.1');
73
- const apiHandler = getApiHandler(url.pathname, request.method);
74
-
75
- if (apiHandler) {
76
- await apiHandler(request, response, url);
77
- return;
78
- }
79
-
80
- await serveStaticFile(url.pathname, response);
81
- }
82
-
83
- async function handleDocumentApi(request, response, url) {
84
- if (!isAuthorized(request, url)) {
85
- sendJson(response, 403, { error: 'Forbidden' });
86
- return;
87
- }
88
-
89
- if (request.method === 'GET') {
90
- await sendDocument(response);
91
- return;
92
- }
93
-
94
- if (request.method === 'PUT') {
95
- await saveDocument(request, response);
96
- return;
97
- }
98
-
99
- response.writeHead(405, { Allow: 'GET, PUT' });
100
- response.end();
101
- }
102
-
103
- async function serveStaticFile(pathname, response) {
104
- const candidate = resolveStaticPath(pathname);
105
-
106
- if (!candidate) {
107
- sendForbiddenStaticFile(response);
108
- return;
109
- }
110
-
111
- const fileStat = await getStaticFileStat(candidate);
112
-
113
- if (!fileStat) {
114
- sendNotFound(response);
115
- return;
116
- }
117
-
118
- response.writeHead(200, {
119
- 'Content-Type': MIME_TYPES[extname(candidate)] ?? 'application/octet-stream',
120
- });
121
- createReadStream(candidate).pipe(response);
122
- }
123
-
94
+
95
+ async function handleRequest(request, response) {
96
+ const url = new URL(request.url ?? '/', 'http://127.0.0.1');
97
+ const apiHandler = getApiHandler(url.pathname, request.method);
98
+
99
+ if (apiHandler) {
100
+ await apiHandler(request, response, url);
101
+ return;
102
+ }
103
+
104
+ await serveStaticFile(url.pathname, response);
105
+ }
106
+
107
+ async function handleDocumentApi(request, response, url) {
108
+ if (!isAuthorized(request, url)) {
109
+ sendJson(response, 403, { error: 'Forbidden' });
110
+ return;
111
+ }
112
+
113
+ if (request.method === 'GET') {
114
+ await sendDocument(response);
115
+ return;
116
+ }
117
+
118
+ if (request.method === 'PUT') {
119
+ await saveDocument(request, response);
120
+ return;
121
+ }
122
+
123
+ response.writeHead(405, { Allow: 'GET, PUT' });
124
+ response.end();
125
+ }
126
+
127
+ async function serveStaticFile(pathname, response) {
128
+ const candidate = resolveStaticPath(pathname);
129
+
130
+ if (!candidate) {
131
+ sendForbiddenStaticFile(response);
132
+ return;
133
+ }
134
+
135
+ const fileStat = await getStaticFileStat(candidate);
136
+
137
+ if (!fileStat) {
138
+ sendNotFound(response);
139
+ return;
140
+ }
141
+
142
+ response.writeHead(200, {
143
+ 'Content-Type': MIME_TYPES[extname(candidate)] ?? 'application/octet-stream',
144
+ });
145
+ createReadStream(candidate).pipe(response);
146
+ }
147
+
124
148
  function getApiHandler(pathname, method) {
125
149
  const route = `${method} ${pathname}`;
126
150
 
@@ -128,267 +152,249 @@ function getApiHandler(pathname, method) {
128
152
  {
129
153
  'GET /api/document': handleDocumentApi,
130
154
  'PUT /api/document': handleDocumentApi,
155
+ 'POST /api/heartbeat': handleHeartbeatApi,
131
156
  'POST /api/shutdown': handleShutdownApi,
132
157
  }[route] ?? null
133
158
  );
134
159
  }
135
160
 
136
- function handleShutdownApi(request, response, url) {
161
+ function handleHeartbeatApi(request, response, url) {
137
162
  if (!isAuthorized(request, url)) {
138
163
  sendJson(response, 403, { error: 'Forbidden' });
139
164
  return;
140
165
  }
141
166
 
167
+ lastHeartbeatAt = Date.now();
142
168
  sendJson(response, 200, { ok: true });
143
- setTimeout(() => process.exit(0), 100);
144
- }
145
-
146
- async function sendDocument(response) {
147
- sendJson(response, 200, {
148
- filename: documentPath.split(/[\\/]/).pop() ?? 'document.md',
149
- markdown: await readFile(documentPath, 'utf8'),
150
- });
151
- }
152
-
153
- async function saveDocument(request, response) {
154
- const body = await readRequestBody(request);
155
- const parsed = JSON.parse(body);
156
-
157
- if (!parsed || typeof parsed.markdown !== 'string') {
158
- sendJson(response, 400, { error: 'Expected markdown string' });
159
- return;
160
- }
161
-
162
- await writeFile(documentPath, parsed.markdown, 'utf8');
163
- sendJson(response, 200, { ok: true });
164
- }
165
-
166
- function resolveStaticPath(pathname) {
167
- const relativePath = pathname === '/' ? 'index.html' : decodeURIComponent(pathname.slice(1));
168
- const candidate = resolve(distDir, relativePath);
169
-
170
- return isInsideDirectory(candidate, distDir) ? candidate : null;
171
- }
172
-
173
- function isInsideDirectory(candidate, directory) {
174
- return (
175
- candidate === directory ||
176
- candidate.startsWith(`${directory}\\`) ||
177
- candidate.startsWith(`${directory}/`)
178
- );
179
- }
180
-
181
- async function getStaticFileStat(filePath) {
182
- try {
183
- const fileStat = await stat(filePath);
184
- return fileStat.isFile() ? fileStat : null;
185
- } catch {
186
- return null;
187
- }
188
- }
189
-
190
- function sendForbiddenStaticFile(response) {
191
- response.writeHead(403);
192
- response.end();
193
169
  }
194
170
 
195
- function sendNotFound(response) {
196
- response.writeHead(404);
197
- response.end('Not found');
198
- }
199
-
200
- function isAuthorized(request, url) {
201
- return (
202
- request.headers['x-nota-session'] === session || url.searchParams.get('session') === session
203
- );
204
- }
205
-
206
- function readRequestBody(request) {
207
- return new Promise((resolveBody, rejectBody) => {
208
- let body = '';
209
-
210
- request.setEncoding('utf8');
211
- request.on('data', (chunk) => {
212
- body += chunk;
213
-
214
- if (Buffer.byteLength(body, 'utf8') > MAX_BODY_BYTES) {
215
- rejectBody(new Error('Request body too large'));
216
- request.destroy();
217
- }
218
- });
219
- request.on('end', () => resolveBody(body));
220
- request.on('error', rejectBody);
221
- });
222
- }
223
-
224
- function sendJson(response, statusCode, payload) {
225
- response.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' });
226
- response.end(JSON.stringify(payload));
227
- }
228
-
229
- async function ensureReadableFile(filePath) {
230
- try {
231
- const fileStat = await stat(filePath);
232
- if (!fileStat.isFile()) throw new Error('Not a file');
233
- await access(filePath);
234
- } catch {
235
- console.error(`Cannot read markdown file: ${filePath}`);
236
- process.exit(1);
237
- }
238
- }
239
-
240
- async function ensureBuildExists(buildDir) {
241
- try {
242
- await access(join(buildDir, 'index.html'));
243
- } catch {
244
- console.error('Missing dist/index.html. Run `npm run build` before using the CLI from source.');
245
- process.exit(1);
246
- }
247
- }
248
-
249
- function openBrowser(url) {
250
- const platform = process.platform;
251
- const command = platform === 'win32' ? 'cmd' : platform === 'darwin' ? 'open' : 'xdg-open';
252
- const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
253
- const child = spawn(command, args, {
254
- detached: true,
255
- stdio: 'ignore',
256
- windowsHide: true,
257
- });
258
-
259
- child.unref();
260
- }
261
-
262
- function printUsage() {
263
- console.log(`Usage:
264
- nota [--no-open] <path/to/plan.md>
265
- nota init [codex|claude|cursor|all]`);
266
- }
267
-
268
- async function initAgentInstructions(target) {
269
- const targets = getAgentTargets(target);
270
-
271
- if (!targets) {
272
- console.error('Unknown agent target. Expected codex, claude, cursor, or all.');
273
- process.exit(1);
274
- }
275
-
276
- await mkdir(resolve(process.cwd(), '.agent-plans'), { recursive: true });
277
- await writeFile(resolve(process.cwd(), '.agent-plans', '.gitkeep'), '', { flag: 'a' });
278
-
279
- for (const agent of targets) {
280
- await writeAgentInstructions(agent);
281
- }
282
-
283
- console.log(`Plan Annotation instructions installed for: ${targets.join(', ')}`);
284
- console.log('Plans directory ready: .agent-plans/');
285
- }
286
-
287
- function getAgentTargets(target) {
288
- const allTargets = ['codex', 'claude', 'cursor'];
289
-
290
- if (target === 'all') return allTargets;
291
- if (allTargets.includes(target)) return [target];
292
-
293
- return null;
294
- }
295
-
296
- async function writeAgentInstructions(agent) {
297
- const filePath = resolve(process.cwd(), getAgentInstructionPath(agent));
298
- const block = getAgentInstructionBlock(agent);
299
-
300
- await mkdir(dirname(filePath), { recursive: true });
301
- await upsertMarkedBlock(filePath, block);
302
- console.log(`Updated ${filePath}`);
303
- }
304
-
305
- function getAgentInstructionPath(agent) {
306
- if (agent === 'codex') return 'AGENTS.md';
307
- if (agent === 'claude') return 'CLAUDE.md';
308
-
309
- return join('.cursor', 'rules', 'nota.mdc');
310
- }
311
-
312
- function getAgentInstructionBlock() {
313
- return `<!-- nota:start -->
314
- ## Plan Annotation Workflow
315
-
316
- When the user asks for an implementation plan, create a Markdown file in:
317
-
318
- \`\`\`text
319
- .agent-plans/<date>-<topic>.md
320
- \`\`\`
321
-
322
- Then launch the local annotation tool.
323
-
324
- On Windows/PowerShell, use the npm \`.cmd\` shim explicitly:
325
-
326
- \`\`\`powershell
327
- nota.cmd .agent-plans/<file>.md
328
- \`\`\`
329
-
330
- Do not use \`Start-Process -FilePath nota\` on Windows/PowerShell; it can resolve to the extensionless npm shim and trigger the Windows "open with" dialog instead of running the CLI.
331
-
332
- On macOS/Linux:
333
-
334
- \`\`\`bash
335
- nota .agent-plans/<file>.md
336
- \`\`\`
337
-
338
- Tell the user that the plan is open for annotation. Do not continue with implementation until the user says they are done annotating.
339
-
340
- After the user is done, read the same Markdown file again. User feedback is stored as CriticMarkup:
341
-
342
- \`\`\`md
343
- {==text being discussed==}{>>user comment<<}
344
- \`\`\`
345
-
346
- Treat those comments as direct user feedback. Update the plan or implementation accordingly.
347
-
348
- Once all annotations have been processed, remove every CriticMarkup annotation from the file, as well as the global annotations block if present:
349
-
350
- \`\`\`md
351
- <!-- nota:globals
352
- [...]
353
- -->
354
- \`\`\`
355
-
356
- Leave only the plain Markdown content.
357
-
358
- If the \`nota\` command is unavailable, tell the user to install it globally:
359
-
360
- \`\`\`bash
361
- npm install -g nota
362
- \`\`\`
363
- <!-- nota:end -->
364
- `;
365
- }
366
-
367
- async function upsertMarkedBlock(filePath, block) {
368
- const startMarker = '<!-- nota:start -->';
369
- const endMarker = '<!-- nota:end -->';
370
- const existing = await readOptionalFile(filePath);
371
- const start = existing.indexOf(startMarker);
372
- const end = existing.indexOf(endMarker);
373
-
374
- if (start !== -1 && end !== -1 && end > start) {
375
- const before = existing.slice(0, start).trimEnd();
376
- const after = existing.slice(end + endMarker.length).trimStart();
377
- await writeFile(filePath, joinTextBlocks(before, block.trimEnd(), after), 'utf8');
378
- return;
379
- }
380
-
381
- await writeFile(filePath, joinTextBlocks(existing.trimEnd(), block.trimEnd()), 'utf8');
171
+ function handleShutdownApi(request, response, url) {
172
+ if (!isAuthorized(request, url)) {
173
+ sendJson(response, 403, { error: 'Forbidden' });
174
+ return;
175
+ }
176
+
177
+ sendJson(response, 200, { ok: true });
178
+ setTimeout(() => process.exit(0), 100);
382
179
  }
383
180
 
384
- async function readOptionalFile(filePath) {
385
- try {
386
- return await readFile(filePath, 'utf8');
387
- } catch {
388
- return '';
389
- }
390
- }
181
+ function closeInactiveServer() {
182
+ if (!lastHeartbeatAt) return;
183
+ if (Date.now() - lastHeartbeatAt < HEARTBEAT_TIMEOUT_MS) return;
391
184
 
392
- function joinTextBlocks(...blocks) {
393
- return `${blocks.filter(Boolean).join('\n\n')}\n`;
185
+ console.log('No active annotation tab detected. Closing nota server.');
186
+ server.close(() => process.exit(0));
394
187
  }
188
+
189
+ async function sendDocument(response) {
190
+ sendJson(response, 200, {
191
+ filename: documentPath.split(/[\\/]/).pop() ?? 'document.md',
192
+ markdown: await readFile(documentPath, 'utf8'),
193
+ });
194
+ }
195
+
196
+ async function saveDocument(request, response) {
197
+ const body = await readRequestBody(request);
198
+ const parsed = JSON.parse(body);
199
+
200
+ if (!parsed || typeof parsed.markdown !== 'string') {
201
+ sendJson(response, 400, { error: 'Expected markdown string' });
202
+ return;
203
+ }
204
+
205
+ await writeFile(documentPath, parsed.markdown, 'utf8');
206
+ sendJson(response, 200, { ok: true });
207
+ }
208
+
209
+ function resolveStaticPath(pathname) {
210
+ const relativePath = pathname === '/' ? 'index.html' : decodeURIComponent(pathname.slice(1));
211
+ const candidate = resolve(distDir, relativePath);
212
+
213
+ return isInsideDirectory(candidate, distDir) ? candidate : null;
214
+ }
215
+
216
+ function isInsideDirectory(candidate, directory) {
217
+ return (
218
+ candidate === directory ||
219
+ candidate.startsWith(`${directory}\\`) ||
220
+ candidate.startsWith(`${directory}/`)
221
+ );
222
+ }
223
+
224
+ async function getStaticFileStat(filePath) {
225
+ try {
226
+ const fileStat = await stat(filePath);
227
+ return fileStat.isFile() ? fileStat : null;
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+
233
+ function sendForbiddenStaticFile(response) {
234
+ response.writeHead(403);
235
+ response.end();
236
+ }
237
+
238
+ function sendNotFound(response) {
239
+ response.writeHead(404);
240
+ response.end('Not found');
241
+ }
242
+
243
+ function isAuthorized(request, url) {
244
+ return (
245
+ request.headers['x-nota-session'] === session || url.searchParams.get('session') === session
246
+ );
247
+ }
248
+
249
+ function readRequestBody(request) {
250
+ return new Promise((resolveBody, rejectBody) => {
251
+ let body = '';
252
+
253
+ request.setEncoding('utf8');
254
+ request.on('data', (chunk) => {
255
+ body += chunk;
256
+
257
+ if (Buffer.byteLength(body, 'utf8') > MAX_BODY_BYTES) {
258
+ rejectBody(new Error('Request body too large'));
259
+ request.destroy();
260
+ }
261
+ });
262
+ request.on('end', () => resolveBody(body));
263
+ request.on('error', rejectBody);
264
+ });
265
+ }
266
+
267
+ function sendJson(response, statusCode, payload) {
268
+ response.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' });
269
+ response.end(JSON.stringify(payload));
270
+ }
271
+
272
+ async function ensureReadableFile(filePath) {
273
+ try {
274
+ const fileStat = await stat(filePath);
275
+ if (!fileStat.isFile()) throw new Error('Not a file');
276
+ await access(filePath);
277
+ } catch {
278
+ console.error(`Cannot read markdown file: ${filePath}`);
279
+ process.exit(1);
280
+ }
281
+ }
282
+
283
+ async function ensureBuildExists(buildDir) {
284
+ try {
285
+ await access(join(buildDir, 'index.html'));
286
+ } catch {
287
+ console.error('Missing dist/index.html. Run `npm run build` before using the CLI from source.');
288
+ process.exit(1);
289
+ }
290
+ }
291
+
292
+ function openBrowser(url) {
293
+ const platform = process.platform;
294
+ const command = platform === 'win32' ? 'cmd' : platform === 'darwin' ? 'open' : 'xdg-open';
295
+ const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
296
+ const child = spawn(command, args, {
297
+ detached: true,
298
+ stdio: 'ignore',
299
+ windowsHide: true,
300
+ });
301
+
302
+ child.unref();
303
+ }
304
+
305
+ function printUsage() {
306
+ console.log(`Usage:
307
+ nota [--no-open] <path/to/plan.md>
308
+ nota init
309
+ nota init [codex|claude|cursor|all]`);
310
+ }
311
+
312
+ async function initAgentInstructions(targets) {
313
+ await mkdir(resolve(process.cwd(), '.agent-plans'), { recursive: true });
314
+ await writeFile(resolve(process.cwd(), '.agent-plans', '.gitkeep'), '', { flag: 'a' });
315
+
316
+ for (const agent of targets) {
317
+ await writeAgentInstructions(agent);
318
+ }
319
+
320
+ console.log(`Plan Annotation instructions installed for: ${targets.join(', ')}`);
321
+ console.log('Plans directory ready: .agent-plans/');
322
+ }
323
+
324
+ function getAgentTargets(target) {
325
+ if (target === 'all') return AGENT_TARGETS;
326
+ if (AGENT_TARGETS.includes(target)) return [target];
327
+
328
+ return null;
329
+ }
330
+
331
+ async function promptForAgentTargets() {
332
+ if (!process.stdin.isTTY) {
333
+ console.error('Interactive init requires a TTY. Use `nota init all` or `nota init <agent>`.');
334
+ process.exit(1);
335
+ }
336
+
337
+ try {
338
+ const { checkbox } = await import('@inquirer/prompts');
339
+
340
+ return await checkbox({
341
+ message: 'Select AI agents to support',
342
+ choices: AGENT_TARGETS.map((agent) => ({
343
+ name: AGENT_LABELS[agent],
344
+ value: agent,
345
+ })),
346
+ });
347
+ } catch (error) {
348
+ if (error?.name === 'ExitPromptError') {
349
+ console.log('\nInitialization cancelled.');
350
+ process.exit(1);
351
+ }
352
+
353
+ throw error;
354
+ }
355
+ }
356
+
357
+ async function writeAgentInstructions(agent) {
358
+ const filePath = resolve(process.cwd(), getAgentInstructionPath(agent));
359
+ const block = getAgentInstructionBlock(agent);
360
+
361
+ await mkdir(dirname(filePath), { recursive: true });
362
+ await upsertMarkedBlock(filePath, block);
363
+ console.log(`Updated ${filePath}`);
364
+ }
365
+
366
+ function getAgentInstructionPath(agent) {
367
+ if (agent === 'codex') return 'AGENTS.md';
368
+ if (agent === 'claude') return 'CLAUDE.md';
369
+
370
+ return join('.cursor', 'rules', 'nota.mdc');
371
+ }
372
+
373
+ async function upsertMarkedBlock(filePath, block) {
374
+ const startMarker = '<!-- nota:start -->';
375
+ const endMarker = '<!-- nota:end -->';
376
+ const existing = await readOptionalFile(filePath);
377
+ const start = existing.indexOf(startMarker);
378
+ const end = existing.indexOf(endMarker);
379
+
380
+ if (start !== -1 && end !== -1 && end > start) {
381
+ const before = existing.slice(0, start).trimEnd();
382
+ const after = existing.slice(end + endMarker.length).trimStart();
383
+ await writeFile(filePath, joinTextBlocks(before, block.trimEnd(), after), 'utf8');
384
+ return;
385
+ }
386
+
387
+ await writeFile(filePath, joinTextBlocks(existing.trimEnd(), block.trimEnd()), 'utf8');
388
+ }
389
+
390
+ async function readOptionalFile(filePath) {
391
+ try {
392
+ return await readFile(filePath, 'utf8');
393
+ } catch {
394
+ return '';
395
+ }
396
+ }
397
+
398
+ function joinTextBlocks(...blocks) {
399
+ return `${blocks.filter(Boolean).join('\n\n')}\n`;
400
+ }