@dinoxx/dinox-cli 1.0.15 → 1.0.17

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 (42) hide show
  1. package/README.md +102 -6
  2. package/dist/cli.js +8 -0
  3. package/dist/commands/graph/index.d.ts +2 -0
  4. package/dist/commands/graph/index.js +113 -0
  5. package/dist/commands/graph/repo.d.ts +40 -0
  6. package/dist/commands/graph/repo.js +126 -0
  7. package/dist/commands/mcp/auth.d.ts +4 -0
  8. package/dist/commands/mcp/auth.js +75 -0
  9. package/dist/commands/mcp/index.d.ts +2 -0
  10. package/dist/commands/mcp/index.js +85 -0
  11. package/dist/commands/mcp/server.d.ts +31 -0
  12. package/dist/commands/mcp/server.js +392 -0
  13. package/dist/commands/mcp/tools.d.ts +2 -0
  14. package/dist/commands/mcp/tools.js +104 -0
  15. package/dist/commands/mcp/types.d.ts +28 -0
  16. package/dist/commands/mcp/types.js +1 -0
  17. package/dist/commands/meta/index.d.ts +2 -0
  18. package/dist/commands/meta/index.js +45 -0
  19. package/dist/commands/meta/repo.d.ts +27 -0
  20. package/dist/commands/meta/repo.js +71 -0
  21. package/dist/commands/notes/index.d.ts +13 -3
  22. package/dist/commands/notes/index.js +135 -6
  23. package/dist/commands/notes/repo.d.ts +26 -0
  24. package/dist/commands/notes/repo.js +99 -0
  25. package/dist/commands/notes/searchOutput.d.ts +6 -0
  26. package/dist/commands/notes/searchOutput.js +41 -0
  27. package/dist/daemon/client.d.ts +2 -0
  28. package/dist/daemon/client.js +86 -0
  29. package/dist/daemon/ensure.d.ts +1 -0
  30. package/dist/daemon/ensure.js +23 -0
  31. package/dist/daemon/index.d.ts +2 -0
  32. package/dist/daemon/index.js +151 -0
  33. package/dist/daemon/pid.d.ts +10 -0
  34. package/dist/daemon/pid.js +55 -0
  35. package/dist/daemon/server.d.ts +10 -0
  36. package/dist/daemon/server.js +140 -0
  37. package/dist/daemon/types.d.ts +19 -0
  38. package/dist/daemon/types.js +1 -0
  39. package/dist/dinox.js +6 -14
  40. package/dist/utils/errors.d.ts +22 -5
  41. package/dist/utils/errors.js +29 -0
  42. package/package.json +1 -1
@@ -1,11 +1,17 @@
1
1
  import { Command } from 'commander';
