@dinoxx/dinox-cli 2.4.0 → 2.6.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 (42) hide show
  1. package/dist/commands/gateway/index.js +26 -4
  2. package/dist/commands/schema/renderSkills.js +1 -1
  3. package/dist/daemon/ensure.js +10 -19
  4. package/dist/daemon/env.d.ts +8 -0
  5. package/dist/daemon/env.js +81 -0
  6. package/dist/daemon/index.d.ts +27 -0
  7. package/dist/daemon/index.js +240 -57
  8. package/dist/daemon/launch.d.ts +27 -0
  9. package/dist/daemon/launch.js +87 -0
  10. package/dist/daemon/log.d.ts +12 -0
  11. package/dist/daemon/log.js +44 -0
  12. package/dist/daemon/server.js +18 -1
  13. package/dist/daemon/supervisor.d.ts +59 -0
  14. package/dist/daemon/supervisor.js +253 -0
  15. package/dist/daemon/types.d.ts +2 -0
  16. package/dist/gateway/adapters/acp.d.ts +10 -5
  17. package/dist/gateway/adapters/acp.js +30 -13
  18. package/dist/gateway/adapters/index.js +3 -2
  19. package/dist/gateway/config.d.ts +11 -1
  20. package/dist/gateway/config.js +38 -0
  21. package/dist/gateway/conversationContext.d.ts +44 -0
  22. package/dist/gateway/conversationContext.js +75 -0
  23. package/dist/gateway/deviceHost.d.ts +10 -1
  24. package/dist/gateway/deviceHost.js +119 -8
  25. package/dist/gateway/keepAwake.d.ts +24 -0
  26. package/dist/gateway/keepAwake.js +64 -0
  27. package/dist/gateway/mcpServer.js +20 -0
  28. package/dist/gateway/protocol.d.ts +88 -1
  29. package/dist/gateway/protocol.js +55 -0
  30. package/dist/gateway/runExecutor.d.ts +24 -0
  31. package/dist/gateway/runExecutor.js +118 -12
  32. package/dist/gateway/service.d.ts +4 -0
  33. package/dist/gateway/service.js +44 -15
  34. package/dist/gateway/threadStore.d.ts +71 -6
  35. package/dist/gateway/threadStore.js +189 -22
  36. package/dist/gateway/types.d.ts +22 -3
  37. package/dist/gateway/types.js +3 -2
  38. package/dist/gateway/workspaceFolders.d.ts +33 -0
  39. package/dist/gateway/workspaceFolders.js +129 -0
  40. package/dist/utils/errorCodes.d.ts +8 -0
  41. package/dist/utils/errorCodes.js +2 -0
  42. package/package.json +1 -1
@@ -1,11 +1,12 @@
1
- import { isDaemonRunning, sendDaemonRequest } from '../../daemon/client.js';
1
+ import { getDaemonInfo, isDaemonRunning, sendDaemonRequest } from '../../daemon/client.js';
2
+ import { getDaemonService } from '../../daemon/supervisor.js';
2
3
  import { ensureDaemonRunning } from '../../daemon/ensure.js';
3
4
  import { loadConfig } from '../../config/store.js';
4
5
  import { addGateway, adoptDetectedAgent, getGatewaysFilePath, loadGateways, removeGateway, } from '../../gateway/config.js';
5
6
  import { detectAcpAgents, inferAgentId } from '../../gateway/detect.js';
6
7
  import { serveDinoxMcpOverStdio } from '../../gateway/mcpServer.js';
7
8
  import { DinoxError } from '../../utils/errors.js';
8
- import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess } from '../../utils/output.js';
9
+ import { getRequestedStructuredOutputFormat, printHumanReadableLine, printHumanReadableWarning, printSuccess, } from '../../utils/output.js';
9
10
  import { registerCommandGroup } from '../descriptor/register.js';
10
11
  import { FORMAT_OPTIONS } from '../schema/globalOptions.js';
11
12
  const GATEWAY_KINDS = ['acp', 'echo'];
@@ -42,13 +43,32 @@ function describeGateway(gateway) {
42
43
  enabled: gateway.enabled,
43
44
  };
44
45
  }
46
+ /**
47
+ * Gateways live exactly as long as the daemon. One not owned by a service
48
+ * manager belongs to the shell or agent session that started it and goes
49
+ * offline with it, so say so where it matters (stderr, so JSON stays clean).
50
+ */
51
+ async function warnIfDaemonUnmanaged() {
52
+ if (!getDaemonService())
53
+ return;
54
+ const info = await getDaemonInfo();
55
+ if (info && !info.supervisor) {
56
+ printHumanReadableWarning('warning: gateways run in a daemon owned by the shell or agent session that started it, and go offline when that session ends. Run `dino daemon install` to keep them online.');
57
+ }
58
+ }
45
59
  /** Applies the account's gateways to the running daemon, starting it when needed. */
