@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
@@ -0,0 +1,151 @@
1
+ import { spawn } from 'child_process';
2
+ import { DaemonServer } from './server.js';
3
+ import { DEFAULT_DAEMON_PORT } from './types.js';
4
+ import { readPid, isProcessRunning, removePid } from './pid.js';
5
+ import { isDaemonRunning } from './client.js';
6
+ import { getPackageVersion } from '../utils/version.js';
7
+ import { DinoxError } from '../utils/errors.js';
8
+ const DAEMON_STOP_POLL_INTERVAL_MS = 100;
9
+ const DAEMON_STOP_TIMEOUT_MS = 5_000;
10
+ function parsePort(value) {
11
+ if (value === undefined || value === null || value === '') {
12
+ return DEFAULT_DAEMON_PORT;
13
+ }
14
+ if (typeof value !== 'string') {
15
+ throw new DinoxError('--port must be a valid port number');
16
+ }
17
+ const trimmed = value.trim();
18
+ if (!/^\d+$/.test(trimmed)) {
19
+ throw new DinoxError('--port must be a valid port number');
20
+ }
21
+ const parsed = Number(trimmed);
22
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
23
+ throw new DinoxError('--port must be between 1 and 65535');
24
+ }
25
+ return Math.trunc(parsed);
26
+ }
27
+ async function waitForProcessExit(pid, timeoutMs) {
28
+ const deadline = Date.now() + timeoutMs;
29
+ while (Date.now() < deadline) {
30
+ if (!isProcessRunning(pid)) {
31
+ return true;
32
+ }
33
+ await new Promise((resolve) => setTimeout(resolve, DAEMON_STOP_POLL_INTERVAL_MS));
34
+ }
35
+ return !isProcessRunning(pid);
36
+ }
37
+ async function startDaemon(port, detached = true) {
38
+ if (await isDaemonRunning()) {
39
+ console.log('Daemon is already running');
40
+ return;
41
+ }
42
+ if (detached) {
43
+ const args = ['daemon', 'start', '--port', String(port), '--no-detach'];
44
+ const child = spawn(process.argv[0], [process.argv[1], ...args], {
45
+ detached: true,
46
+ stdio: 'ignore',
47
+ });
48
+ child.unref();
49
+ await new Promise((resolve) => setTimeout(resolve, 1000));
50
+ if (await isDaemonRunning()) {
51
+ console.log(`Daemon started on port ${port}`);
52
+ }
53
+ else {
54
+ console.error('Failed to start daemon');
55
+ }
56
+ }
57
+ else {
58
+ console.log(`Starting daemon on port ${port}...`);
59
+ const server = new DaemonServer();
60
+ await server.initialize();
61
+ await server.start(port);
62
+ console.log(`Daemon running on port ${port} (PID: ${process.pid})`);
63
+ process.on('SIGINT', async () => {
64
+ console.log('\nStopping daemon...');
65
+ await server.stop();
66
+ process.exit(0);
67
+ });
68
+ process.on('SIGTERM', async () => {
69
+ await server.stop();
70
+ process.exit(0);
71
+ });
72
+ }
73
+ }
74
+ async function stopDaemon() {
75
+ const pidInfo = readPid();
76
+ if (!pidInfo) {
77
+ console.log('Daemon is not running');
78
+ return;
79
+ }
80
+ if (!isProcessRunning(pidInfo.pid)) {
81
+ console.log('Daemon is not running (stale PID file)');
82
+ removePid();
83
+ return;
84
+ }
85
+ try {
86
+ process.kill(pidInfo.pid, 'SIGTERM');
87
+ }
88
+ catch (error) {
89
+ throw new DinoxError(`Failed to stop daemon: ${error.message}`);
90
+ }
91
+ const stopped = await waitForProcessExit(pidInfo.pid, DAEMON_STOP_TIMEOUT_MS);
92
+ if (!stopped) {
93
+ throw new DinoxError(`Daemon did not stop within ${DAEMON_STOP_TIMEOUT_MS}ms. Try stopping it manually and rerun \`dino daemon status\`.`);
94
+ }
95
+ removePid();
96
+ console.log('Daemon stopped');
97
+ }
98
+ async function daemonStatus() {
99
+ const pidInfo = readPid();
100
+ if (!pidInfo) {
101
+ console.log('Status: Not running');
102
+ return;
103
+ }
104
+ const running = await isDaemonRunning();
105
+ if (running) {
106
+ console.log('Status: Running');
107
+ console.log(`PID: ${pidInfo.pid}`);
108
+ console.log(`Port: ${pidInfo.port}`);
109
+ console.log(`Version: ${getPackageVersion()}`);
110
+ }
111
+ else {
112
+ console.log('Status: Not running (stale PID file)');
113
+ removePid();
114
+ }
115
+ }
116
+ export function registerDaemonCommands(program) {
117
+ const daemon = program.command('daemon').description('Manage daemon process');
118
+ daemon
119
+ .command('start')
120
+ .description('Start daemon process')
121
+ .option('--port <number>', 'daemon port', String(DEFAULT_DAEMON_PORT))
122
+ .option('--no-detach', 'run in foreground (for debugging)')
123
+ .action(async (options) => {
124
+ const port = parsePort(options.port);
125
+ await startDaemon(port, options.detach !== false);
126
+ });
127
+ daemon
128
+ .command('stop')
129
+ .description('Stop daemon process')
130
+ .action(async () => {
131
+ await stopDaemon();
132
+ });
133
+ daemon
134
+ .command('status')
135
+ .description('Show daemon status')
136
+ .action(async () => {
137
+ await daemonStatus();
138
+ });
139
+ daemon
140
+ .command('restart')
141
+ .description('Restart daemon process')
142
+ .option('--port <number>', 'daemon port', String(DEFAULT_DAEMON_PORT))
143
+ .action(async (options) => {
144
+ await stopDaemon();
145
+ await new Promise((resolve) => setTimeout(resolve, 1000));
146
+ await startDaemon(parsePort(options.port));
147
+ });
148
+ daemon.action((_options, command) => {
149
+ command.outputHelp();
150
+ });
151
+ }
@@ -0,0 +1,10 @@
1
+ export declare function getDaemonDir(): string;
2
+ export declare function getPidFilePath(): string;
3
+ export declare function getPortFilePath(): string;
4
+ export declare function savePid(pid: number, port: number): void;
5
+ export declare function readPid(): {
6
+ pid: number;
7
+ port: number;
8
+ } | null;
9
+ export declare function removePid(): void;
10
+ export declare function isProcessRunning(pid: number): boolean;
@@ -0,0 +1,55 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
4
+ export function getDaemonDir() {
5
+ return join(homedir(), '.dinox', 'daemon');
6
+ }
7
+ export function getPidFilePath() {
8
+ return join(getDaemonDir(), 'daemon.pid');
9
+ }
10
+ export function getPortFilePath() {
11
+ return join(getDaemonDir(), 'daemon.port');
12
+ }
13
+ export function savePid(pid, port) {
14
+ const dir = getDaemonDir();
15
+ if (!existsSync(dir)) {
16
+ mkdirSync(dir, { recursive: true });
17
+ }
18
+ writeFileSync(getPidFilePath(), String(pid));
19
+ writeFileSync(getPortFilePath(), String(port));
20
+ }
21
+ export function readPid() {
22
+ try {
23
+ const pid = parseInt(readFileSync(getPidFilePath(), 'utf-8').trim());
24
+ const port = parseInt(readFileSync(getPortFilePath(), 'utf-8').trim());
25
+ if (!Number.isFinite(pid) || pid <= 0) {
26
+ return null;
27
+ }
28
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
29
+ return null;
30
+ }
31
+ return { pid, port };
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ export function removePid() {
38
+ for (const path of [getPidFilePath(), getPortFilePath()]) {
39
+ try {
40
+ unlinkSync(path);
41
+ }
42
+ catch {
43
+ // Ignore missing files/stale paths.
44
+ }
45
+ }
46
+ }
47
+ export function isProcessRunning(pid) {
48
+ try {
49
+ process.kill(pid, 0);
50
+ return true;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
@@ -0,0 +1,10 @@
1
+ import type { DaemonRequest, DaemonResponse } from './types.js';
2
+ export declare class DaemonServer {
3
+ private db;
4
+ private server;
5
+ private startTime;
6
+ initialize(): Promise<void>;
7
+ handleRequest(req: DaemonRequest): Promise<DaemonResponse>;
8
+ start(port: number): Promise<void>;
9
+ stop(): Promise<void>;
10
+ }
@@ -0,0 +1,140 @@
1
+ import { createServer } from 'http';
2
+ import { loadConfig } from '../config/store.js';
3
+ import { resolveConfig } from '../config/resolve.js';
4
+ import { connectPowerSync, waitForIdleDataFlow } from '../powersync/runtime.js';
5
+ import { searchNotes, getNoteDetails } from '../commands/notes/repo.js';
6
+ import { getBacklinks, getGraphStats, getOutlinks, getRelatedNotes } from '../commands/graph/repo.js';
7
+ import { searchTodoTasks } from '../commands/todo/repo.js';
8
+ import { savePid, removePid } from './pid.js';
9
+ import { getPackageVersion } from '../utils/version.js';
10
+ const MAX_DAEMON_REQUEST_BYTES = 1 * 1024 * 1024;
11
+ async function readJsonBody(req) {
12
+ return new Promise((resolve, reject) => {
13
+ const chunks = [];
14
+ let totalBytes = 0;
15
+ req.on('data', (chunk) => {
16
+ totalBytes += chunk.length;
17
+ if (totalBytes > MAX_DAEMON_REQUEST_BYTES) {
18
+ req.destroy(new Error('Request body too large'));
19
+ return;
20
+ }
21
+ chunks.push(chunk);
22
+ });
23
+ req.on('end', () => {
24
+ try {
25
+ const body = Buffer.concat(chunks).toString('utf-8');
26
+ resolve(JSON.parse(body));
27
+ }
28
+ catch (error) {
29
+ reject(error);
30
+ }
31
+ });
32
+ req.on('error', reject);
33
+ });
34
+ }
35
+ export class DaemonServer {
36
+ db = null;
37
+ server = null;
38
+ startTime = Date.now();
39
+ async initialize() {
40
+ const config = resolveConfig(await loadConfig());
41
+ const { db } = await connectPowerSync({ config, offline: false, timeoutMs: config.sync.timeoutMs });
42
+ this.db = db;
43
+ setInterval(async () => {
44
+ if (this.db) {
45
+ await waitForIdleDataFlow(this.db, 5000).catch(() => { });
46
+ }
47
+ }, 30000);
48
+ }
49
+ async handleRequest(req) {
50
+ try {
51
+ if (!this.db) {
52
+ throw new Error('Database not initialized');
53
+ }
54
+ const { method, params } = req;
55
+ switch (method) {
56
+ case 'ping':
57
+ return { id: req.id, ok: true, result: { uptime: Date.now() - this.startTime, version: getPackageVersion() } };
58
+ case 'note.search':
59
+ const notes = await searchNotes(this.db, params.query || '', {
60
+ includeDeleted: params.includeDeleted || false,
61
+ tagsExpression: params.tags,
62
+ createdAtFrom: params.from,
63
+ createdAtTo: params.to,
64
+ });
65
+ return { id: req.id, ok: true, result: notes };
66
+ case 'note.get':
67
+ const noteDetails = await getNoteDetails(this.db, [params.id]);
68
+ return { id: req.id, ok: true, result: noteDetails[0] || null };
69
+ case 'graph.backlinks':
70
+ const backlinks = await getBacklinks(this.db, params.id);
71
+ return { id: req.id, ok: true, result: backlinks };
72
+ case 'graph.outlinks':
73
+ const outlinks = await getOutlinks(this.db, params.id);
74
+ return { id: req.id, ok: true, result: outlinks };
75
+ case 'graph.related':
76
+ const related = await getRelatedNotes(this.db, params.id, params.depth || 2);
77
+ return { id: req.id, ok: true, result: related };
78
+ case 'graph.stats':
79
+ const graphStats = await getGraphStats(this.db);
80
+ return { id: req.id, ok: true, result: graphStats };
81
+ case 'todo.search':
82
+ const todos = await searchTodoTasks(this.db, params.query || '', {
83
+ includeDeleted: false,
84
+ taskStatus: params.status || 'all',
85
+ taskTagFilter: params.tags || [],
86
+ limit: params.limit || 50,
87
+ scanLimit: params.scanLimit || 200,
88
+ });
89
+ return { id: req.id, ok: true, result: todos };
90
+ default:
91
+ throw new Error(`Unknown method: ${method}`);
92
+ }
93
+ }
94
+ catch (error) {
95
+ return { id: req.id, ok: false, error: error.message };
96
+ }
97
+ }
98
+ async start(port) {
99
+ this.server = createServer(async (req, res) => {
100
+ if (req.method === 'POST') {
101
+ try {
102
+ const daemonReq = await readJsonBody(req);
103
+ const daemonRes = await this.handleRequest(daemonReq);
104
+ res.writeHead(200, { 'Content-Type': 'application/json' });
105
+ res.end(JSON.stringify(daemonRes));
106
+ }
107
+ catch (error) {
108
+ const message = error instanceof Error ? error.message : String(error);
109
+ const statusCode = message.includes('too large') ? 413 : 400;
110
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
111
+ res.end(JSON.stringify({ ok: false, error: message }));
112
+ }
113
+ }
114
+ else {
115
+ res.writeHead(405);
116
+ res.end();
117
+ }
118
+ });
119
+ this.server.headersTimeout = 5_000;
120
+ this.server.requestTimeout = 10_000;
121
+ await new Promise((resolve, reject) => {
122
+ if (!this.server) {
123
+ reject(new Error('Server not initialized'));
124
+ return;
125
+ }
126
+ this.server.once('error', reject);
127
+ this.server.listen(port, '127.0.0.1', () => resolve());
128
+ });
129
+ savePid(process.pid, port);
130
+ }
131
+ async stop() {
132
+ if (this.server) {
133
+ await new Promise((resolve) => this.server?.close(() => resolve()));
134
+ }
135
+ if (this.db) {
136
+ await this.db.close().catch(() => { });
137
+ }
138
+ removePid();
139
+ }
140
+ }
@@ -0,0 +1,19 @@
1
+ export type DaemonRequest = {
2
+ id: string;
3
+ method: string;
4
+ params?: any;
5
+ };
6
+ export type DaemonResponse = {
7
+ id: string;
8
+ ok: boolean;
9
+ result?: any;
10
+ error?: string;
11
+ };
12
+ export type DaemonStatus = {
13
+ running: boolean;
14
+ pid?: number;
15
+ port?: number;
16
+ uptime?: number;
17
+ version?: string;
18
+ };
19
+ export declare const DEFAULT_DAEMON_PORT = 45678;
@@ -0,0 +1 @@
1
+ export const DEFAULT_DAEMON_PORT = 45678;
package/dist/dinox.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createCli } from './cli.js';
3
- import { isDinoxError } from './utils/errors.js';
3
+ import { buildStructuredErrorPayload, isDinoxError } from './utils/errors.js';
4
4
  import { printYaml } from './utils/output.js';
5
5
  function normalizeArgv(argv) {
6
6
  // pnpm/npm script forwarding can prepend a standalone "--".
@@ -25,26 +25,18 @@ main().catch((error) => {
25
25
  const argv = process.argv;
26
26
  const json = shouldOutputJson(argv);
27
27
  const verbose = isVerbose(argv);
28
- const message = error instanceof Error ? error.message : String(error);
29
- const name = error instanceof Error ? error.name : 'Error';
30
- const stack = error instanceof Error ? error.stack : undefined;
31
- const details = isDinoxError(error) ? error.details : undefined;
28
+ const payload = buildStructuredErrorPayload(error, verbose);
32
29
  const exitCode = isDinoxError(error) ? error.exitCode : 1;
33
30
  process.exitCode = exitCode;
34
31
  if (json) {
35
32
  printYaml({
36
33
  ok: false,
37
- error: {
38
- name,
39
- message,
40
- ...(details !== undefined ? { details } : {}),
41
- ...(verbose && stack ? { stack } : {}),
42
- },
34
+ error: payload,
43
35
  });
44
36
  return;
45
37
  }
46
- console.error(message);
47
- if (verbose && stack) {
48
- console.error(stack);
38
+ console.error(payload.message);
39
+ if (verbose && payload.stack) {
40
+ console.error(payload.stack);
49
41
  }
50
42
  });
@@ -1,10 +1,27 @@
1
+ export type DinoxErrorOptions = {
2
+ exitCode?: number;
3
+ cause?: unknown;
4
+ details?: unknown;
5
+ code?: string;
6
+ suggestion?: string;
7
+ recoverable?: boolean;
8
+ };
9
+ export type StructuredErrorPayload = {
10
+ name: string;
11
+ code: string;
12
+ message: string;
13
+ recoverable: boolean;
14
+ suggestion?: string;
15
+ details?: unknown;
16
+ stack?: string;
17
+ };
1
18
  export declare class DinoxError extends Error {
2
19
  readonly exitCode: number;
3
20
  readonly details?: unknown;
4
- constructor(message: string, options?: {
5
- exitCode?: number;
6
- cause?: unknown;
7
- details?: unknown;
8
- });
21
+ readonly code?: string;
22
+ readonly suggestion?: string;
23
+ readonly recoverable: boolean;
24
+ constructor(message: string, options?: DinoxErrorOptions);
9
25
  }
10
26
  export declare function isDinoxError(error: unknown): error is DinoxError;
27
+ export declare function buildStructuredErrorPayload(error: unknown, verbose: boolean): StructuredErrorPayload;
@@ -1,11 +1,17 @@
1
1
  export class DinoxError extends Error {
2
2
  exitCode;
3
3
  details;
4
+ code;
5
+ suggestion;
6
+ recoverable;
4
7
  constructor(message, options) {
5
8
  super(message);
6
9
  this.name = 'DinoxError';
7
10
  this.exitCode = options?.exitCode ?? 1;
8
11
  this.details = options?.details;
12
+ this.code = options?.code;
13
+ this.suggestion = options?.suggestion;
14
+ this.recoverable = options?.recoverable ?? true;
9
15
  if (options?.cause !== undefined) {
10
16
  // Node 16+ supports Error.cause.
11
17
  this.cause = options.cause;
@@ -15,3 +21,26 @@ export class DinoxError extends Error {
15
21
  export function isDinoxError(error) {
16
22
  return Boolean(error) && typeof error === 'object' && error.name === 'DinoxError';
17
23
  }
24
+ export function buildStructuredErrorPayload(error, verbose) {
25
+ const message = error instanceof Error ? error.message : String(error);
26
+ const name = error instanceof Error ? error.name : 'Error';
27
+ const stack = error instanceof Error ? error.stack : undefined;
28
+ if (isDinoxError(error)) {
29
+ return {
30
+ name,
31
+ code: error.code ?? 'DINOX_ERROR',
32
+ message,
33
+ recoverable: error.recoverable,
34
+ ...(error.suggestion ? { suggestion: error.suggestion } : {}),
35
+ ...(error.details !== undefined ? { details: error.details } : {}),
36
+ ...(verbose && stack ? { stack } : {}),
37
+ };
38
+ }
39
+ return {
40
+ name,
41
+ code: 'INTERNAL_ERROR',
42
+ message,
43
+ recoverable: false,
44
+ ...(verbose && stack ? { stack } : {}),
45
+ };
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dinoxx/dinox-cli",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Dinox CLI",
5
5
  "main": "dist/dinox.js",
6
6
  "scripts": {