@dinoxx/dinox-cli 2.13.1 → 2.15.0

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 (112) hide show
  1. package/dist/ai/ragxClient.d.ts +15 -0
  2. package/dist/ai/ragxClient.js +71 -0
  3. package/dist/cli.js +31 -4
  4. package/dist/commands/auth/index.d.ts +2 -1
  5. package/dist/commands/auth/index.js +18 -1
  6. package/dist/commands/batch.d.ts +35 -0
  7. package/dist/commands/batch.js +202 -0
  8. package/dist/commands/boxes/index.js +21 -21
  9. package/dist/commands/doctor.d.ts +4 -1
  10. package/dist/commands/doctor.js +20 -1
  11. package/dist/commands/graph/index.js +2 -1
  12. package/dist/commands/mcp.d.ts +6 -0
  13. package/dist/commands/mcp.js +47 -0
  14. package/dist/commands/notes/actions.js +18 -2
  15. package/dist/commands/notes/contentPatch.js +2 -1
  16. package/dist/commands/notes/descriptors.js +111 -5
  17. package/dist/commands/notes/detailContent.d.ts +19 -0
  18. package/dist/commands/notes/detailContent.js +62 -0
  19. package/dist/commands/notes/export.js +7 -2
  20. package/dist/commands/notes/repo/search.d.ts +2 -1
  21. package/dist/commands/notes/repo/search.js +1 -1
  22. package/dist/commands/notes/repo/semantic.d.ts +14 -0
  23. package/dist/commands/notes/repo/semantic.js +31 -0
  24. package/dist/commands/notes/searchResponse.d.ts +3 -0
  25. package/dist/commands/notes/searchResponse.js +1 -0
  26. package/dist/commands/notes/semantic.d.ts +116 -0
  27. package/dist/commands/notes/semantic.js +332 -0
  28. package/dist/commands/prompt/index.js +7 -5
  29. package/dist/commands/runtime.d.ts +8 -0
  30. package/dist/commands/runtime.js +49 -1
  31. package/dist/commands/savedViews/index.js +12 -3
  32. package/dist/commands/schema/globalOptions.d.ts +7 -0
  33. package/dist/commands/schema/globalOptions.js +7 -0
  34. package/dist/commands/schema/renderSkills.js +6 -2
  35. package/dist/commands/schema/spec.js +20 -1
  36. package/dist/commands/storage/index.js +7 -5
  37. package/dist/commands/storage/repo/operations.js +3 -1
  38. package/dist/commands/sync.d.ts +2 -1
  39. package/dist/commands/sync.js +27 -8
  40. package/dist/commands/tags/index.js +21 -21
  41. package/dist/commands/todo/index.js +22 -2
  42. package/dist/commands/todo/repo.d.ts +5 -0
  43. package/dist/commands/todo/repo.js +7 -2
  44. package/dist/commands/writeReceipt.d.ts +13 -0
  45. package/dist/commands/writeReceipt.js +12 -0
  46. package/dist/config/serviceEndpoints.d.ts +2 -0
  47. package/dist/config/serviceEndpoints.js +2 -0
  48. package/dist/daemon/client.d.ts +2 -0
  49. package/dist/daemon/client.js +53 -3
  50. package/dist/daemon/connectivity.d.ts +20 -0
  51. package/dist/daemon/connectivity.js +42 -0
  52. package/dist/daemon/ensure.js +48 -12
  53. package/dist/daemon/forward.d.ts +37 -0
  54. package/dist/daemon/forward.js +155 -0
  55. package/dist/daemon/fresh.d.ts +4 -0
  56. package/dist/daemon/fresh.js +10 -0
  57. package/dist/daemon/index.d.ts +11 -6
  58. package/dist/daemon/index.js +90 -25
  59. package/dist/daemon/launch.d.ts +13 -1
  60. package/dist/daemon/launch.js +31 -2
  61. package/dist/daemon/lifecycle.d.ts +3 -1
  62. package/dist/daemon/lifecycle.js +27 -3
  63. package/dist/daemon/pid.d.ts +8 -0
  64. package/dist/daemon/pid.js +20 -0
  65. package/dist/daemon/route.js +2 -1
  66. package/dist/daemon/server.d.ts +45 -1
  67. package/dist/daemon/server.js +192 -4
  68. package/dist/daemon/supervise.d.ts +6 -6
  69. package/dist/daemon/supervise.js +12 -10
  70. package/dist/daemon/supervisor.d.ts +21 -5
  71. package/dist/daemon/supervisor.js +125 -6
  72. package/dist/daemon/types.d.ts +22 -0
  73. package/dist/dinox.js +43 -64
  74. package/dist/gateway/deviceHost.js +2 -1
  75. package/dist/gateway/mcpCommandTools.d.ts +24 -5
  76. package/dist/gateway/mcpCommandTools.js +70 -29
  77. package/dist/gateway/mcpEnv.d.ts +2 -0
  78. package/dist/gateway/mcpEnv.js +2 -0
  79. package/dist/gateway/mcpGroups.d.ts +3 -0
  80. package/dist/gateway/mcpGroups.js +4 -0
  81. package/dist/gateway/mcpServer.d.ts +10 -1
  82. package/dist/gateway/mcpServer.js +25 -5
  83. package/dist/powersync/runtime.d.ts +2 -0
  84. package/dist/powersync/runtime.js +3 -1
  85. package/dist/powersync/syncPass.js +13 -1
  86. package/dist/powersync/tokenIndex.d.ts +2 -0
  87. package/dist/powersync/tokenIndex.js +1 -0
  88. package/dist/requests/execute.d.ts +11 -0
  89. package/dist/requests/execute.js +26 -0
  90. package/dist/requests/store.d.ts +10 -0
  91. package/dist/requests/store.js +190 -0
  92. package/dist/runCli.d.ts +2 -0
  93. package/dist/runCli.js +64 -0
  94. package/dist/skills/commandRefs.js +2 -10
  95. package/dist/utils/argValue.js +2 -1
  96. package/dist/utils/cursor.d.ts +9 -0
  97. package/dist/utils/cursor.js +61 -0
  98. package/dist/utils/defaultFormat.d.ts +8 -0
  99. package/dist/utils/defaultFormat.js +38 -0
  100. package/dist/utils/errorCodes.d.ts +24 -0
  101. package/dist/utils/errorCodes.js +6 -0
  102. package/dist/utils/output.d.ts +4 -0
  103. package/dist/utils/output.js +72 -10
  104. package/dist/utils/progress.d.ts +25 -0
  105. package/dist/utils/progress.js +59 -0
  106. package/dist/utils/requestContext.d.ts +32 -0
  107. package/dist/utils/requestContext.js +13 -0
  108. package/dist/utils/select.d.ts +20 -0
  109. package/dist/utils/select.js +92 -0
  110. package/dist/utils/usageError.d.ts +37 -0
  111. package/dist/utils/usageError.js +127 -0
  112. package/package.json +2 -1