46
60
  async function applyToDaemon() {
61
+ let gateways;
47
62
  if (!(await isDaemonRunning())) {
48
63
  await ensureDaemonRunning();
49
- return (await sendDaemonRequest('gateway.status')).gateways;
64
+ gateways = (await sendDaemonRequest('gateway.status')).gateways;
65
+ }
66
+ else {
67
+ gateways = (await sendDaemonRequest('gateway.reload', undefined, { timeoutMs: 30_000 })).gateways;
50
68
  }
51
- return (await sendDaemonRequest('gateway.reload', undefined, { timeoutMs: 30_000 })).gateways;
69
+ if (gateways.length > 0)
70
+ await warnIfDaemonUnmanaged();
71
+ return gateways;
52
72
  }
53
73
  function printStatuses(statuses) {
54
74
  if (statuses.length === 0) {
@@ -162,6 +182,8 @@ async function runRemove(context) {
162
182
  async function runStatus() {
163
183
  await ensureDaemonRunning();
164
184
  const { gateways } = (await sendDaemonRequest('gateway.status'));
185
+ if (gateways.length > 0)
186
+ await warnIfDaemonUnmanaged();
165
187
  if (getRequestedStructuredOutputFormat()) {
166
188
  printSuccess({ gateways });
167
189
  return;
@@ -4,7 +4,7 @@ import { listErrorCodes } from '../../utils/errorCodes.js';
4
4
  import { listCommandSchemas } from './spec.js';
5
5
  const SECTION_ORDER = [
6
6
  { title: 'Auth', paths: ['auth.login', 'auth.logout', 'auth.status'] },
7
- { title: 'Daemon', paths: ['daemon.start', 'daemon.status', 'daemon.restart', 'daemon.stop'] },
7
+ { title: 'Daemon', paths: ['daemon.start', 'daemon.status', 'daemon.restart', 'daemon.stop', 'daemon.install', 'daemon.uninstall'] },
8
8
  {
9
9
  title: 'Agent Gateways',
10
10
  paths: ['gateway.detect', 'gateway.add', 'gateway.list', 'gateway.remove', 'gateway.status', 'gateway.reload', 'gateway.mcp'],
@@ -1,7 +1,8 @@
1
- import { spawn } from 'child_process';
2
1
  import { getDaemonInfo, isDaemonFresh, waitForFreshDaemon } from './client.js';
3
2
  import { stopDaemonLocked } from './index.js';
4
3
  import { DEFAULT_DAEMON_PORT } from './types.js';
4
+ import { launchDaemon } from './launch.js';
5
+ import { getDaemonLogPath } from './log.js';
5
6
  import { acquireDaemonStartLock, } from './pid.js';
6
7
  import { loadConfig } from '../config/store.js';
7
8
  import { fingerprintDaemonRuntimeConfig, resolveConfig } from '../config/resolve.js';
@@ -9,8 +10,8 @@ import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
9
10
  import { getPackageVersion } from '../utils/version.js';
10
11
  import { printHumanReadableWarning } from '../utils/output.js';
11
12
  import { withAsyncRelease } from '../utils/asyncRelease.js';
13
+ import { DinoxError } from '../utils/errors.js';
12
14
  const DAEMON_READY_TIMEOUT_MS = 10_000;
13
- const DAEMON_CHILD_ENV = 'DINOX_INTERNAL_DAEMON_CHILD';
14
15
  async function hydrateDaemonIdentity(baseConfig) {
15
16
  const { config } = await hydrateResolvedAuthIdentity(baseConfig, { timeoutMs: baseConfig.sync.timeoutMs });
16
17
  return {
@@ -19,20 +20,6 @@ async function hydrateDaemonIdentity(baseConfig) {
19
20
  runtimeConfigFingerprint: fingerprintDaemonRuntimeConfig(config),
20
21
  };
21
22
  }
22
- function spawnDetachedDaemon() {
23
- const args = ['daemon', 'start', '--port', String(DEFAULT_DAEMON_PORT), '--no-detach'];
24
- const entrypoint = process.argv[1];
25
- if (!entrypoint) {
26
- throw new Error('Cannot determine the CLI entrypoint for daemon startup');
27
- }
28
- const child = spawn(process.execPath, [...process.execArgv, entrypoint, ...args], {
29
- detached: true,
30
- stdio: 'ignore',
31
- env: { ...process.env, [DAEMON_CHILD_ENV]: '1' },
32
- });
33
- child.unref();
34
- return child;
35
- }
36
23
  function matchesRuntimeTokenBinding(info, version, config) {
37
24
  return Boolean(config.auth.authorization
38
25
  && info.authenticated
@@ -73,13 +60,17 @@ async function ensureDaemonRunningUnlocked() {
73
60
  else {
74
61
  printHumanReadableWarning('Starting daemon...');
75
62
  }
76
- const child = spawnDetachedDaemon();
63
+ const launch = await launchDaemon(DEFAULT_DAEMON_PORT);
77
64
  if (await waitForFreshDaemon(expectedVersion, expectedIdentity.userId, expectedIdentity.tokenFingerprint, expectedIdentity.runtimeConfigFingerprint, DAEMON_READY_TIMEOUT_MS)) {
78
65
  printHumanReadableWarning('Daemon started');
79
66
  return;
80
67
  }
81
- child.kill('SIGTERM');
82
- throw new Error('Failed to start daemon');
68
+ launch.abort();
69
+ throw new DinoxError('Failed to start daemon', {
70
+ code: 'DAEMON_START_FAILED',
71
+ details: { service: launch.service, logPath: getDaemonLogPath() },
72
+ suggestion: `Inspect ${getDaemonLogPath()} for the daemon's startup error.`,
73
+ });
83
74
  }
84
75
  export async function ensureDaemonRunning() {
85
76
  await withAsyncRelease(() => acquireDaemonStartLock(), () => ensureDaemonRunningUnlocked());
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Makes sure the Node runtime running the CLI is on PATH, so agents installed
3
+ * as `#!/usr/bin/env node` scripts still start under a service manager's
4
+ * minimal PATH.
5
+ */
6
+ export declare function withRuntimeOnPath(pathValue: string | undefined, execPath?: string): string;
7
+ /** The environment a daemon process starts with: an allow-list of the caller's. */
8
+ export declare function buildDaemonEnv(source?: NodeJS.ProcessEnv, execPath?: string): Record<string, string>;
@@ -0,0 +1,81 @@
1
+ import path from 'node:path';
2
+ /**
3
+ * Variables the daemon may inherit from whoever starts it. Everything else is
4
+ * dropped: the daemon outlives its caller, so it must not keep a coding agent's
5
+ * session markers (CLAUDECODE, CLAUDE_CODE_SESSION_ID, ...), credentials
6
+ * (ANTHROPIC_AUTH_TOKEN, ...) or app identity (XPC_SERVICE_NAME,
7
+ * __CFBundleIdentifier) and hand them on to the agents its gateways spawn.
8
+ * Agent credentials belong in `dino gateway add --env KEY=VALUE`.
9
+ */
10
+ const INHERITED_NAMES = new Set([
11
+ 'HOME',
12
+ 'USER',
13
+ 'LOGNAME',
14
+ 'SHELL',
15
+ 'PATH',
16
+ 'TMPDIR',
17
+ 'LANG',
18
+ 'TZ',
19
+ 'XDG_CONFIG_HOME',
20
+ 'XDG_DATA_HOME',
21
+ 'XDG_STATE_HOME',
22
+ 'XDG_CACHE_HOME',
23
+ 'XDG_RUNTIME_DIR',
24
+ 'HTTP_PROXY',
25
+ 'HTTPS_PROXY',
26
+ 'ALL_PROXY',
27
+ 'NO_PROXY',
28
+ 'NODE_EXTRA_CA_CERTS',
29
+ 'SSL_CERT_FILE',
30
+ 'SSL_CERT_DIR',
31
+ 'POWERSYNC_TOKEN_ENDPOINT',
32
+ // Windows process essentials.
33
+ 'APPDATA',
34
+ 'LOCALAPPDATA',
35
+ 'USERPROFILE',
36
+ 'HOMEDRIVE',
37
+ 'HOMEPATH',
38
+ 'SystemRoot',
39
+ 'windir',
40
+ 'ComSpec',
41
+ 'PATHEXT',
42
+ 'TEMP',
43
+ 'TMP',
44
+ 'ProgramData',
45
+ 'ProgramFiles',
46
+ 'ProgramFiles(x86)',
47
+ ]);
48
+ const INHERITED_PREFIXES = ['DINOX_', 'LC_'];
49
+ /** Internal markers the parent sets for the child; never inherited from outside. */
50
+ const INTERNAL_NAMES = new Set(['DINOX_INTERNAL_DAEMON_CHILD', 'DINOX_DAEMON_SUPERVISOR']);
51
+ const INHERITED_UPPER = new Set([...INHERITED_NAMES].map((name) => name.toUpperCase()));
52
+ // Compared upper-cased: Windows names are case-insensitive (`Path`, `SystemRoot`).
53
+ function isInherited(name) {
54
+ const upper = name.toUpperCase();
55
+ if (INTERNAL_NAMES.has(upper))
56
+ return false;
57
+ return INHERITED_UPPER.has(upper) || INHERITED_PREFIXES.some((prefix) => upper.startsWith(prefix));
58
+ }
59
+ /**
60
+ * Makes sure the Node runtime running the CLI is on PATH, so agents installed
61
+ * as `#!/usr/bin/env node` scripts still start under a service manager's
62
+ * minimal PATH.
63
+ */
64
+ export function withRuntimeOnPath(pathValue, execPath = process.execPath) {
65
+ const runtimeDir = path.dirname(execPath);
66
+ const entries = (pathValue ?? '').split(path.delimiter).filter(Boolean);
67
+ if (!entries.includes(runtimeDir))
68
+ entries.unshift(runtimeDir);
69
+ return entries.join(path.delimiter);
70
+ }
71
+ /** The environment a daemon process starts with: an allow-list of the caller's. */
72
+ export function buildDaemonEnv(source = process.env, execPath = process.execPath) {
73
+ const env = {};
74
+ for (const [name, value] of Object.entries(source)) {
75
+ if (typeof value === 'string' && isInherited(name))
76
+ env[name] = value;
77
+ }
78
+ const pathKey = Object.keys(env).find((name) => name.toUpperCase() === 'PATH') ?? 'PATH';
79
+ env[pathKey] = withRuntimeOnPath(env[pathKey], execPath);
80
+ return env;
81
+ }
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import type { CommandGroupDescriptor } from '../commands/descriptor/types.js';
3
+ import { type DaemonServiceKind } from './supervisor.js';
3
4
  type DaemonStartResult = {
4
5
  running: boolean;
5
6
  port: number;
@@ -7,6 +8,8 @@ type DaemonStartResult = {
7
8
  transport: 'socket';
8
9
  detached: boolean;
9
10
  alreadyRunning: boolean;
11
+ /** Service manager that started it; null for a detached child or foreground run. */
12
+ service: DaemonServiceKind | null;
10
13
  };
11
14
  type DaemonStartOptions = {
12
15
  quiet?: boolean;
@@ -17,6 +20,20 @@ type DaemonStopResult = {
17
20
  wasRunning: boolean;
18
21
  stalePidFile: boolean;
19
22
  };
23
+ type DaemonInstallResult = {
24
+ installed: boolean;
25
+ service: DaemonServiceKind;
26
+ label: string;
27
+ definitionPath: string;
28
+ logPath: string;
29
+ running: boolean;
30
+ replacedRunning: boolean;
31
+ };
32
+ type DaemonUninstallResult = {
33
+ uninstalled: boolean;
34
+ service: DaemonServiceKind | null;
35
+ definitionPath: string | null;
36
+ };
20
37
  export declare function startDaemonLocked(port: number, detached: boolean, options?: DaemonStartOptions): Promise<DaemonStartResult>;
21
38
  /** Caller must hold the daemon start lock. */
22
39
  export declare function stopDaemonLocked(): Promise<DaemonStopResult>;
@@ -31,6 +48,7 @@ export declare function restartDaemonLocked(port?: number, options?: {
31
48
  transport: 'socket';
32
49
  detached: boolean;
33
50
  alreadyRunning: boolean;
51
+ service: DaemonServiceKind | null;
34
52
  }>;
35
53
  export declare function restartDaemon(port?: number, options?: {
36
54
  quiet?: boolean;
@@ -42,7 +60,16 @@ export declare function restartDaemon(port?: number, options?: {
42
60
  transport: 'socket';
43
61
  detached: boolean;
44
62
  alreadyRunning: boolean;
63
+ service: DaemonServiceKind | null;
45
64
  }>;
65
+ /**
66
+ * Hands the daemon to the platform's service manager so it no longer belongs
67
+ * to whichever process first started it, then waits until the managed
68
+ * instance answers with the current identity.
69
+ */
70
+ export declare function installDaemonService(port?: number): Promise<DaemonInstallResult>;
71
+ /** Stops the managed daemon and removes its service definition. */
72
+ export declare function uninstallDaemonService(): Promise<DaemonUninstallResult>;
46
73
  export declare const daemonCommandGroup: CommandGroupDescriptor;
47
74
  export declare function registerDaemonCommands(program: Command): void;
48
75
  export {};