2
- import { type SearchedNote } from './repo.js';
2
+ import { type NoteSearchField, type NoteSearchResultItem } from './searchOutput.js';
3
3
  type NoteSearchResponse = {
4
4
  meta: {
5
5
  query: string;
6
6
  returned_count: number;
7
7
  total_count: number;
8
8
  stale: boolean;
9
+ pagination: {
10
+ limit: number | null;
11
+ offset: number;
12
+ has_more: boolean;
13
+ };
14
+ fields: NoteSearchField[] | null;
9
15
  applied_filters: {
10
16
  include_deleted: boolean;
11
17
  tags_expression: string | null;
@@ -20,13 +26,17 @@ type NoteSearchResponse = {
20
26
  sql_expression: string | null;
21
27
  };
22
28
  };
23
- notes: SearchedNote[];
29
+ notes: NoteSearchResultItem[];
24
30
  };
25
31
  type BuildNoteSearchResponseInput = {
26
32
  query: string;
27
- notes: SearchedNote[];
33
+ notes: NoteSearchResultItem[];
34
+ totalCount: number;
28
35
  stale: boolean;
29
36
  includeDeleted: boolean;
37
+ limit?: number;
38
+ offset?: number;
39
+ fields?: NoteSearchField[];
30
40
  tagsExpression?: string;
31
41
  createdAtFrom?: string;
32
42
  createdAtTo?: string;
@@ -9,8 +9,11 @@ import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
9
9
  import { resolveAuthIdentity } from '../../auth/userInfo.js';
10
10
  import { connectPowerSync, waitForIdleDataFlow } from '../../powersync/runtime.js';
11
11
  import { syncNoteTokenIndex } from '../../powersync/tokenIndex.js';
12
- import { createNote, getNote, getNoteDetails, assertNotesUpdatable, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, resolveZettelBoxIdsForUpdate, searchNotes, softDeleteNote, updateNote, validateTagsForCreate, validateTagsForUpdate, } from './repo.js';
12
+ import { createNote, getNote, getNoteContext, getNoteDetails, getNotePreview, assertNotesUpdatable, resolveZettelBoxIdsForCreate, resolveZettelBoxIdsForSearch, resolveZettelBoxIdsForUpdate, searchNotes, softDeleteNote, updateNote, validateTagsForCreate, validateTagsForUpdate, } from './repo.js';
13
+ import { parseNoteSearchFields, projectNoteSearchFields, } from './searchOutput.js';
13
14
  import { parseCreatedAtRange } from './searchTime.js';
15
+ const DEFAULT_NOTE_SEARCH_LIMIT = 50;
16
+ const MAX_NOTE_SEARCH_LIMIT = 500;
14
17
  function parseNoteType(value) {
15
18
  const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
16
19
  if (normalized === 'note' || normalized === 'crawl') {
@@ -18,6 +21,63 @@ function parseNoteType(value) {
18
21
  }
19
22
  throw new DinoxError('--type must be one of: note, crawl');
20
23
  }
24
+ function parseSearchLimit(value) {
25
+ if (value === undefined || value === null || value === '') {
26
+ return DEFAULT_NOTE_SEARCH_LIMIT;
27
+ }
28
+ if (typeof value !== 'string') {
29
+ throw new DinoxError('--limit must be a positive integer');
30
+ }
31
+ const trimmed = value.trim();
32
+ if (!/^\d+$/.test(trimmed)) {
33
+ throw new DinoxError('--limit must be a positive integer');
34
+ }
35
+ const parsed = Number(trimmed);
36
+ if (!Number.isFinite(parsed) || parsed <= 0) {
37
+ throw new DinoxError('--limit must be a positive integer');
38
+ }
39
+ if (parsed > MAX_NOTE_SEARCH_LIMIT) {
40
+ throw new DinoxError(`--limit cannot be greater than ${MAX_NOTE_SEARCH_LIMIT}`);
41
+ }
42
+ return Math.trunc(parsed);
43
+ }
44
+ function parseSearchOffset(value) {
45
+ if (value === undefined || value === null || value === '') {
46
+ return 0;
47
+ }
48
+ if (typeof value !== 'string') {
49
+ throw new DinoxError('--offset must be a non-negative integer');
50
+ }
51
+ const trimmed = value.trim();
52
+ if (!/^\d+$/.test(trimmed)) {
53
+ throw new DinoxError('--offset must be a non-negative integer');
54
+ }
55
+ const parsed = Number(trimmed);
56
+ if (!Number.isFinite(parsed) || parsed < 0) {
57
+ throw new DinoxError('--offset must be a non-negative integer');
58
+ }
59
+ return Math.trunc(parsed);
60
+ }
61
+ function parsePreviewLineCount(value) {
62
+ if (value === undefined || value === null || value === '') {
63
+ return 10;
64
+ }
65
+ if (typeof value !== 'string') {
66
+ throw new DinoxError('--lines must be a positive integer');
67
+ }
68
+ const trimmed = value.trim();
69
+ if (!/^\d+$/.test(trimmed)) {
70
+ throw new DinoxError('--lines must be a positive integer');
71
+ }
72
+ const parsed = Number(trimmed);
73
+ if (!Number.isFinite(parsed) || parsed <= 0) {
74
+ throw new DinoxError('--lines must be a positive integer');
75
+ }
76
+ if (parsed > 500) {
77
+ throw new DinoxError('--lines cannot be greater than 500');
78
+ }
79
+ return Math.trunc(parsed);
80
+ }
21
81
  async function parseStringInput(value) {
22
82
  return readStringOrFile(value);
23
83
  }
@@ -115,12 +175,23 @@ export function buildNoteSearchResponse(input) {
115
175
  const tagsExpression = input.tagsExpression?.trim() ?? '';
116
176
  const sqlExpression = input.sqlExpression?.trim() ?? '';
117
177
  const hasCreatedAtRange = Boolean(input.createdAtFrom || input.createdAtTo);
178
+ const offset = input.offset ?? 0;
179
+ const limit = input.limit ?? null;
180
+ const returnedCount = input.notes.length;
181
+ const totalCount = input.totalCount;
182
+ const hasMore = offset + returnedCount < totalCount;
118
183
  return {
119
184
  meta: {
120
185
  query,
121
- returned_count: input.notes.length,
122
- total_count: input.notes.length,
186
+ returned_count: returnedCount,
187
+ total_count: totalCount,
123
188
  stale: input.stale,
189
+ pagination: {
190
+ limit,
191
+ offset,
192
+ has_more: hasMore,
193
+ },
194
+ fields: input.fields && input.fields.length > 0 ? [...input.fields] : null,
124
195
  applied_filters: {
125
196
  include_deleted: input.includeDeleted,
126
197
  tags_expression: tagsExpression || null,
@@ -152,6 +223,9 @@ export function registerNoteCommands(program) {
152
223
  .option('--days <n>', 'recent N days by created_at (cannot be used with --from/--to)')
153
224
  .option('--box <string|@file>', 'zettel box names (JSON array/comma list), or [] for empty boxes')
154
225
  .option('--sql <expr>', 'SQL-like expression over id/content_md/summary/tags/zettel_boxes/created_at/type')
226
+ .option('--limit <n>', `maximum returned notes (default: ${DEFAULT_NOTE_SEARCH_LIMIT}, max: ${MAX_NOTE_SEARCH_LIMIT})`)
227
+ .option('--offset <n>', 'result offset for pagination (default: 0)')
228
+ .option('--fields <list>', 'comma-separated fields: id,title,summary,tags,created_at,zettel_boxes')
155
229
  .option('--include-deleted', 'include soft-deleted notes')
156
230
  .action(async (query, options, command) => {
157
231
  const globals = command.optsWithGlobals?.() ?? {};
@@ -174,6 +248,9 @@ export function registerNoteCommands(program) {
174
248
  : { values: [], isExplicitEmptyArray: false };
175
249
  const zettelBoxIds = await resolveZettelBoxIdsForSearch(db, parsedBoxInput.values);
176
250
  const sqlExpression = typeof options.sql === 'string' ? options.sql.trim() : '';
251
+ const limit = parseSearchLimit(options.limit);
252
+ const offset = parseSearchOffset(options.offset);
253
+ const fields = parseNoteSearchFields(options.fields, '--fields');
177
254
  const rows = await searchNotes(db, keyword, {
178
255
  includeDeleted: Boolean(options.includeDeleted),
179
256
  tagsExpression: tagsExpression || undefined,
@@ -183,11 +260,18 @@ export function registerNoteCommands(program) {
183
260
  matchEmptyZettelBoxes: parsedBoxInput.isExplicitEmptyArray,
184
261
  sqlExpression: sqlExpression || undefined,
185
262
  });
263
+ const totalCount = rows.length;
264
+ const pagedRows = rows.slice(offset, offset + limit);
265
+ const projectedRows = projectNoteSearchFields(pagedRows, fields);
186
266
  const response = buildNoteSearchResponse({
187
267
  query: keyword,
188
- notes: rows,
268
+ notes: projectedRows,
269
+ totalCount,
189
270
  stale: offline ? true : stale,
190
271
  includeDeleted: Boolean(options.includeDeleted),
272
+ limit,
273
+ offset,
274
+ fields,
191
275
  tagsExpression: tagsExpression || undefined,
192
276
  createdAtFrom,
193
277
  createdAtTo,
@@ -207,7 +291,8 @@ export function registerNoteCommands(program) {
207
291
  .command('get')
208
292
  .description('Get note by id')
209
293
  .argument('<id>', 'note id')
210
- .action(async (id, _options, command) => {
294
+ .option('--context-only', 'return lightweight note context (title/tags/summary/links)')
295
+ .action(async (id, options, command) => {
211
296
  const globals = command.optsWithGlobals?.() ?? {};
212
297
  const offline = Boolean(globals.offline);
213
298
  const jsonOutput = Boolean(globals.json);
@@ -216,9 +301,25 @@ export function registerNoteCommands(program) {
216
301
  const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
217
302
  try {
218
303
  await syncBeforeNoteCommand(db, offline, timeoutMs);
304
+ if (options.contextOnly) {
305
+ const context = await getNoteContext(db, id);
306
+ if (!context) {
307
+ throw new DinoxError(`Note not found: ${id}`, {
308
+ code: 'NOTE_NOT_FOUND',
309
+ suggestion: "Run 'dino note search <keyword>' to locate the note id.",
310
+ recoverable: true,
311
+ });
312
+ }
313
+ printYaml({ stale: offline ? true : stale, note: context });
314
+ return;
315
+ }
219
316
  const note = await getNote(db, id);
220
317
  if (!note) {
221
- throw new DinoxError(`Note not found: ${id}`);
318
+ throw new DinoxError(`Note not found: ${id}`, {
319
+ code: 'NOTE_NOT_FOUND',
320
+ suggestion: "Run 'dino note search <keyword>' to locate the note id.",
321
+ recoverable: true,
322
+ });
222
323
  }
223
324
  if (jsonOutput) {
224
325
  printYaml({ stale, note });
@@ -240,6 +341,34 @@ export function registerNoteCommands(program) {
240
341
  await db.close().catch(() => undefined);
241
342
  }
242
343
  });
344
+ note
345
+ .command('preview')
346
+ .description('Preview the first N lines of note markdown')
347
+ .argument('<id>', 'note id')
348
+ .option('--lines <n>', 'number of lines to return (default: 10)')
349
+ .action(async (id, options, command) => {
350
+ const globals = command.optsWithGlobals?.() ?? {};
351
+ const offline = Boolean(globals.offline);
352
+ const config = resolveConfig(await loadConfig());
353
+ const timeoutMs = parseSyncTimeoutMs(globals.syncTimeout) ?? config.sync.timeoutMs;
354
+ const lines = parsePreviewLineCount(options.lines);
355
+ const { db, stale } = await connectPowerSync({ config, offline, timeoutMs });
356
+ try {
357
+ await syncBeforeNoteCommand(db, offline, timeoutMs);
358
+ const preview = await getNotePreview(db, id, lines);
359
+ if (!preview) {
360
+ throw new DinoxError(`Note not found: ${id}`, {
361
+ code: 'NOTE_NOT_FOUND',
362
+ suggestion: "Run 'dino note search <keyword>' to locate the note id.",
363
+ recoverable: true,
364
+ });
365
+ }
366
+ printYaml({ stale: offline ? true : stale, note: preview });
367
+ }
368
+ finally {
369
+ await db.close().catch(() => undefined);
370
+ }
371
+ });
243
372
  note
244
373
  .command('detail')
245
374
  .description('Get full note details by id')
@@ -41,6 +41,30 @@ export type NoteDetail = {
41
41
  content_md: string | null;
42
42
  zettel_boxes: string[];
43
43
  };
44
+ export type NoteContext = {
45
+ id: string;
46
+ title: string;
47
+ summary: string;
48
+ tags: string[];
49
+ created_at: string | null;
50
+ updated_at: string | null;
51
+ is_del: number | null;
52
+ zettel_boxes: string[];
53
+ links: {
54
+ backlinks: number;
55
+ outlinks: number;
56
+ };
57
+ };
58
+ export type NotePreview = {
59
+ id: string;
60
+ title: string;
61
+ requested_lines: number;
62
+ total_lines: number;
63
+ returned_lines: number;
64
+ truncated: boolean;
65
+ preview: string;
66
+ updated_at: string | null;
67
+ };
44
68
  export type UpdateNoteResult = {
45
69
  id: string;
46
70
  updated: boolean;
@@ -49,6 +73,8 @@ export type UpdateNoteResult = {
49
73
  };
50
74
  export declare function searchNotes(db: AbstractPowerSyncDatabase, query: string, options: SearchNotesOptions): Promise<SearchedNote[]>;
51
75
  export declare function getNote(db: AbstractPowerSyncDatabase, id: string): Promise<NoteRow | null>;
76
+ export declare function getNoteContext(db: AbstractPowerSyncDatabase, id: string): Promise<NoteContext | null>;
77
+ export declare function getNotePreview(db: AbstractPowerSyncDatabase, id: string, requestedLines: number): Promise<NotePreview | null>;
52
78
  export declare function getNoteDetails(db: AbstractPowerSyncDatabase, ids: string[]): Promise<NoteDetail[]>;
53
79
  export declare function getNoteDetail(db: AbstractPowerSyncDatabase, id: string): Promise<NoteDetail | null>;
54
80
  export declare function createNote(db: AbstractPowerSyncDatabase, input: {
@@ -495,6 +495,105 @@ export async function getNote(db, id) {
495
495
  WHERE id = ?
496
496
  `, [id]);
497
497
  }
498
+ async function getLinkCount(db, direction, noteId) {
499
+ const [column, boundColumn] = direction === 'in'
500
+ ? ['src_note_id', 'dst_note_id']
501
+ : ['dst_note_id', 'src_note_id'];
502
+ const row = await db.getOptional(`
503
+ SELECT COUNT(1) AS count
504
+ FROM c_note_link l
505
+ JOIN c_note n ON n.id = l.${column}
506
+ WHERE l.${boundColumn} = ?
507
+ AND l.is_del = 0
508
+ AND (n.is_del IS NULL OR n.is_del = 0)
509
+ `, [noteId]);
510
+ return Number(row?.count ?? 0);
511
+ }
512
+ function buildPreviewText(contentMd, requestedLines) {
513
+ const normalized = (contentMd ?? '').replace(/\r\n/g, '\n');
514
+ if (!normalized) {
515
+ return {
516
+ preview: '',
517
+ totalLines: 0,
518
+ returnedLines: 0,
519
+ truncated: false,
520
+ };
521
+ }
522
+ const lines = normalized.split('\n');
523
+ const selected = lines.slice(0, requestedLines);
524
+ return {
525
+ preview: selected.join('\n'),
526
+ totalLines: lines.length,
527
+ returnedLines: selected.length,
528
+ truncated: selected.length < lines.length,
529
+ };
530
+ }
531
+ export async function getNoteContext(db, id) {
532
+ const row = await db.getOptional(`
533
+ SELECT
534
+ n.id,
535
+ n.title,
536
+ n.summary,
537
+ n.content_text,
538
+ n.tags,
539
+ n.created_at,
540
+ n.updated_at,
541
+ n.is_del,
542
+ n.zettel_boxes AS zettel_boxes_raw
543
+ FROM c_note n
544
+ WHERE n.id = ?
545
+ `, [id]);
546
+ if (!row) {
547
+ return null;
548
+ }
549
+ const boxIds = parseZettelBoxIds(row.zettel_boxes_raw);
550
+ const boxNameMap = await loadZettelBoxNameMap(db, boxIds);
551
+ const [backlinks, outlinks] = await Promise.all([
552
+ getLinkCount(db, 'in', id),
553
+ getLinkCount(db, 'out', id),
554
+ ]);
555
+ return {
556
+ id: row.id,
557
+ title: row.title?.trim() ?? '',
558
+ summary: resolveSummary(row.summary, row.content_text),
559
+ tags: parseStoredTags(row.tags),
560
+ created_at: row.created_at,
561
+ updated_at: row.updated_at,
562
+ is_del: row.is_del,
563
+ zettel_boxes: boxIds
564
+ .map((boxId) => boxNameMap.get(boxId))
565
+ .filter((name) => typeof name === 'string' && name.length > 0),
566
+ links: {
567
+ backlinks,
568
+ outlinks,
569
+ },
570
+ };
571
+ }
572
+ export async function getNotePreview(db, id, requestedLines) {
573
+ const row = await db.getOptional(`
574
+ SELECT
575
+ n.id,
576
+ n.title,
577
+ n.content_md,
578
+ n.updated_at
579
+ FROM c_note n
580
+ WHERE n.id = ?
581
+ `, [id]);
582
+ if (!row) {
583
+ return null;
584
+ }
585
+ const preview = buildPreviewText(row.content_md, requestedLines);
586
+ return {
587
+ id: row.id,
588
+ title: row.title?.trim() ?? '',
589
+ requested_lines: requestedLines,
590
+ total_lines: preview.totalLines,
591
+ returned_lines: preview.returnedLines,
592
+ truncated: preview.truncated,
593
+ preview: preview.preview,
594
+ updated_at: row.updated_at ?? null,
595
+ };
596
+ }
498
597
  function mapNoteDetailRow(note, boxNameMap) {
499
598
  const boxIds = parseZettelBoxIds(note.zettel_boxes_raw);
500
599
  const zettelBoxes = boxIds
@@ -0,0 +1,6 @@
1
+ import type { SearchedNote } from './repo.js';
2
+ export declare const NOTE_SEARCH_FIELDS: readonly ["id", "title", "summary", "tags", "created_at", "zettel_boxes"];
3
+ export type NoteSearchField = (typeof NOTE_SEARCH_FIELDS)[number];
4
+ export type NoteSearchResultItem = Partial<SearchedNote>;
5
+ export declare function parseNoteSearchFields(value: unknown, optionName: string): NoteSearchField[] | undefined;
6
+ export declare function projectNoteSearchFields(notes: SearchedNote[], fields: NoteSearchField[] | undefined): NoteSearchResultItem[];
@@ -0,0 +1,41 @@
1
+ import { DinoxError } from '../../utils/errors.js';
2
+ export const NOTE_SEARCH_FIELDS = ['id', 'title', 'summary', 'tags', 'created_at', 'zettel_boxes'];
3
+ const NOTE_SEARCH_FIELD_SET = new Set(NOTE_SEARCH_FIELDS);
4
+ export function parseNoteSearchFields(value, optionName) {
5
+ if (value === undefined || value === null || value === '') {
6
+ return undefined;
7
+ }
8
+ let rawFields = [];
9
+ if (Array.isArray(value)) {
10
+ rawFields = value.map((item) => String(item).trim());
11
+ }
12
+ else if (typeof value === 'string') {
13
+ rawFields = value
14
+ .split(/[\n,,]/g)
15
+ .map((item) => item.trim());
16
+ }
17
+ else {
18
+ throw new DinoxError(`${optionName} must be a comma-separated string or string array`);
19
+ }
20
+ const normalized = Array.from(new Set(rawFields.filter(Boolean)));
21
+ if (normalized.length === 0) {
22
+ throw new DinoxError(`${optionName} must include at least one field`);
23
+ }
24
+ const invalidField = normalized.find((field) => !NOTE_SEARCH_FIELD_SET.has(field));
25
+ if (invalidField) {
26
+ throw new DinoxError(`${optionName} has unsupported field: ${invalidField}. Allowed: ${NOTE_SEARCH_FIELDS.join(', ')}`);
27
+ }
28
+ return normalized;
29
+ }
30
+ export function projectNoteSearchFields(notes, fields) {
31
+ if (!fields || fields.length === 0) {
32
+ return notes.map((note) => ({ ...note }));
33
+ }
34
+ return notes.map((note) => {
35
+ const item = {};
36
+ for (const field of fields) {
37
+ item[field] = note[field];
38
+ }
39
+ return item;
40
+ });
41
+ }
@@ -0,0 +1,2 @@
1
+ export declare function sendDaemonRequest(method: string, params?: unknown): Promise<unknown>;
2
+ export declare function isDaemonRunning(): Promise<boolean>;
@@ -0,0 +1,86 @@
1
+ import { request as httpRequest } from 'node:http';
2
+ import { DEFAULT_DAEMON_PORT } from './types.js';
3
+ import { readPid } from './pid.js';
4
+ const DEFAULT_DAEMON_REQUEST_TIMEOUT_MS = 2_000;
5
+ const MAX_DAEMON_RESPONSE_BYTES = 5 * 1024 * 1024;
6
+ export async function sendDaemonRequest(method, params) {
7
+ const pidInfo = readPid();
8
+ const port = pidInfo?.port || DEFAULT_DAEMON_PORT;
9
+ const request = {
10
+ id: Date.now().toString(),
11
+ method,
12
+ params,
13
+ };
14
+ return new Promise((resolve, reject) => {
15
+ let settled = false;
16
+ const finalize = (error, value) => {
17
+ if (settled)
18
+ return;
19
+ settled = true;
20
+ if (error) {
21
+ reject(error);
22
+ return;
23
+ }
24
+ resolve(value);
25
+ };
26
+ const data = JSON.stringify(request);
27
+ const options = {
28
+ hostname: '127.0.0.1',
29
+ port,
30
+ path: '/',
31
+ method: 'POST',
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ 'Content-Length': Buffer.byteLength(data),
35
+ },
36
+ timeout: DEFAULT_DAEMON_REQUEST_TIMEOUT_MS,
37
+ };
38
+ const req = httpRequest(options, (res) => {
39
+ const statusCode = res.statusCode ?? 0;
40
+ const chunks = [];
41
+ let totalBytes = 0;
42
+ res.on('data', (chunk) => {
43
+ totalBytes += chunk.length;
44
+ if (totalBytes > MAX_DAEMON_RESPONSE_BYTES) {
45
+ req.destroy(new Error('Daemon response too large'));
46
+ return;
47
+ }
48
+ chunks.push(chunk);
49
+ });
50
+ res.on('end', () => {
51
+ try {
52
+ const body = Buffer.concat(chunks).toString('utf-8');
53
+ if (statusCode < 200 || statusCode >= 300) {
54
+ finalize(new Error(`Daemon request failed (HTTP ${statusCode || 'unknown'}): ${body || '(empty)'}`));
55
+ return;
56
+ }
57
+ const response = JSON.parse(body);
58
+ if (response.ok) {
59
+ finalize(undefined, response.result);
60
+ }
61
+ else {
62
+ finalize(new Error(response.error || 'Unknown error'));
63
+ }
64
+ }
65
+ catch (error) {
66
+ finalize(error);
67
+ }
68
+ });
69
+ });
70
+ req.on('timeout', () => {
71
+ req.destroy(new Error('Daemon request timed out'));
72
+ });
73
+ req.on('error', (error) => finalize(error));
74
+ req.write(data);
75
+ req.end();
76
+ });
77
+ }
78
+ export async function isDaemonRunning() {
79
+ try {
80
+ await sendDaemonRequest('ping');
81
+ return true;
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
@@ -0,0 +1 @@
1
+ export declare function ensureDaemonRunning(): Promise<void>;
@@ -0,0 +1,23 @@
1
+ import { spawn } from 'child_process';
2
+ import { isDaemonRunning } from './client.js';
3
+ import { DEFAULT_DAEMON_PORT } from './types.js';
4
+ export async function ensureDaemonRunning() {
5
+ if (await isDaemonRunning()) {
6
+ return;
7
+ }
8
+ console.log('Starting daemon...');
9
+ const args = ['daemon', 'start', '--port', String(DEFAULT_DAEMON_PORT), '--no-detach'];
10
+ const child = spawn(process.argv[0], [process.argv[1], ...args], {
11
+ detached: true,
12
+ stdio: 'ignore',
13
+ });
14
+ child.unref();
15
+ for (let i = 0; i < 10; i++) {
16
+ await new Promise((resolve) => setTimeout(resolve, 500));
17
+ if (await isDaemonRunning()) {
18
+ console.log('Daemon started');
19
+ return;
20
+ }
21
+ }
22
+ throw new Error('Failed to start daemon');
23
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerDaemonCommands(program: Command): void;