@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.
- package/dist/commands/gateway/index.js +26 -4
- package/dist/commands/schema/renderSkills.js +1 -1
- package/dist/daemon/ensure.js +10 -19
- package/dist/daemon/env.d.ts +8 -0
- package/dist/daemon/env.js +81 -0
- package/dist/daemon/index.d.ts +27 -0
- package/dist/daemon/index.js +240 -57
- package/dist/daemon/launch.d.ts +27 -0
- package/dist/daemon/launch.js +87 -0
- package/dist/daemon/log.d.ts +12 -0
- package/dist/daemon/log.js +44 -0
- package/dist/daemon/server.js +18 -1
- package/dist/daemon/supervisor.d.ts +59 -0
- package/dist/daemon/supervisor.js +253 -0
- package/dist/daemon/types.d.ts +2 -0
- package/dist/gateway/adapters/acp.d.ts +10 -5
- package/dist/gateway/adapters/acp.js +30 -13
- package/dist/gateway/adapters/index.js +3 -2
- package/dist/gateway/config.d.ts +11 -1
- package/dist/gateway/config.js +38 -0
- package/dist/gateway/conversationContext.d.ts +44 -0
- package/dist/gateway/conversationContext.js +75 -0
- package/dist/gateway/deviceHost.d.ts +10 -1
- package/dist/gateway/deviceHost.js +119 -8
- package/dist/gateway/keepAwake.d.ts +24 -0
- package/dist/gateway/keepAwake.js +64 -0
- package/dist/gateway/mcpServer.js +20 -0
- package/dist/gateway/protocol.d.ts +88 -1
- package/dist/gateway/protocol.js +55 -0
- package/dist/gateway/runExecutor.d.ts +24 -0
- package/dist/gateway/runExecutor.js +118 -12
- package/dist/gateway/service.d.ts +4 -0
- package/dist/gateway/service.js +44 -15
- package/dist/gateway/threadStore.d.ts +71 -6
- package/dist/gateway/threadStore.js +189 -22
- package/dist/gateway/types.d.ts +22 -3
- package/dist/gateway/types.js +3 -2
- package/dist/gateway/workspaceFolders.d.ts +33 -0
- package/dist/gateway/workspaceFolders.js +129 -0
- package/dist/utils/errorCodes.d.ts +8 -0
- package/dist/utils/errorCodes.js +2 -0
- package/package.json +1 -1
package/dist/gateway/types.d.ts
CHANGED
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
* Claude Code, Codex, ... over ACP) as remote devices the Dinox app can drive.
|
|
4
4
|
*
|
|
5
5
|
* The app sees each gateway as one `dinox.remote-device.v2` device. The agent
|
|
6
|
-
* owns its conversation context (gateway-session mode): each turn sends
|
|
7
|
-
*
|
|
6
|
+
* owns its conversation context (gateway-session mode): each turn sends the new
|
|
7
|
+
* user message plus any transcript messages its session has not seen (another
|
|
8
|
+
* agent answered meanwhile), while Dinox keeps the synced copy of the transcript.
|
|
8
9
|
*/
|
|
9
10
|
export type GatewayKind = 'acp' | 'echo';
|
|
11
|
+
/**
|
|
12
|
+
* When the agent must ask before acting: `ask` every time, or `auto_read`
|
|
13
|
+
* (reading and searching run without asking; edits and commands still ask).
|
|
14
|
+
*/
|
|
15
|
+
export type GatewayApprovalMode = 'ask' | 'auto_read';
|
|
10
16
|
/** Local-only gateway definition (never synced: it may carry commands and env). */
|
|
11
17
|
export type GatewayDefinition = {
|
|
12
18
|
/** Stable id; also the `c_robot.id` the app binds agents to. */
|
|
@@ -20,13 +26,22 @@ export type GatewayDefinition = {
|
|
|
20
26
|
/** ACP: executable started over stdio, e.g. `hermes` with args `["acp"]`. */
|
|
21
27
|
command?: string;
|
|
22
28
|
args?: string[];
|
|
23
|
-
/** Working directory
|
|
29
|
+
/** Working directory for new agent sessions (each session keeps the one it started in). */
|
|
24
30
|
cwd?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Directory of sessions bound before sessions recorded their own: the
|
|
33
|
+
* default directory as it was when it was first changed.
|
|
34
|
+
*/
|
|
35
|
+
previousCwd?: string;
|
|
25
36
|
/** Extra environment for the agent process (secrets stay on this machine). */
|
|
26
37
|
env?: Record<string, string>;
|
|
27
38
|
enabled: boolean;
|
|
28
39
|
/** Offer the read-only Dinox knowledge tools (MCP) to the agent; default true. */
|
|
29
40
|
dinoxTools?: boolean;
|
|
41
|
+
/** Default `ask`. */
|
|
42
|
+
approval?: GatewayApprovalMode;
|
|
43
|
+
/** Stays online but refuses new turns, so the owner can resume it from the app. */
|
|
44
|
+
paused?: boolean;
|
|
30
45
|
createdAt: string;
|
|
31
46
|
};
|
|
32
47
|
export type GatewayToolStatus = 'pending' | 'running' | 'completed' | 'failed';
|
|
@@ -82,7 +97,11 @@ export type GatewayStopReason = 'end_turn' | 'cancelled' | 'max_tokens' | 'refus
|
|
|
82
97
|
export type GatewayTurnInput = {
|
|
83
98
|
/** Agent session to continue; absent starts a new one. */
|
|
84
99
|
sessionId: string | null;
|
|
100
|
+
/** Working directory of the session (a new one starts here; a continued one is loaded with it). */
|
|
101
|
+
cwd: string;
|
|
85
102
|
prompt: string;
|
|
103
|
+
/** Prepended to the prompt when the turn starts a new session, e.g. the agent's instructions. */
|
|
104
|
+
sessionPreamble?: string;
|
|
86
105
|
/** Attachments the user sent with this turn (trusted, https). */
|
|
87
106
|
resources: Array<{
|
|
88
107
|
uri: string;
|
package/dist/gateway/types.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Claude Code, Codex, ... over ACP) as remote devices the Dinox app can drive.
|
|
4
4
|
*
|
|
5
5
|
* The app sees each gateway as one `dinox.remote-device.v2` device. The agent
|
|
6
|
-
* owns its conversation context (gateway-session mode): each turn sends
|
|
7
|
-
*
|
|
6
|
+
* owns its conversation context (gateway-session mode): each turn sends the new
|
|
7
|
+
* user message plus any transcript messages its session has not seen (another
|
|
8
|
+
* agent answered meanwhile), while Dinox keeps the synced copy of the transcript.
|
|
8
9
|
*/
|
|
9
10
|
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folders an agent may work in, chosen from the app. The rules keep a remote
|
|
3
|
+
* choice inside the owner's own files: an existing directory under the home
|
|
4
|
+
* directory, never the home directory itself, nothing hidden (`~/.ssh`) and
|
|
5
|
+
* nothing in `~/Library`. Not a security boundary on its own (an agent with a
|
|
6
|
+
* shell can still `cd`); it keeps a tap from pointing an agent somewhere wrong.
|
|
7
|
+
*/
|
|
8
|
+
export type WorkspaceFolderErrorCode = 'not_found' | 'not_allowed';
|
|
9
|
+
export declare class WorkspaceFolderError extends Error {
|
|
10
|
+
readonly code: WorkspaceFolderErrorCode;
|
|
11
|
+
constructor(code: WorkspaceFolderErrorCode, message: string);
|
|
12
|
+
}
|
|
13
|
+
export type WorkspaceFolder = {
|
|
14
|
+
name: string;
|
|
15
|
+
path: string;
|
|
16
|
+
/** Has a `.git` entry: likely a project the user works on. */
|
|
17
|
+
isProject: boolean;
|
|
18
|
+
};
|
|
19
|
+
/** The folder an agent may work in, resolved to its real path. */
|
|
20
|
+
export declare function resolveWorkspaceFolder(input: string, home?: string): Promise<string>;
|
|
21
|
+
/** `~/Dinox Agents/<agent name>`: a folder of the agent's own, offered as the default choice. */
|
|
22
|
+
export declare function agentHomeFolder(agentName: string, home?: string): string;
|
|
23
|
+
/** Creates the agent's own folder when missing; the only folder the app can create. */
|
|
24
|
+
export declare function ensureAgentHomeFolder(agentName: string, home?: string): Promise<string>;
|
|
25
|
+
/** The visible subfolders of a folder (the home folder included), for browsing. */
|
|
26
|
+
export declare function listWorkspaceFolders(input: string | null | undefined, home?: string): Promise<{
|
|
27
|
+
path: string;
|
|
28
|
+
parent: string | null;
|
|
29
|
+
folders: WorkspaceFolder[];
|
|
30
|
+
truncated: boolean;
|
|
31
|
+
}>;
|
|
32
|
+
/** Folders worth offering first: the given candidates that exist and are allowed, deduplicated. */
|
|
33
|
+
export declare function existingWorkspaceFolders(candidates: Array<string | null | undefined>, home?: string): Promise<WorkspaceFolder[]>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export class WorkspaceFolderError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = 'WorkspaceFolderError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const MAX_FOLDERS = 200;
|
|
13
|
+
/** Home-level folders that hold system data rather than the user's work. */
|
|
14
|
+
const SYSTEM_FOLDERS = new Set(['Library', 'Applications']);
|
|
15
|
+
const SKIPPED_FOLDERS = new Set(['node_modules']);
|
|
16
|
+
const AGENT_HOME_PARENT = 'Dinox Agents';
|
|
17
|
+
async function realHome(home) {
|
|
18
|
+
return fs.realpath(home).catch(() => path.resolve(home));
|
|
19
|
+
}
|
|
20
|
+
function expandHome(input, home) {
|
|
21
|
+
if (input === '~')
|
|
22
|
+
return home;
|
|
23
|
+
if (input.startsWith('~/'))
|
|
24
|
+
return path.join(home, input.slice(2));
|
|
25
|
+
return input;
|
|
26
|
+
}
|
|
27
|
+
/** Segments of `target` below `home`, or null when it is not inside it. */
|
|
28
|
+
function segmentsBelow(home, target) {
|
|
29
|
+
const relative = path.relative(home, target);
|
|
30
|
+
if (relative === '')
|
|
31
|
+
return [];
|
|
32
|
+
if (relative.startsWith('..') || path.isAbsolute(relative))
|
|
33
|
+
return null;
|
|
34
|
+
return relative.split(path.sep);
|
|
35
|
+
}
|
|
36
|
+
function isAllowedBelowHome(segments) {
|
|
37
|
+
if (segments.some((segment) => segment.startsWith('.')))
|
|
38
|
+
return false;
|
|
39
|
+
return !SYSTEM_FOLDERS.has(segments[0] ?? '');
|
|
40
|
+
}
|
|
41
|
+
async function resolveDirectory(input, home) {
|
|
42
|
+
const expanded = expandHome(input.trim(), home);
|
|
43
|
+
if (!path.isAbsolute(expanded)) {
|
|
44
|
+
throw new WorkspaceFolderError('not_allowed', 'Folder must be an absolute path');
|
|
45
|
+
}
|
|
46
|
+
// Refused by its name whether or not it exists (`~/.ssh`), so the answer
|
|
47
|
+
// never depends on what this computer happens to have.
|
|
48
|
+
const named = segmentsBelow(path.resolve(home), path.resolve(expanded));
|
|
49
|
+
if (named !== null && !isAllowedBelowHome(named)) {
|
|
50
|
+
throw new WorkspaceFolderError('not_allowed', 'This folder cannot be used');
|
|
51
|
+
}
|
|
52
|
+
let real;
|
|
53
|
+
try {
|
|
54
|
+
real = await fs.realpath(expanded);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new WorkspaceFolderError('not_found', 'Folder does not exist');
|
|
58
|
+
}
|
|
59
|
+
const stat = await fs.stat(real);
|
|
60
|
+
if (!stat.isDirectory())
|
|
61
|
+
throw new WorkspaceFolderError('not_found', 'Not a folder');
|
|
62
|
+
const segments = segmentsBelow(await realHome(home), real);
|
|
63
|
+
if (segments === null || !isAllowedBelowHome(segments)) {
|
|
64
|
+
throw new WorkspaceFolderError('not_allowed', 'This folder cannot be used');
|
|
65
|
+
}
|
|
66
|
+
return { real, segments };
|
|
67
|
+
}
|
|
68
|
+
/** The folder an agent may work in, resolved to its real path. */
|
|
69
|
+
export async function resolveWorkspaceFolder(input, home = os.homedir()) {
|
|
70
|
+
const { real, segments } = await resolveDirectory(input, home);
|
|
71
|
+
if (segments.length === 0) {
|
|
72
|
+
throw new WorkspaceFolderError('not_allowed', 'Choose a folder inside your home folder');
|
|
73
|
+
}
|
|
74
|
+
return real;
|
|
75
|
+
}
|
|
76
|
+
/** `~/Dinox Agents/<agent name>`: a folder of the agent's own, offered as the default choice. */
|
|
77
|
+
export function agentHomeFolder(agentName, home = os.homedir()) {
|
|
78
|
+
const safeName = agentName.replace(/[\u0000-\u001f\u007f/\\:*?"<>|]/g, '').trim() || 'Agent';
|
|
79
|
+
return path.join(home, AGENT_HOME_PARENT, safeName.slice(0, 64));
|
|
80
|
+
}
|
|
81
|
+
/** Creates the agent's own folder when missing; the only folder the app can create. */
|
|
82
|
+
export async function ensureAgentHomeFolder(agentName, home = os.homedir()) {
|
|
83
|
+
const folder = agentHomeFolder(agentName, home);
|
|
84
|
+
await fs.mkdir(folder, { recursive: true, mode: 0o755 });
|
|
85
|
+
return resolveWorkspaceFolder(folder, home);
|
|
86
|
+
}
|
|
87
|
+
async function hasGit(folder) {
|
|
88
|
+
return fs
|
|
89
|
+
.access(path.join(folder, '.git'))
|
|
90
|
+
.then(() => true)
|
|
91
|
+
.catch(() => false);
|
|
92
|
+
}
|
|
93
|
+
/** The visible subfolders of a folder (the home folder included), for browsing. */
|
|
94
|
+
export async function listWorkspaceFolders(input, home = os.homedir()) {
|
|
95
|
+
const { real, segments } = await resolveDirectory(input?.trim() || home, home);
|
|
96
|
+
const entries = await fs.readdir(real, { withFileTypes: true });
|
|
97
|
+
const names = entries
|
|
98
|
+
.filter((entry) => entry.isDirectory())
|
|
99
|
+
.map((entry) => entry.name)
|
|
100
|
+
.filter((name) => !name.startsWith('.') && !SKIPPED_FOLDERS.has(name))
|
|
101
|
+
.filter((name) => segments.length > 0 || !SYSTEM_FOLDERS.has(name))
|
|
102
|
+
.sort((a, b) => a.localeCompare(b));
|
|
103
|
+
const shown = names.slice(0, MAX_FOLDERS);
|
|
104
|
+
const folders = await Promise.all(shown.map(async (name) => {
|
|
105
|
+
const folder = path.join(real, name);
|
|
106
|
+
return { name, path: folder, isProject: await hasGit(folder) };
|
|
107
|
+
}));
|
|
108
|
+
return {
|
|
109
|
+
path: real,
|
|
110
|
+
parent: segments.length === 0 ? null : path.dirname(real),
|
|
111
|
+
folders,
|
|
112
|
+
truncated: names.length > shown.length,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Folders worth offering first: the given candidates that exist and are allowed, deduplicated. */
|
|
116
|
+
export async function existingWorkspaceFolders(candidates, home = os.homedir()) {
|
|
117
|
+
const seen = new Set();
|
|
118
|
+
const folders = [];
|
|
119
|
+
for (const candidate of candidates) {
|
|
120
|
+
if (!candidate)
|
|
121
|
+
continue;
|
|
122
|
+
const folder = await resolveWorkspaceFolder(candidate, home).catch(() => null);
|
|
123
|
+
if (!folder || seen.has(folder))
|
|
124
|
+
continue;
|
|
125
|
+
seen.add(folder);
|
|
126
|
+
folders.push({ name: path.basename(folder), path: folder, isProject: await hasGit(folder) });
|
|
127
|
+
}
|
|
128
|
+
return folders;
|
|
129
|
+
}
|
|
@@ -63,6 +63,14 @@ export declare const ERROR_CODES: {
|
|
|
63
63
|
readonly exitCode: 1;
|
|
64
64
|
readonly description: "The daemon did not release database ownership in time.";
|
|
65
65
|
};
|
|
66
|
+
readonly DAEMON_SERVICE_FAILED: {
|
|
67
|
+
readonly exitCode: 1;
|
|
68
|
+
readonly description: "The service manager (launchd / systemd) rejected a daemon service operation.";
|
|
69
|
+
};
|
|
70
|
+
readonly DAEMON_SERVICE_UNSUPPORTED: {
|
|
71
|
+
readonly exitCode: 5;
|
|
72
|
+
readonly description: "The daemon cannot run as a login service on this platform or with this auth setup.";
|
|
73
|
+
};
|
|
66
74
|
readonly DAEMON_STALE: {
|
|
67
75
|
readonly exitCode: 5;
|
|
68
76
|
readonly description: "The daemon state is stale and needs a restart.";
|
package/dist/utils/errorCodes.js
CHANGED
|
@@ -21,6 +21,8 @@ export const ERROR_CODES = {
|
|
|
21
21
|
DAEMON_LEGACY_UNVERIFIED: { exitCode: 1, description: 'A daemon from an older CLI could not be verified safely.' },
|
|
22
22
|
DAEMON_RELEASE_FAILED: { exitCode: 1, description: 'The daemon could not release database ownership.' },
|
|
23
23
|
DAEMON_RELEASE_TIMEOUT: { exitCode: 1, description: 'The daemon did not release database ownership in time.' },
|
|
24
|
+
DAEMON_SERVICE_FAILED: { exitCode: 1, description: 'The service manager (launchd / systemd) rejected a daemon service operation.' },
|
|
25
|
+
DAEMON_SERVICE_UNSUPPORTED: { exitCode: 5, description: 'The daemon cannot run as a login service on this platform or with this auth setup.' },
|
|
24
26
|
DAEMON_STALE: { exitCode: 5, description: 'The daemon state is stale and needs a restart.' },
|
|
25
27
|
DAEMON_START_FAILED: { exitCode: 5, description: 'The daemon process failed to start.' },
|
|
26
28
|
DAEMON_START_TIMEOUT: { exitCode: 1, description: 'The daemon did not become ready in time.' },
|