@@ -17,6 +17,7 @@ export const ERROR_CODES = {
17
17
  COMMAND_FAILED: { exitCode: 1, description: 'Generic command failure without a more specific code.' },
18
18
  CONFIG_INVALID_JSON: { exitCode: 1, description: 'The CLI config file is not valid JSON.' },
19
19
  CONFIG_WRITE_BUSY: { exitCode: 1, description: 'Another process is writing the CLI config.' },
20
+ DAEMON_COMMAND_NOT_FORWARDABLE: { exitCode: 2, description: 'The command must run in the CLI process.' },
20
21
  DAEMON_ERROR: { exitCode: 5, description: 'The background daemon reported an error.' },
21
22
  DAEMON_INSTANCE_CHANGED: { exitCode: 1, description: 'The daemon instance changed between requests.' },
22
23
  DAEMON_INSTANCE_MISMATCH: { exitCode: 1, description: 'The running daemon belongs to a different CLI version or account.' },
@@ -25,6 +26,7 @@ export const ERROR_CODES = {
25
26
  DAEMON_RELEASE_FAILED: { exitCode: 1, description: 'The daemon could not release database ownership.' },
26
27
  DAEMON_RELEASE_TIMEOUT: { exitCode: 1, description: 'The daemon did not release database ownership in time.' },
27
28
  DAEMON_SERVICE_FAILED: { exitCode: 1, description: 'The service manager (launchd / systemd) rejected a daemon service operation.' },
29
+ DAEMON_SERVICE_FOREIGN: { exitCode: 1, description: 'The daemon service runs another dino install; it was left unchanged.' },
28
30
  DAEMON_SERVICE_UNSUPPORTED: { exitCode: 5, description: 'The daemon cannot run as a login service on this platform or with this auth setup.' },
29
31
  DAEMON_STALE: { exitCode: 5, description: 'The daemon state is stale and needs a restart.' },
30
32
  DAEMON_START_FAILED: { exitCode: 5, description: 'The daemon process failed to start.' },
@@ -91,6 +93,9 @@ export const ERROR_CODES = {
91
93
  PROPERTY_UNIQUE_ID_TYPE_MISMATCH: { exitCode: 1, description: 'A unique-id setting was supplied for a different property type.' },
92
94
  PROPERTY_URL_INVALID: { exitCode: 1, description: 'A url property value is invalid.' },
93
95
  PROPERTY_VALUE_INVALID: { exitCode: 1, description: 'A property value does not match its definition.' },
96
+ REQUEST_ID_CONFLICT: { exitCode: 2, description: 'The request id was already used for different arguments.' },
97
+ REQUEST_INTERRUPTED: { exitCode: 1, description: 'The earlier request owner exited; its outcome is unknown.' },
98
+ REQUEST_IN_PROGRESS: { exitCode: 1, description: 'The earlier request is still running; retry with the same id.' },
94
99
  SAVED_VIEW_FILTER_CORRUPT: { exitCode: 1, description: 'A stored saved view filter is corrupt.' },
95
100
  SAVED_VIEW_FILTER_TOO_COMPLEX: { exitCode: 1, description: 'The saved view filter exceeds supported complexity.' },
96
101
  SAVED_VIEW_FILTER_VALUE_REQUIRED: { exitCode: 5, description: 'A saved view filter condition is missing its value.' },
@@ -118,6 +123,7 @@ export const ERROR_CODES = {
118
123
  SAVED_VIEW_UNSUPPORTED_LAYOUT: { exitCode: 1, description: 'The saved view layout is unsupported.' },
119
124
  SAVED_VIEW_VALUE_TYPE_MISMATCH: { exitCode: 1, description: 'A filter value does not match the field type.' },
120
125
  SCHEMA_PATH_NOT_FOUND: { exitCode: 5, description: 'No command schema exists for the requested path.' },
126
+ SEMANTIC_SEARCH_UNAVAILABLE: { exitCode: 1, description: 'The semantic search service could not be reached or failed; keyword search (note search) still works.' },
121
127
  SKILLS_LOCK_INVALID: { exitCode: 1, description: 'The skills installer lock file is malformed.' },
122
128
  SKILLS_LOCK_READ_FAILED: { exitCode: 1, description: 'The skills installer lock file could not be read.' },
123
129
  SKILLS_MIGRATION_FAILED: { exitCode: 1, description: 'The skills installer failed during migration.' },
@@ -1,7 +1,10 @@
1
+ import { type FieldSelection } from './select.js';
1
2
  export type StructuredOutputFormat = 'yaml' | 'json';
2
3
  export type StructuredNoticeMap = Record<string, unknown>;
3
4
  export declare function toYaml(value: unknown): string;
4
5
  export declare function toJson(value: unknown): string;
6
+ export declare function writeToStdout(text: string): void;
7
+ export declare function writeToStderr(text: string): void;
5
8
  export declare function printHumanReadableLine(text: string): void;
6
9
  export declare function printHumanReadableBlock(text: string): void;
7
10
  export declare function printHumanReadableWarning(text: string): void;
@@ -14,6 +17,7 @@ export declare function parseStructuredOutputFormatFromArgv(argv: string[]): Str
14
17
  export declare function setRequestedStructuredOutputFormat(format: StructuredOutputFormat | null): void;
15
18
  export declare function clearRequestedStructuredOutputFormat(): void;
16
19
  export declare function getRequestedStructuredOutputFormat(): StructuredOutputFormat | null;
20
+ export declare function setRequestedFieldSelection(selection: FieldSelection | null): void;
17
21
  export declare function setPendingStructuredNoticeProvider(provider: (() => StructuredNoticeMap | null) | null): void;
18
22
  export declare function clearPendingStructuredNoticeProvider(): void;
19
23
  export declare function printStructuredData(value: unknown, format?: StructuredOutputFormat | null): void;
@@ -1,8 +1,11 @@
1
1
  import { stringify as stringifyYaml } from 'yaml';
2
2
  import { DinoxError } from './errors.js';
3
3
  import { sanitizeForTerminal } from './terminalSanitization.js';
4
+ import { requestContext } from './requestContext.js';
5
+ import { projectFields } from './select.js';
4
6
  let requestedStructuredOutputFormat = null;
5
7
  let pendingStructuredNoticeProvider = null;
8
+ let requestedFieldSelection = null;
6
9
  export function toYaml(value) {
7
10
  return stringifyYaml(value, {
8
11
  lineWidth: 0,
@@ -12,10 +15,20 @@ export function toYaml(value) {
12
15
  export function toJson(value) {
13
16
  return `${JSON.stringify(value, null, 2)}\n`;
14
17
  }
15
- function writeToStdout(text) {
18
+ export function writeToStdout(text) {
19
+ const context = requestContext.getStore();
20
+ if (context) {
21
+ context.stdout.push(text);
22
+ return;
23
+ }
16
24
  process.stdout.write(text);
17
25
  }
18
- function writeToStderr(text) {
26
+ export function writeToStderr(text) {
27
+ const context = requestContext.getStore();
28
+ if (context) {
29
+ context.stderr.push(text);
30
+ return;
31
+ }
19
32
  process.stderr.write(text);
20
33
  }
21
34
  export function printHumanReadableLine(text) {
@@ -92,25 +105,68 @@ export function parseStructuredOutputFormatFromArgv(argv) {
92
105
  return explicitFormat ?? (hasLegacyJson ? 'yaml' : null);
93
106
  }
94
107
  export function setRequestedStructuredOutputFormat(format) {
108
+ const context = requestContext.getStore();
109
+ if (context) {
110
+ context.format = format;
111
+ return;
112
+ }
95
113
  requestedStructuredOutputFormat = format;
96
114
  }
97
115
  export function clearRequestedStructuredOutputFormat() {
98
- requestedStructuredOutputFormat = null;
116
+ setRequestedStructuredOutputFormat(null);
99
117
  }
100
118
  export function getRequestedStructuredOutputFormat() {
101
- return requestedStructuredOutputFormat;
119
+ const context = requestContext.getStore();
120
+ return context ? context.format : requestedStructuredOutputFormat;
121
+ }
122
+ export function setRequestedFieldSelection(selection) {
123
+ const context = requestContext.getStore();
124
+ if (context) {
125
+ context.select = selection;
126
+ return;
127
+ }
128
+ requestedFieldSelection = selection;
129
+ }
130
+ function getRequestedFieldSelection() {
131
+ const context = requestContext.getStore();
132
+ return context ? context.select ?? null : requestedFieldSelection;
133
+ }
134
+ /** Applies `--select` to a command result; unmatched paths become a notice. */
135
+ function selectRequestedFields(value) {
136
+ const selection = getRequestedFieldSelection();
137
+ if (!selection)
138
+ return { value, notice: null };
139
+ const { value: projected, missing } = projectFields(value, selection);
140
+ if (missing.length === 0)
141
+ return { value: projected, notice: null };
142
+ return {
143
+ value: projected,
144
+ notice: {
145
+ select_missing: {
146
+ message: 'These --select fields are not in the result; check `dino schema <command>` output fields.',
147
+ fields: missing,
148
+ },
149
+ },
150
+ };
102
151
  }
103
152
  export function setPendingStructuredNoticeProvider(provider) {
153
+ const context = requestContext.getStore();
154
+ if (context) {
155
+ context.noticeProvider = provider;
156
+ return;
157
+ }
104
158
  pendingStructuredNoticeProvider = provider;
105
159
  }
106
160
  export function clearPendingStructuredNoticeProvider() {
107
- pendingStructuredNoticeProvider = null;
161
+ setPendingStructuredNoticeProvider(null);
108
162
  }
109
163
  function getPendingStructuredNotice() {
110
- if (!pendingStructuredNoticeProvider) {
164
+ const context = requestContext.getStore();
165
+ const provider = context ? context.noticeProvider : pendingStructuredNoticeProvider;
166
+ if (!provider) {
111
167
  return null;
112
168
  }
113
- return pendingStructuredNoticeProvider();
169
+ return provider();
114
170
  }
115
171
  export function printStructuredData(value, format = getRequestedStructuredOutputFormat()) {
116
172
  if (format === 'json') {
@@ -166,15 +222,21 @@ function summarizeStructuredError(value) {
166
222
  export function printSuccess(value, format = getRequestedStructuredOutputFormat()) {
167
223
  if (format === 'json') {
168
224
  const data = normalizeSuccessEnvelopeData(value);
169
- const notice = mergeStructuredNotices(getPendingStructuredNotice(), deriveStructuredNotice(data));
225
+ const selected = selectRequestedFields(data);
226
+ const notice = mergeStructuredNotices(mergeStructuredNotices(getPendingStructuredNotice(), deriveStructuredNotice(data)) ?? null, selected.notice);
170
227
  printStructuredData({
171
228
  ok: true,
172
- data,
229
+ data: selected.value,
173
230
  ...(notice ? { _notice: notice } : {}),
174
231
  }, 'json');
175
232
  return;
176
233
  }
177
- printStructuredData(value, format);
234
+ const selected = selectRequestedFields(value);
235
+ if (selected.notice) {
236
+ const fields = selected.notice.select_missing.fields;
237
+ printHumanReadableWarning(`warning: --select fields not in the result: ${fields.join(', ')}`);
238
+ }
239
+ printStructuredData(selected.value, format);
178
240
  }
179
241
  export function printError(value, format = getRequestedStructuredOutputFormat()) {
180
242
  if (format === 'json') {
@@ -0,0 +1,25 @@
1
+ /** One step of a long-running command, reported on stderr under `--progress`. */
2
+ export type ProgressEvent = {
3
+ /** What the command is doing: `download`, `index`, `write`, ... */
4
+ phase: string;
5
+ current?: number;
6
+ total?: number;
7
+ message?: string;
8
+ };
9
+ export type ProgressReporting = {
10
+ /** Command path (`sync`, `note.export`), carried by every event. */
11
+ command: string;
12
+ startedAtMs: number;
13
+ lastPhase?: string;
14
+ lastAtMs?: number;
15
+ };
16
+ /** Enables (`command`) or disables (`null`) progress events for the running command. */
17
+ export declare function setProgressReporting(command: string | null, now?: number): void;
18
+ /**
19
+ * Structured output gets one JSON object per line (`"type":"progress"`), so
20
+ * stdout stays a single result while stderr streams events; human output gets
21
+ * a short line.
22
+ */
23
+ export declare function renderProgressEvent(reporting: ProgressReporting, event: ProgressEvent, structured: boolean, now: number): string;
24
+ /** Reports a progress event when `--progress` is on; otherwise does nothing. */
25
+ export declare function reportProgress(event: ProgressEvent, now?: number): void;
@@ -0,0 +1,59 @@
1
+ import { getRequestedStructuredOutputFormat, writeToStderr } from './output.js';
2
+ import { requestContext } from './requestContext.js';
3
+ /** Repeated events of one phase closer together than this are dropped, except the last of a known total. */
4
+ const MIN_INTERVAL_MS = 200;
5
+ let localReporting = null;
6
+ /** Enables (`command`) or disables (`null`) progress events for the running command. */
7
+ export function setProgressReporting(command, now = Date.now()) {
8
+ const reporting = command ? { command, startedAtMs: now } : null;
9
+ const context = requestContext.getStore();
10
+ if (context) {
11
+ context.progress = reporting;
12
+ return;
13
+ }
14
+ localReporting = reporting;
15
+ }
16
+ /**
17
+ * Structured output gets one JSON object per line (`"type":"progress"`), so
18
+ * stdout stays a single result while stderr streams events; human output gets
19
+ * a short line.
20
+ */
21
+ export function renderProgressEvent(reporting, event, structured, now) {
22
+ if (structured) {
23
+ return `${JSON.stringify({
24
+ type: 'progress',
25
+ command: reporting.command,
26
+ phase: event.phase,
27
+ ...(event.current !== undefined ? { current: event.current } : {}),
28
+ ...(event.total !== undefined ? { total: event.total } : {}),
29
+ ...(event.message !== undefined ? { message: event.message } : {}),
30
+ elapsed_ms: Math.max(0, now - reporting.startedAtMs),
31
+ })}\n`;
32
+ }
33
+ const count = event.current === undefined
34
+ ? ''
35
+ : event.total === undefined
36
+ ? ` ${event.current}`
37
+ : ` ${event.current}/${event.total}${event.total > 0 ? ` (${Math.floor((event.current / event.total) * 100)}%)` : ''}`;
38
+ return `[${reporting.command}] ${event.phase}${count}${event.message ? ` ${event.message}` : ''}\n`;
39
+ }
40
+ /** Reports a progress event when `--progress` is on; otherwise does nothing. */
41
+ export function reportProgress(event, now = Date.now()) {
42
+ const context = requestContext.getStore();
43
+ const reporting = context ? context.progress ?? null : localReporting;
44
+ if (!reporting)
45
+ return;
46
+ const finished = event.total !== undefined && event.current === event.total;
47
+ if (reporting.lastPhase === event.phase
48
+ && reporting.lastAtMs !== undefined
49
+ && now - reporting.lastAtMs < MIN_INTERVAL_MS
50
+ && !finished)
51
+ return;
52
+ reporting.lastPhase = event.phase;
53
+ reporting.lastAtMs = now;
54
+ const text = renderProgressEvent(reporting, event, getRequestedStructuredOutputFormat() !== null, now);
55
+ if (context?.onProgress)
56
+ context.onProgress(text);
57
+ else
58
+ writeToStderr(text);
59
+ }
@@ -0,0 +1,32 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import type { AbstractPowerSyncDatabase } from '@powersync/common';
3
+ import type { ResolvedDinoxConfig } from '../config/types.js';
4
+ import type { StructuredNoticeMap, StructuredOutputFormat } from './output.js';
5
+ import type { FieldSelection } from './select.js';
6
+ import type { ProgressReporting } from './progress.js';
7
+ export type RequestContext = {
8
+ stdout: string[];
9
+ stderr: string[];
10
+ format: StructuredOutputFormat | null;
11
+ /** `--select` of the running command; absent means the whole result. */
12
+ select?: FieldSelection | null;
13
+ noticeProvider: (() => StructuredNoticeMap | null) | null;
14
+ cwd: string;
15
+ /** `--progress` of the running command; absent means off. */
16
+ progress?: ProgressReporting | null;
17
+ /** Delivers rendered progress events live (the daemon streams them to the client); default stderr. */
18
+ onProgress?: (text: string) => void;
19
+ daemon?: {
20
+ db: AbstractPowerSyncDatabase;
21
+ config: ResolvedDinoxConfig;
22
+ /**
23
+ * Set after the daemon reconnected (wake from sleep, network change): the
24
+ * stream is only known to be current once a checkpoint completes after it.
25
+ */
26
+ syncUnconfirmedSince?: () => string | null;
27
+ };
28
+ };
29
+ export declare const requestContext: AsyncLocalStorage<RequestContext>;
30
+ export declare function resolveUserPath(p: string): string;
31
+ export declare function assertStdinAvailable(): void;
32
+ export declare const localCommandExecution: AsyncLocalStorage<boolean>;
@@ -0,0 +1,13 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import path from 'node:path';
3
+ import { DinoxError } from './errors.js';
4
+ export const requestContext = new AsyncLocalStorage();
5
+ export function resolveUserPath(p) {
6
+ return path.resolve(requestContext.getStore()?.cwd ?? process.cwd(), p);
7
+ }
8
+ export function assertStdinAvailable() {
9
+ if (requestContext.getStore()?.daemon) {
10
+ throw new DinoxError('Reading stdin inside the daemon is not supported.', { code: 'USAGE_ERROR' });
11
+ }
12
+ }
13
+ export const localCommandExecution = new AsyncLocalStorage();
@@ -0,0 +1,20 @@
1
+ /** A parsed `--select` list: each key keeps the whole value (true) or a nested selection. */
2
+ export type FieldSelection = {
3
+ [key: string]: FieldSelection | true;
4
+ };
5
+ /**
6
+ * Parses `--select notes.id,notes.title,count` into a selection tree. A path
7
+ * that names a parent also keeps everything below it (`notes,notes.id` keeps
8
+ * whole notes).
9
+ */
10
+ export declare function parseFieldSelection(value: unknown): FieldSelection;
11
+ /**
12
+ * Keeps only the selected fields. Arrays are projected item by item, so
13
+ * `notes.id` selects the id of every note. Returns the projected value and the
14
+ * paths that matched nothing anywhere, so a typo is reported instead of
15
+ * silently producing empty output.
16
+ */
17
+ export declare function projectFields(value: unknown, selection: FieldSelection): {
18
+ value: unknown;
19
+ missing: string[];
20
+ };
@@ -0,0 +1,92 @@
1
+ import { DinoxError } from './errors.js';
2
+ const FIELD_PATH = /^[A-Za-z0-9_$-]+(\.[A-Za-z0-9_$-]+)*$/;
3
+ /**
4
+ * Parses `--select notes.id,notes.title,count` into a selection tree. A path
5
+ * that names a parent also keeps everything below it (`notes,notes.id` keeps
6
+ * whole notes).
7
+ */
8
+ export function parseFieldSelection(value) {
9
+ const raw = Array.isArray(value) ? value.join(',') : String(value ?? '');
10
+ const paths = raw.split(',').map((path) => path.trim()).filter(Boolean);
11
+ if (paths.length === 0) {
12
+ throw new DinoxError('--select needs at least one field path, e.g. --select notes.id,notes.title', {
13
+ code: 'INVALID_ARGUMENT',
14
+ recoverable: true,
15
+ });
16
+ }
17
+ const selection = {};
18
+ for (const path of paths) {
19
+ if (!FIELD_PATH.test(path)) {
20
+ throw new DinoxError(`Invalid --select field path: ${path}`, {
21
+ code: 'INVALID_ARGUMENT',
22
+ recoverable: true,
23
+ suggestion: 'Use dot-separated output field names from `dino schema <command> --format json`.',
24
+ });
25
+ }
26
+ let node = selection;
27
+ const segments = path.split('.');
28
+ segments.forEach((segment, index) => {
29
+ if (node[segment] === true)
30
+ return;
31
+ if (index === segments.length - 1) {
32
+ node[segment] = true;
33
+ return;
34
+ }
35
+ node[segment] ??= {};
36
+ node = node[segment];
37
+ });
38
+ }
39
+ return selection;
40
+ }
41
+ /**
42
+ * Keeps only the selected fields. Arrays are projected item by item, so
43
+ * `notes.id` selects the id of every note. Returns the projected value and the
44
+ * paths that matched nothing anywhere, so a typo is reported instead of
45
+ * silently producing empty output.
46
+ */
47
+ export function projectFields(value, selection) {
48
+ const matched = new Set();
49
+ // Paths below an empty list or a null value cannot be checked; never report them.
50
+ const assumeMatched = (node, prefix) => {
51
+ for (const [key, child] of Object.entries(node)) {
52
+ const path = prefix ? `${prefix}.${key}` : key;
53
+ matched.add(path);
54
+ if (child !== true)
55
+ assumeMatched(child, path);
56
+ }
57
+ };
58
+ const project = (current, node, prefix) => {
59
+ if (Array.isArray(current)) {
60
+ if (current.length === 0)
61
+ assumeMatched(node, prefix);
62
+ return current.map((item) => project(item, node, prefix));
63
+ }
64
+ if (!current || typeof current !== 'object') {
65
+ assumeMatched(node, prefix);
66
+ return current;
67
+ }
68
+ const record = current;
69
+ const result = {};
70
+ for (const [key, child] of Object.entries(node)) {
71
+ if (!Object.prototype.hasOwnProperty.call(record, key))
72
+ continue;
73
+ const path = prefix ? `${prefix}.${key}` : key;
74
+ matched.add(path);
75
+ result[key] = child === true ? record[key] : project(record[key], child, path);
76
+ }
77
+ return result;
78
+ };
79
+ const projected = project(value, selection, '');
80
+ const missing = [];
81
+ const collect = (node, prefix) => {
82
+ for (const [key, child] of Object.entries(node)) {
83
+ const path = prefix ? `${prefix}.${key}` : key;
84
+ if (!matched.has(path))
85
+ missing.push(path);
86
+ else if (child !== true)
87
+ collect(child, path);
88
+ }
89
+ };
90
+ collect(selection, '');
91
+ return { value: projected, missing };
92
+ }
@@ -0,0 +1,37 @@
1
+ import type { Command } from 'commander';
2
+ import type { SuggestedAction } from './errors.js';
3
+ export type UsageErrorKind = 'unknown_option' | 'unknown_command' | 'missing_argument' | 'missing_option_value' | 'missing_required_option' | 'invalid_option_value' | 'too_many_arguments' | 'usage';
4
+ /** Machine-readable form of a Commander usage error, carried in the error's `details`. */
5
+ export type UsageErrorDetails = {
6
+ kind: UsageErrorKind;
7
+ /** The offending option, command, or argument name, when Commander names one. */
8
+ token: string | null;
9
+ /** Commander's spelling suggestions, closest first. */
10
+ did_you_mean: string[];
11
+ /** Dot path of the deepest command that was recognized, e.g. `note.search`. */
12
+ command: string | null;
13
+ };
14
+ export type ParsedUsageError = {
15
+ message: string;
16
+ details: UsageErrorDetails;
17
+ suggestion?: string;
18
+ suggestedAction?: SuggestedAction;
19
+ };
20
+ type ResolvedCommand = {
21
+ /** Names of the recognized commands, outermost first. */
22
+ path: string[];
23
+ command: Command;
24
+ /** The first word that did not name a subcommand of `command`. */
25
+ unmatched: string | null;
26
+ };
27
+ /** Walks argv through the command tree as far as it names commands. */
28
+ export declare function resolveCommand(program: Command, argv: string[]): ResolvedCommand;
29
+ /** Closest candidates, using the same thresholds as Commander's own suggestions. */
30
+ export declare function suggestSimilar(word: string, candidates: string[]): string[];
31
+ /**
32
+ * Turns Commander's error text ("error: unknown option '--limt'\n(Did you mean
33
+ * --limit?)") into a message, structured details and, when there is exactly
34
+ * one suggestion, the corrected command line an agent can run.
35
+ */
36
+ export declare function parseCommanderUsageError(text: string, argv: string[], program: Command): ParsedUsageError;
37
+ export {};
@@ -0,0 +1,127 @@
1
+ import { GLOBAL_VALUE_OPTIONS } from '../commands/schema/globalOptions.js';
2
+ const PATTERNS = [
3
+ ['unknown_option', /^unknown option '([^']+)'/],
4
+ ['unknown_command', /^unknown command '([^']+)'/],
5
+ ['missing_argument', /^missing required argument '([^']+)'/],
6
+ ['missing_option_value', /^option '([^']+)' argument missing/],
7
+ ['missing_required_option', /^required option '([^']+)' not specified/],
8
+ ['invalid_option_value', /^option '([^']+)' argument '.*' is invalid/s],
9
+ ['too_many_arguments', /^too many arguments/],
10
+ ];
11
+ /** Walks argv through the command tree as far as it names commands. */
12
+ export function resolveCommand(program, argv) {
13
+ const path = [];
14
+ let current = program;
15
+ for (let i = 0; i < argv.length; i++) {
16
+ const arg = argv[i];
17
+ if (arg === '--')
18
+ break;
19
+ if (GLOBAL_VALUE_OPTIONS.includes(arg)) {
20
+ i++;
21
+ continue;
22
+ }
23
+ if (arg.startsWith('-'))
24
+ continue;
25
+ const next = current.commands.find((command) => command.name() === arg || command.aliases().includes(arg));
26
+ if (!next)
27
+ return { path, command: current, unmatched: arg };
28
+ path.push(next.name());
29
+ current = next;
30
+ }
31
+ return { path, command: current, unmatched: null };
32
+ }
33
+ /** Optimal string alignment distance (Damerau-Levenshtein with adjacent transpositions). */
34
+ function editDistance(a, b) {
35
+ const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
36
+ for (let j = 0; j <= b.length; j++)
37
+ d[0][j] = j;
38
+ for (let i = 1; i <= a.length; i++) {
39
+ for (let j = 1; j <= b.length; j++) {
40
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
41
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
42
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
43
+ d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
44
+ }
45
+ }
46
+ }
47
+ return d[a.length][b.length];
48
+ }
49
+ /** Closest candidates, using the same thresholds as Commander's own suggestions. */
50
+ export function suggestSimilar(word, candidates) {
51
+ let best = 3;
52
+ let similar = [];
53
+ for (const candidate of new Set(candidates)) {
54
+ if (candidate.length <= 1)
55
+ continue;
56
+ const distance = editDistance(word, candidate);
57
+ const length = Math.max(word.length, candidate.length);
58
+ if ((length - distance) / length <= 0.4)
59
+ continue;
60
+ if (distance < best) {
61
+ best = distance;
62
+ similar = [candidate];
63
+ }
64
+ else if (distance === best) {
65
+ similar.push(candidate);
66
+ }
67
+ }
68
+ return similar.sort((a, b) => a.localeCompare(b));
69
+ }
70
+ function shellQuote(arg) {
71
+ return /^[A-Za-z0-9_@%+=:,./-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
72
+ }
73
+ /**
74
+ * Turns Commander's error text ("error: unknown option '--limt'\n(Did you mean
75
+ * --limit?)") into a message, structured details and, when there is exactly
76
+ * one suggestion, the corrected command line an agent can run.
77
+ */
78
+ export function parseCommanderUsageError(text, argv, program) {
79
+ const body = text.trim().replace(/^error:\s*/i, '');
80
+ const suggestionMatch = /\n?\(Did you mean (?:one of )?(.+)\?\)\s*$/.exec(body);
81
+ let message = (suggestionMatch ? body.slice(0, suggestionMatch.index) : body).trim();
82
+ let didYouMean = suggestionMatch ? suggestionMatch[1].split(',').map((item) => item.trim()).filter(Boolean) : [];
83
+ const resolved = resolveCommand(program, argv);
84
+ let kind = 'usage';
85
+ let token = null;
86
+ for (const [candidate, pattern] of PATTERNS) {
87
+ const match = pattern.exec(message);
88
+ if (match) {
89
+ kind = candidate;
90
+ token = match[1] ?? null;
91
+ break;
92
+ }
93
+ }
94
+ // A command group with its own action reports a mistyped subcommand as an
95
+ // extra argument; report it as the unknown command it is.
96
+ if (resolved.command.commands.length > 0 && resolved.unmatched && (kind === 'too_many_arguments' || kind === 'unknown_command')) {
97
+ kind = 'unknown_command';
98
+ token = resolved.unmatched;
99
+ message = `unknown command '${token}'${resolved.path.length > 0 ? ` for 'dino ${resolved.path.join(' ')}'` : ''}`;
100
+ }
101
+ if (kind === 'unknown_command' && token) {
102
+ didYouMean = suggestSimilar(token, resolved.command.commands.flatMap((command) => [command.name(), ...command.aliases()]));
103
+ }
104
+ const command = resolved.path.length > 0 ? resolved.path.join('.') : null;
105
+ const details = { kind, token, did_you_mean: didYouMean, command };
106
+ // Only leaf commands have a schema of their own; the bare form lists them all.
107
+ const schemaHint = command && resolved.command.commands.length === 0
108
+ ? `dino schema ${command} --format json`
109
+ : 'dino schema --format json';
110
+ const parsed = {
111
+ message,
112
+ details,
113
+ suggestion: didYouMean.length > 0
114
+ ? `Did you mean ${didYouMean.join(' or ')}? Run \`${schemaHint}\` to list valid ${kind === 'unknown_command' ? 'commands' : 'options'}.`
115
+ : `Run \`${schemaHint}\` for the valid arguments and options.`,
116
+ };
117
+ const index = token ? argv.indexOf(token) : -1;
118
+ if (didYouMean.length === 1 && index !== -1 && (kind === 'unknown_option' || kind === 'unknown_command')) {
119
+ const corrected = [...argv];
120
+ corrected[index] = didYouMean[0];
121
+ parsed.suggestedAction = {
122
+ command: `dino ${corrected.map(shellQuote).join(' ')}`,
123
+ reason: `${token} is not a valid ${kind === 'unknown_option' ? 'option' : 'command'}; ${didYouMean[0]} is the closest match.`,
124
+ };
125
+ }
126
+ return parsed;
127
+ }
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@dinoxx/dinox-cli",
3
- "version": "2.13.1",
3
+ "version": "2.15.0",
4
4
  "description": "Dinox CLI",
5
5
  "main": "dist/dinox.js",
6
6
  "scripts": {
7
+ "bench:startup": "node scripts/bench-startup.mjs",
7
8
  "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json && node scripts/add-shebang.mjs",
8
9
  "dev": "tsx src/dinox.ts",
9
10
  "prepublishOnly": "pnpm run skills:check && pnpm run build",