@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
@@ -0,0 +1,15 @@
1
+ export type RagxRequest = {
2
+ baseUrl: string;
3
+ authorization: string | undefined;
4
+ path: '/api/ragx/search' | '/api/ragx/similar' | '/api/ragx/context';
5
+ body: unknown;
6
+ timeoutMs: number;
7
+ fetch?: typeof fetch;
8
+ };
9
+ /**
10
+ * Calls a dinox-tool RAG endpoint and returns its `data`. Business codes map
11
+ * onto CLI codes: 000001 is a rejected token or request, 000006 a missing
12
+ * target, anything else (and every transport failure) is the service being
13
+ * unavailable.
14
+ */
15
+ export declare function postRagx<T>(request: RagxRequest): Promise<T>;
@@ -0,0 +1,71 @@
1
+ import { toAuthorizationHeaderValue } from '../utils/authToken.js';
2
+ import { DinoxError } from '../utils/errors.js';
3
+ const RAGX_OK = '000000';
4
+ const RAGX_INVALID = '000001';
5
+ const RAGX_NOT_FOUND = '000006';
6
+ function unavailable(message, cause) {
7
+ return new DinoxError(message, {
8
+ code: 'SEMANTIC_SEARCH_UNAVAILABLE',
9
+ cause,
10
+ recoverable: true,
11
+ suggestion: 'Retry shortly, or fall back to keyword search with `dino note search <query> --format json`.',
12
+ });
13
+ }
14
+ /**
15
+ * Calls a dinox-tool RAG endpoint and returns its `data`. Business codes map
16
+ * onto CLI codes: 000001 is a rejected token or request, 000006 a missing
17
+ * target, anything else (and every transport failure) is the service being
18
+ * unavailable.
19
+ */
20
+ export async function postRagx(request) {
21
+ if (!request.authorization) {
22
+ throw new DinoxError('Semantic search needs a logged-in account.', { code: 'AUTH_REQUIRED', recoverable: true });
23
+ }
24
+ let response;
25
+ try {
26
+ response = await (request.fetch ?? fetch)(`${request.baseUrl}${request.path}`, {
27
+ method: 'POST',
28
+ headers: {
29
+ Accept: 'application/json',
30
+ 'Content-Type': 'application/json',
31
+ Authorization: toAuthorizationHeaderValue(request.authorization),
32
+ },
33
+ body: JSON.stringify(request.body),
34
+ signal: AbortSignal.timeout(request.timeoutMs),
35
+ });
36
+ }
37
+ catch (error) {
38
+ const timedOut = error instanceof Error && error.name === 'TimeoutError';
39
+ throw unavailable(timedOut
40
+ ? `Semantic search did not answer within ${request.timeoutMs} ms`
41
+ : `Semantic search service unreachable: ${error instanceof Error ? error.message : String(error)}`, error);
42
+ }
43
+ let payload;
44
+ try {
45
+ payload = JSON.parse(await response.text());
46
+ }
47
+ catch {
48
+ payload = null;
49
+ }
50
+ const code = typeof payload?.code === 'string' ? payload.code : null;
51
+ const message = typeof payload?.msg === 'string' ? payload.msg.trim() : '';
52
+ if (code === RAGX_OK && response.ok)
53
+ return (payload?.data ?? null);
54
+ if (response.status === 401 || response.status === 403 || (code === RAGX_INVALID && /authorization|userNo|token/i.test(message))) {
55
+ throw new DinoxError('The semantic search service rejected the login token.', {
56
+ code: 'AUTH_INVALID',
57
+ recoverable: true,
58
+ suggestion: 'Run `dino auth status --format json`; if the token expired, log in again with `dino auth login`.',
59
+ });
60
+ }
61
+ if (code === RAGX_INVALID) {
62
+ throw new DinoxError(`Semantic search rejected the request: ${message || 'invalid request'}`, {
63
+ code: 'INVALID_ARGUMENT',
64
+ recoverable: true,
65
+ });
66
+ }
67
+ if (code === RAGX_NOT_FOUND) {
68
+ throw new DinoxError(message || 'The semantic search target was not found.', { code: 'NOT_FOUND', recoverable: true });
69
+ }
70
+ throw unavailable(`Semantic search failed (HTTP ${response.status}${code ? `, code ${code}` : ''})${message ? `: ${message}` : ''}`);
71
+ }
package/dist/cli.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { Command } from 'commander';
2
2
  import { registerAuthCommands } from './commands/auth/index.js';
3
+ import { parseCommanderUsageError } from './utils/usageError.js';
4
+ import { registerBatchCommand } from './commands/batch.js';
5
+ import { registerMcpCommand } from './commands/mcp.js';
3
6
  import { registerBoxesCommand } from './commands/boxes/index.js';
4
7
  import { registerConfigCommands } from './commands/config/index.js';
5
8
  import { registerDaemonCommands } from './daemon/index.js';
@@ -21,8 +24,18 @@ import { registerTagsCommand } from './commands/tags/index.js';
21
24
  import { registerTemplateCommands } from './commands/templates/index.js';
22
25
  import { registerTodoCommands } from './commands/todo/index.js';
23
26
  import { getPackageVersion } from './utils/version.js';
27
+ import { validateRequestId } from './requests/store.js';
28
+ import { parseFieldSelection } from './utils/select.js';
29
+ import { setProgressReporting } from './utils/progress.js';
24
30
  import { buildStructuredErrorPayload, DinoxError } from './utils/errors.js';
25
- import { clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, resolveStructuredOutputFormat, setRequestedStructuredOutputFormat, } from './utils/output.js';
31
+ import { writeToStdout, writeToStderr, clearRequestedStructuredOutputFormat, parseStructuredOutputFormatFromArgv, printError, resolveStructuredOutputFormat, setRequestedFieldSelection, setRequestedStructuredOutputFormat, } from './utils/output.js';
32
+ /** `note.export` for `dino note export`. */
33
+ function commandPath(command) {
34
+ const names = [];
35
+ for (let current = command; current?.parent; current = current.parent)
36
+ names.unshift(current.name());
37
+ return names.join('.');
38
+ }
26
39
  export function createCli() {
27
40
  clearRequestedStructuredOutputFormat();
28
41
  const program = new Command();
@@ -33,9 +46,10 @@ export function createCli() {
33
46
  .version(getPackageVersion())
34
47
  .showHelpAfterError()
35
48
  .configureOutput({
49
+ writeOut: writeToStdout,
36
50
  writeErr: (text) => {
37
51
  if (!structuredCommanderErrorWritten) {
38
- process.stderr.write(text);
52
+ writeToStderr(text);
39
53
  }
40
54
  },
41
55
  outputError: (text, write) => {
@@ -53,10 +67,14 @@ export function createCli() {
53
67
  return;
54
68
  }
55
69
  structuredCommanderErrorWritten = true;
56
- const message = text.trim().replace(/^error:\s*/i, '');
57
- const error = new DinoxError(message, {
70
+ const args = program.rawArgs?.slice(2) ?? [];
71
+ const usage = parseCommanderUsageError(text, args, program);
72
+ const error = new DinoxError(usage.message, {
58
73
  code: 'USAGE_ERROR',
59
74
  recoverable: true,
75
+ details: usage.details,
76
+ suggestion: usage.suggestion,
77
+ suggestedAction: usage.suggestedAction,
60
78
  });
61
79
  printError(buildStructuredErrorPayload(error, false), format);
62
80
  },
@@ -68,15 +86,23 @@ export function createCli() {
68
86
  .option('--offline', 'skip connect/sync and only use local cache')
69
87
  .option('--require-sync', 'fail if the local PowerSync cache cannot be confirmed fresh before reading')
70
88
  .option('--sync-timeout <ms>', 'override sync/connect timeout (milliseconds)')
89
+ .option('--request-id <id>', 'idempotency key: replays the stored result instead of running the command again (24h)', validateRequestId)
90
+ .option('--select <fields>', 'keep only these comma-separated output fields (dot paths, e.g. notes.id,notes.title)')
91
+ .option('--progress', 'report progress of long-running commands (sync, note export) on stderr')
71
92
  .option('--verbose', 'enable verbose logging');
72
93
  program.hook('preAction', (_thisCommand, actionCommand) => {
73
94
  const options = actionCommand.optsWithGlobals?.() ?? {};
74
95
  setRequestedStructuredOutputFormat(resolveStructuredOutputFormat(options));
96
+ setRequestedFieldSelection(options.select === undefined ? null : parseFieldSelection(options.select));
97
+ setProgressReporting(options.progress === true ? commandPath(actionCommand) : null);
75
98
  });
76
99
  program.hook('postAction', () => {
77
100
  clearRequestedStructuredOutputFormat();
101
+ setRequestedFieldSelection(null);
102
+ setProgressReporting(null);
78
103
  });
79
104
  registerAuthCommands(program);
105
+ registerBatchCommand(program);
80
106
  registerBoxesCommand(program);
81
107
  registerConfigCommands(program);
82
108
  registerDaemonCommands(program);
@@ -84,6 +110,7 @@ export function createCli() {
84
110
  registerDoctorCommand(program);
85
111
  registerGraphCommands(program);
86
112
  registerInfoCommand(program);
113
+ registerMcpCommand(program);
87
114
  registerMetaCommands(program);
88
115
  registerSyncCommand(program);
89
116
  registerUpdateCommand(program);
@@ -3,13 +3,14 @@ import { fetchUserInfoWithAuthorization } from '../../auth/userInfo.js';
3
3
  import { loadConfig, updateConfig } from '../../config/store.js';
4
4
  import { resolveConfig } from '../../config/resolve.js';
5
5
  import { restoreSuspendedDaemonLocked, suspendDaemonForExclusiveAccessLocked } from '../../daemon/lifecycle.js';
6
- import { acquireDaemonStartLock } from '../../daemon/pid.js';
6
+ import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock } from '../../daemon/pid.js';
7
7
  import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
8
8
  import type { CommandGroupDescriptor } from '../descriptor/types.js';
9
9
  import { printHumanReadableWarning } from '../../utils/output.js';
10
10
  export declare function releaseOwnerLeases(releases: ReadonlyArray<() => Promise<void>>): Promise<unknown[]>;
11
11
  type AuthLoginDependencies = {
12
12
  acquireDaemonStartLock: typeof acquireDaemonStartLock;
13
+ acquireDaemonIntentLock: typeof acquireDaemonExclusiveIntentLock;
13
14
  loadConfig: typeof loadConfig;
14
15
  resolveConfig: typeof resolveConfig;
15
16
  fetchCredentials: typeof fetchCredentialsWithAuthorization;
@@ -10,7 +10,7 @@ import { getDaemonInfo } from '../../daemon/client.js';
10
10
  import { restoreSuspendedDaemonLocked, suspendDaemonForExclusiveAccessLocked, } from '../../daemon/lifecycle.js';
11
11
  import { stopDaemonLocked } from '../../daemon/index.js';
12
12
  import { autoInstallDaemonService, describeDaemonServiceAutoInstall } from '../../daemon/autoInstall.js';
13
- import { acquireDaemonStartLock } from '../../daemon/pid.js';
13
+ import { acquireDaemonExclusiveIntentLock, acquireDaemonStartLock } from '../../daemon/pid.js';
14
14
  import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
15
15
  import { withExclusivePowerSync } from '../../powersync/exclusiveRuntime.js';
16
16
  import { acquirePowerSyncOwnerLease, getStatusSnapshot, } from '../../powersync/runtime.js';
@@ -22,6 +22,7 @@ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printHumanR
22
22
  import { redactAuthorization } from '../../utils/redact.js';
23
23
  import { withAsyncRelease } from '../../utils/asyncRelease.js';
24
24
  import { parseSyncTimeoutMs } from '../../utils/syncTimeout.js';
25
+ import { assertStdinAvailable } from '../../utils/requestContext.js';
25
26
  const AUTH_LOGIN_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
26
27
  const AUTH_STATUS_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
27
28
  const MAX_STDIN_TOKEN_BYTES = 64 * 1024;
@@ -80,6 +81,7 @@ export async function releaseOwnerLeases(releases) {
80
81
  return errors;
81
82
  }
82
83
  async function readAuthTokenFromStdin() {
84
+ assertStdinAvailable();
83
85
  if (process.stdin.isTTY) {
84
86
  throw new DinoxError('--token-stdin requires piped stdin', {
85
87
  code: 'USAGE_ERROR',
@@ -100,6 +102,7 @@ async function readAuthTokenFromStdin() {
100
102
  }
101
103
  const authLoginDependencies = {
102
104
  acquireDaemonStartLock,
105
+ acquireDaemonIntentLock: acquireDaemonExclusiveIntentLock,
103
106
  loadConfig,
104
107
  resolveConfig,
105
108
  fetchCredentials: fetchCredentialsWithAuthorization,
@@ -117,6 +120,15 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
117
120
  const releaseStartLock = await dependencies.acquireDaemonStartLock(parseSyncTimeoutMs(syncTimeout) ?? 10_000);
118
121
  let previousConfig = null;
119
122
  let suspended = null;
123
+ // Held from stopping the daemon until the new login is saved, so a daemon
124
+ // the service manager respawns meanwhile starts with the new identity.
125
+ // Released before any restore, which waits for the daemon to come up.
126
+ let releaseIntentLock = null;
127
+ const releaseIntent = async () => {
128
+ const release = releaseIntentLock;
129
+ releaseIntentLock = null;
130
+ await release?.();
131
+ };
120
132
  let lifecycleLockReleaseError;
121
133
  let outcome;
122
134
  try {
@@ -138,6 +150,7 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
138
150
  });
139
151
  const verifiedConfig = mergeAuthIdentityIntoConfig(previousConfig, rawToken, identity);
140
152
  const verifiedResolved = dependencies.resolveConfig(verifiedConfig);
153
+ releaseIntentLock = await dependencies.acquireDaemonIntentLock(timeoutMs);
141
154
  suspended = await dependencies.suspendDaemon(previousResolved, timeoutMs, { forceStop: true });
142
155
  await dependencies.updateConfig((current) => {
143
156
  if (!persistedAuthStateMatches(current, previousConfig)) {
@@ -149,6 +162,7 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
149
162
  }
150
163
  return rotatePersistedAuthStateRevision(mergeAuthIdentityIntoConfig(current, rawToken, identity));
151
164
  }, { timeoutMs });
165
+ await releaseIntent();
152
166
  outcome = {
153
167
  ok: true,
154
168
  value: {
@@ -164,6 +178,9 @@ export async function performAuthLogin(rawToken, syncTimeout, overrides = {}) {
164
178
  }
165
179
  catch (error) {
166
180
  outcome = { ok: false, error };
181
+ await releaseIntent().catch((releaseError) => {
182
+ dependencies.printWarning(`warning: failed to release daemon intent lock: ${authLoginErrorMessage(releaseError)}`);
183
+ });
167
184
  if (previousConfig && suspended) {
168
185
  try {
169
186
  const current = await dependencies.loadConfig();
@@ -0,0 +1,35 @@
1
+ import { Command } from 'commander';
2
+ import type { CommandExecuteResult } from '../daemon/types.js';
3
+ import { executeCommandLine } from '../requests/execute.js';
4
+ import type { CommandDescriptor, CommandExecutionContext } from './descriptor/types.js';
5
+ export declare const MAX_BATCH_ITEMS = 100;
6
+ export type BatchItem = {
7
+ index: number;
8
+ id: string | null;
9
+ args: string[];
10
+ };
11
+ export type BatchItemResult = {
12
+ index: number;
13
+ id: string | null;
14
+ args: string[];
15
+ ok: boolean;
16
+ exit_code: number;
17
+ data?: unknown;
18
+ notice?: unknown;
19
+ error?: unknown;
20
+ };
21
+ /** Validates the batch document: argv arrays, or `{ id?, args, request_id? }` objects. */
22
+ export declare function parseBatchItems(text: string): BatchItem[];
23
+ /** Adds the batch's own sync options to an item that does not set them itself. */
24
+ export declare function itemArgv(args: string[], globals: Record<string, unknown>): string[];
25
+ export declare function toBatchItemResult(item: BatchItem, result: CommandExecuteResult): BatchItemResult;
26
+ declare function readStdin(): Promise<string>;
27
+ declare const batchDependencies: {
28
+ readStdin: typeof readStdin;
29
+ readFile: (file: string) => Promise<string>;
30
+ execute: typeof executeCommandLine;
31
+ };
32
+ export declare function runBatch(context: CommandExecutionContext, overrides?: Partial<typeof batchDependencies>): Promise<void>;
33
+ export declare const batchCommandDescriptor: CommandDescriptor;
34
+ export declare function registerBatchCommand(program: Command): void;
35
+ export {};
@@ -0,0 +1,202 @@
1
+ import fs from 'node:fs/promises';
2
+ import { commandExecutionTarget } from '../daemon/forward.js';
3
+ import { executeCommandLine } from '../requests/execute.js';
4
+ import { validateRequestId } from '../requests/store.js';
5
+ import { DinoxError } from '../utils/errors.js';
6
+ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../utils/output.js';
7
+ import { assertStdinAvailable, resolveUserPath } from '../utils/requestContext.js';
8
+ import { registerStandaloneCommand } from './descriptor/register.js';
9
+ import { FORMAT_OPTIONS, OFFLINE_OPTIONS, REQUIRE_SYNC_OPTIONS, SYNC_TIMEOUT_OPTIONS } from './schema/globalOptions.js';
10
+ export const MAX_BATCH_ITEMS = 100;
11
+ /** Options each item may not set: the batch owns output format and help. */
12
+ const RESERVED_ITEM_OPTIONS = ['--format', '--json', '--help', '-h', '--version', '-V'];
13
+ function invalidInput(message, details) {
14
+ return new DinoxError(message, {
15
+ code: 'INVALID_ARGUMENT',
16
+ recoverable: true,
17
+ details,
18
+ suggestion: 'Pass a JSON array whose items are argument arrays, e.g. [["note","search","AI"],{"id":"t1","args":["todo","search"]}].',
19
+ });
20
+ }
21
+ /** Validates the batch document: argv arrays, or `{ id?, args, request_id? }` objects. */
22
+ export function parseBatchItems(text) {
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(text);
26
+ }
27
+ catch (error) {
28
+ throw invalidInput(`Batch input is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
29
+ }
30
+ if (!Array.isArray(parsed) || parsed.length === 0)
31
+ throw invalidInput('Batch input must be a non-empty JSON array');
32
+ if (parsed.length > MAX_BATCH_ITEMS)
33
+ throw invalidInput(`Batch input has ${parsed.length} items; the limit is ${MAX_BATCH_ITEMS}`);
34
+ return parsed.map((entry, index) => {
35
+ const record = Array.isArray(entry) ? { args: entry } : entry;
36
+ const args = record && typeof record === 'object' ? record.args : undefined;
37
+ if (!Array.isArray(args) || args.length === 0 || !args.every((arg) => typeof arg === 'string')) {
38
+ throw invalidInput(`Batch item ${index} must be a non-empty array of strings or an object with "args"`, { index });
39
+ }
40
+ const id = record.id;
41
+ if (id !== undefined && typeof id !== 'string')
42
+ throw invalidInput(`Batch item ${index} has a non-string "id"`, { index });
43
+ const reserved = args.find((arg) => RESERVED_ITEM_OPTIONS.includes(arg) || arg.startsWith('--format='));
44
+ if (reserved)
45
+ throw invalidInput(`Batch item ${index} may not set ${reserved}; the batch returns JSON for every item`, { index });
46
+ if (commandExecutionTarget(args) !== 'daemon') {
47
+ throw invalidInput(`Batch item ${index} (${args.join(' ')}) is not a data command; only note, todo, tag, box, view, prop, template, prompt, graph, storage and meta commands can be batched`, { index });
48
+ }
49
+ const requestId = record.request_id;
50
+ if (requestId !== undefined) {
51
+ if (typeof requestId !== 'string')
52
+ throw invalidInput(`Batch item ${index} has a non-string "request_id"`, { index });
53
+ validateRequestId(requestId);
54
+ if (args.includes('--request-id'))
55
+ throw invalidInput(`Batch item ${index} sets both "request_id" and --request-id`, { index });
56
+ }
57
+ return {
58
+ index,
59
+ id: typeof id === 'string' ? id : null,
60
+ args: requestId === undefined ? [...args] : [...args, '--request-id', requestId],
61
+ };
62
+ });
63
+ }
64
+ /** Adds the batch's own sync options to an item that does not set them itself. */
65
+ export function itemArgv(args, globals) {
66
+ const argv = [...args];
67
+ if (globals.offline && !argv.includes('--offline'))
68
+ argv.push('--offline');
69
+ if (globals.requireSync && !argv.includes('--require-sync'))
70
+ argv.push('--require-sync');
71
+ if (typeof globals.syncTimeout === 'string' && !argv.some((arg) => arg === '--sync-timeout' || arg.startsWith('--sync-timeout='))) {
72
+ argv.push('--sync-timeout', globals.syncTimeout);
73
+ }
74
+ return [...argv, '--format', 'json'];
75
+ }
76
+ /** Reads the JSON document a command printed, skipping any plain-text lines before it. */
77
+ function parseJsonOutput(text) {
78
+ const start = text.search(/^[{[]/m);
79
+ if (start === -1)
80
+ return null;
81
+ try {
82
+ const value = JSON.parse(text.slice(start));
83
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ export function toBatchItemResult(item, result) {
90
+ const base = { index: item.index, id: item.id, args: item.args, exit_code: result.exitCode };
91
+ if (result.exitCode === 0) {
92
+ const envelope = parseJsonOutput(result.stdout);
93
+ return {
94
+ ...base,
95
+ ok: true,
96
+ data: envelope && 'data' in envelope ? envelope.data : (envelope ?? result.stdout),
97
+ ...(envelope?._notice ? { notice: envelope._notice } : {}),
98
+ };
99
+ }
100
+ const payload = parseJsonOutput(result.stderr);
101
+ return {
102
+ ...base,
103
+ ok: false,
104
+ error: payload?.error ?? payload ?? { code: 'COMMAND_FAILED', message: result.stderr.trim() || `exit code ${result.exitCode}` },
105
+ };
106
+ }
107
+ function errorResult(item, error) {
108
+ const dinox = error instanceof DinoxError ? error : null;
109
+ return {
110
+ index: item.index,
111
+ id: item.id,
112
+ args: item.args,
113
+ ok: false,
114
+ exit_code: dinox?.exitCode ?? 1,
115
+ error: {
116
+ code: dinox?.code ?? 'INTERNAL_ERROR',
117
+ message: error instanceof Error ? error.message : String(error),
118
+ ...(dinox?.suggestion ? { suggestion: dinox.suggestion } : {}),
119
+ ...(dinox?.details !== undefined ? { details: dinox.details } : {}),
120
+ },
121
+ };
122
+ }
123
+ async function readStdin() {
124
+ assertStdinAvailable();
125
+ if (process.stdin.isTTY) {
126
+ throw invalidInput('dino batch reads a JSON array from stdin; pipe it in or pass --input <file>');
127
+ }
128
+ const chunks = [];
129
+ for await (const chunk of process.stdin)
130
+ chunks.push(Buffer.from(chunk));
131
+ return Buffer.concat(chunks).toString('utf8');
132
+ }
133
+ const batchDependencies = {
134
+ readStdin,
135
+ readFile: (file) => fs.readFile(resolveUserPath(file), 'utf8'),
136
+ execute: executeCommandLine,
137
+ };
138
+ export async function runBatch(context, overrides = {}) {
139
+ const deps = { ...batchDependencies, ...overrides };
140
+ const input = typeof context.options.input === 'string' && context.options.input !== '-'
141
+ ? await deps.readFile(context.options.input)
142
+ : await deps.readStdin();
143
+ const items = parseBatchItems(input);
144
+ const stopOnError = Boolean(context.options.stopOnError);
145
+ const results = [];
146
+ for (const item of items) {
147
+ let result;
148
+ try {
149
+ result = toBatchItemResult(item, await deps.execute(itemArgv(item.args, context.globals)));
150
+ }
151
+ catch (error) {
152
+ result = errorResult(item, error);
153
+ }
154
+ results.push(result);
155
+ if (!result.ok && stopOnError)
156
+ break;
157
+ }
158
+ const failed = results.filter((result) => !result.ok).length;
159
+ const summary = {
160
+ total: items.length,
161
+ succeeded: results.length - failed,
162
+ failed,
163
+ skipped: items.length - results.length,
164
+ };
165
+ if (getRequestedStructuredOutputFormat()) {
166
+ printSuccess({ ok: failed === 0, summary, results });
167
+ return;
168
+ }
169
+ for (const result of results) {
170
+ const label = result.id ? `${result.id} ` : '';
171
+ const error = result.error;
172
+ printHumanReadableLine(`#${result.index} ${label}${result.ok ? 'ok' : `failed (${error?.code ?? 'ERROR'}: ${error?.message ?? ''})`}\t${result.args.join(' ')}`);
173
+ }
174
+ printHumanReadableLine(`${summary.succeeded} succeeded, ${summary.failed} failed, ${summary.skipped} skipped of ${summary.total}.`);
175
+ }
176
+ export const batchCommandDescriptor = {
177
+ name: 'batch',
178
+ description: 'Run several data commands from one JSON array on stdin or a file and return every result',
179
+ options: [
180
+ { flags: '--input <file>', description: 'Read the JSON array from this file instead of stdin (- for stdin)' },
181
+ { flags: '--stop-on-error', description: 'Stop at the first failed item; later items are reported as skipped', type: 'boolean' },
182
+ ],
183
+ schema: {
184
+ path: 'batch',
185
+ risk: 'write',
186
+ auth: 'required',
187
+ globalOptions: [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS],
188
+ output: {
189
+ type: 'structured',
190
+ summary: 'Per-item results in input order with a success summary; each item carries the command\'s own data or structured error',
191
+ fields: ['ok', 'summary', 'results'],
192
+ },
193
+ examples: [
194
+ 'echo \'[["note","search","AI","--limit","5"],["todo","search","--status","uncompleted"]]\' | dino batch --format json',
195
+ 'dino batch --input ops.json --stop-on-error --format json',
196
+ ],
197
+ },
198
+ action: (context) => runBatch(context),
199
+ };
200
+ export function registerBatchCommand(program) {
201
+ registerStandaloneCommand(program, batchCommandDescriptor);
202
+ }