@dinoxx/dinox-cli 2.3.0 → 2.5.2
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/README.md +17 -3
- package/dist/cli.js +2 -0
- package/dist/commands/gateway/index.d.ts +4 -0
- package/dist/commands/gateway/index.js +300 -0
- package/dist/commands/schema/renderSkills.js +4 -0
- package/dist/commands/schema/spec.js +2 -0
- package/dist/config/paths.d.ts +1 -0
- package/dist/config/paths.js +1 -1
- package/dist/daemon/server.d.ts +1 -0
- package/dist/daemon/server.js +41 -0
- package/dist/gateway/adapters/acp.d.ts +47 -0
- package/dist/gateway/adapters/acp.js +302 -0
- package/dist/gateway/adapters/echo.d.ts +12 -0
- package/dist/gateway/adapters/echo.js +51 -0
- package/dist/gateway/adapters/index.d.ts +9 -0
- package/dist/gateway/adapters/index.js +31 -0
- package/dist/gateway/assistantProjection.d.ts +32 -0
- package/dist/gateway/assistantProjection.js +193 -0
- package/dist/gateway/config.d.ts +60 -0
- package/dist/gateway/config.js +291 -0
- package/dist/gateway/conversationContext.d.ts +44 -0
- package/dist/gateway/conversationContext.js +75 -0
- package/dist/gateway/detect.d.ts +40 -0
- package/dist/gateway/detect.js +145 -0
- package/dist/gateway/deviceHost.d.ts +91 -0
- package/dist/gateway/deviceHost.js +641 -0
- package/dist/gateway/keepAwake.d.ts +24 -0
- package/dist/gateway/keepAwake.js +64 -0
- package/dist/gateway/mcpServer.d.ts +4 -0
- package/dist/gateway/mcpServer.js +138 -0
- package/dist/gateway/protocol.d.ts +116 -0
- package/dist/gateway/protocol.js +182 -0
- package/dist/gateway/runExecutor.d.ts +91 -0
- package/dist/gateway/runExecutor.js +422 -0
- package/dist/gateway/service.d.ts +30 -0
- package/dist/gateway/service.js +112 -0
- package/dist/gateway/streamPublisher.d.ts +33 -0
- package/dist/gateway/streamPublisher.js +93 -0
- package/dist/gateway/threadStore.d.ts +222 -0
- package/dist/gateway/threadStore.js +597 -0
- package/dist/gateway/types.d.ts +136 -0
- package/dist/gateway/types.js +10 -0
- package/dist/gateway/workspaceFolders.d.ts +33 -0
- package/dist/gateway/workspaceFolders.js +129 -0
- package/dist/vendor/electron/domain/properties.js +12 -1
- package/dist/vendor/electron/domain/saved-views.js +4 -1
- package/dist/vendor/electron/shared/external-types.d.ts +18 -0
- package/dist/vendor/electron/shared/note-properties.d.ts +16 -0
- package/dist/vendor/electron/shared/note-properties.js +45 -0
- package/dist/vendor/electron/shared/powersync-read-models.d.ts +7 -1
- package/dist/vendor/electron/shared/remote-device-agent.d.ts +373 -0
- package/dist/vendor/electron/shared/remote-device-agent.js +191 -0
- package/package.json +5 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dinox agent gateway: hosts external agent runtimes (Hermes, OpenClaw, pi,
|
|
3
|
+
* Claude Code, Codex, ... over ACP) as remote devices the Dinox app can drive.
|
|
4
|
+
*
|
|
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 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.
|
|
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';
|
|
16
|
+
/** Local-only gateway definition (never synced: it may carry commands and env). */
|
|
17
|
+
export type GatewayDefinition = {
|
|
18
|
+
/** Stable id; also the `c_robot.id` the app binds agents to. */
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
kind: GatewayKind;
|
|
22
|
+
/** Detected agent type (`cursor`, `opencode`, ...); one gateway per type per machine. */
|
|
23
|
+
agentId?: string;
|
|
24
|
+
/** NATS device id the app addresses (`[a-z0-9][a-z0-9_-]{0,63}`). */
|
|
25
|
+
deviceId: string;
|
|
26
|
+
/** ACP: executable started over stdio, e.g. `hermes` with args `["acp"]`. */
|
|
27
|
+
command?: string;
|
|
28
|
+
args?: string[];
|
|
29
|
+
/** Working directory for new agent sessions (each session keeps the one it started in). */
|
|
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;
|
|
36
|
+
/** Extra environment for the agent process (secrets stay on this machine). */
|
|
37
|
+
env?: Record<string, string>;
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
/** Offer the read-only Dinox knowledge tools (MCP) to the agent; default true. */
|
|
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;
|
|
45
|
+
createdAt: string;
|
|
46
|
+
};
|
|
47
|
+
export type GatewayToolStatus = 'pending' | 'running' | 'completed' | 'failed';
|
|
48
|
+
/** Normalized agent output, independent of the adapter protocol. */
|
|
49
|
+
export type GatewayEvent = {
|
|
50
|
+
type: 'text-delta';
|
|
51
|
+
text: string;
|
|
52
|
+
} | {
|
|
53
|
+
type: 'reasoning-delta';
|
|
54
|
+
text: string;
|
|
55
|
+
} | {
|
|
56
|
+
type: 'tool';
|
|
57
|
+
toolCallId: string;
|
|
58
|
+
title?: string;
|
|
59
|
+
status?: GatewayToolStatus;
|
|
60
|
+
input?: unknown;
|
|
61
|
+
output?: unknown;
|
|
62
|
+
} | {
|
|
63
|
+
type: 'approval-request';
|
|
64
|
+
approvalId: string;
|
|
65
|
+
toolCallId: string;
|
|
66
|
+
toolName: string;
|
|
67
|
+
input?: unknown;
|
|
68
|
+
} | {
|
|
69
|
+
type: 'approval-resolved';
|
|
70
|
+
approvalId: string;
|
|
71
|
+
approved: boolean;
|
|
72
|
+
} | {
|
|
73
|
+
type: 'usage';
|
|
74
|
+
inputTokens?: number | null;
|
|
75
|
+
outputTokens?: number | null;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* A choice the agent exposes per session (an ACP select config option), e.g.
|
|
79
|
+
* its model or mode. Values are the agent's own ids; `group` is set when the
|
|
80
|
+
* agent groups its values (OpenCode groups models by provider).
|
|
81
|
+
*/
|
|
82
|
+
export type GatewayConfigOption = {
|
|
83
|
+
id: string;
|
|
84
|
+
name: string;
|
|
85
|
+
/** ACP semantic category: `model`, `mode`, `thought_level`, ... (UX only). */
|
|
86
|
+
category: string | null;
|
|
87
|
+
description: string | null;
|
|
88
|
+
currentValue: string;
|
|
89
|
+
values: Array<{
|
|
90
|
+
value: string;
|
|
91
|
+
name: string;
|
|
92
|
+
description: string | null;
|
|
93
|
+
group: string | null;
|
|
94
|
+
}>;
|
|
95
|
+
};
|
|
96
|
+
export type GatewayStopReason = 'end_turn' | 'cancelled' | 'max_tokens' | 'refusal' | 'error';
|
|
97
|
+
export type GatewayTurnInput = {
|
|
98
|
+
/** Agent session to continue; absent starts a new one. */
|
|
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;
|
|
102
|
+
prompt: string;
|
|
103
|
+
/** Prepended to the prompt when the turn starts a new session, e.g. the agent's instructions. */
|
|
104
|
+
sessionPreamble?: string;
|
|
105
|
+
/** Attachments the user sent with this turn (trusted, https). */
|
|
106
|
+
resources: Array<{
|
|
107
|
+
uri: string;
|
|
108
|
+
name: string;
|
|
109
|
+
mediaType?: string | null;
|
|
110
|
+
}>;
|
|
111
|
+
signal: AbortSignal;
|
|
112
|
+
/** Option id → value to apply to the session before the prompt; unknown entries are skipped. */
|
|
113
|
+
config?: Readonly<Record<string, string>>;
|
|
114
|
+
onEvent: (event: GatewayEvent) => void;
|
|
115
|
+
/** Resolves with the user's decision for an `approval-request` event. */
|
|
116
|
+
awaitApproval: (approvalId: string) => Promise<boolean>;
|
|
117
|
+
};
|
|
118
|
+
export type GatewayTurnResult = {
|
|
119
|
+
sessionId: string;
|
|
120
|
+
stopReason: GatewayStopReason;
|
|
121
|
+
error?: string;
|
|
122
|
+
};
|
|
123
|
+
export type GatewayAdapterInfo = {
|
|
124
|
+
kind: GatewayKind;
|
|
125
|
+
agentName: string | null;
|
|
126
|
+
agentVersion: string | null;
|
|
127
|
+
};
|
|
128
|
+
/** One agent runtime. Implementations keep their own process/connection state. */
|
|
129
|
+
export interface GatewayAdapter {
|
|
130
|
+
readonly kind: GatewayKind;
|
|
131
|
+
info(): GatewayAdapterInfo;
|
|
132
|
+
runTurn(input: GatewayTurnInput): Promise<GatewayTurnResult>;
|
|
133
|
+
/** The agent's session settings (models, modes); absent when it has none. */
|
|
134
|
+
describeConfig?(): Promise<GatewayConfigOption[]>;
|
|
135
|
+
dispose(): Promise<void>;
|
|
136
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dinox agent gateway: hosts external agent runtimes (Hermes, OpenClaw, pi,
|
|
3
|
+
* Claude Code, Codex, ... over ACP) as remote devices the Dinox app can drive.
|
|
4
|
+
*
|
|
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 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.
|
|
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
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Inputs: typed PowerSync read-model and command payloads for property definitions and note values.
|
|
4
4
|
* Outputs: catalog rows plus transactional definition, migration, and note-property mutations.
|
|
5
5
|
*/
|
|
6
|
-
import { ACTIVE_PROPERTY_TYPES, applyNotePropertySlotOperation, buildPropertyDefConfigKey, coercePropertyValue, DEFAULT_STATUS_OPTION_INPUTS, findPropertyOptionById, findPropertyOptionByLabel, getDefaultPropertyDefConfig, isIssuedUniqueId, isPropertyKeyValid, isPropertyType, mergeNotePropertiesPatch, NOTE_PROPERTY_LIMITS, normalizeDatePropertyValue, normalizeFilePropertyValue, normalizeNumberPropertyValue, normalizePlacePropertyValue, normalizePropertyDefConfig, normalizePropertyOptionLabel, normalizePropertyOptions, normalizeUniqueIdPrefix, normalizeUrlPropertyValue, parsePropertyDefConfigEnvelope, parsePropertyDefOptionsEnvelope, parseStoredNoteProperties, parseUniqueIdPropertyConfig, propertyOptionLabelKey, PROPERTY_DEF_CONFIG_KEY_PREFIX, PROPERTY_OPTION_COLORS, PROPERTY_OPTION_LABEL_MAX_LENGTH, PROPERTY_OPTION_LIMITS, savedViewFilterSchema, serializePropertyDefConfig, serializePropertyDefOptions, serializeRawNoteProperties, serializeUniqueIdPropertyConfig, STATUS_PROPERTY_GROUPS, tryParseRawNoteProperties, } from "../shared/index.js";
|
|
6
|
+
import { ACTIVE_PROPERTY_TYPES, applyNotePropertySlotOperation, buildPropertyDefConfigKey, coercePropertyValue, DEFAULT_STATUS_OPTION_INPUTS, findPropertyOptionById, findPropertyOptionByLabel, getDefaultPropertyDefConfig, isIssuedUniqueId, isNumberPropertyValueAllowed, isPropertyKeyValid, isPropertyType, mergeNotePropertiesPatch, NOTE_PROPERTY_LIMITS, NUMBER_PROPERTY_PRESETS, normalizeDatePropertyValue, normalizeFilePropertyValue, normalizeNumberPropertyValue, normalizePlacePropertyValue, normalizePropertyDefConfig, normalizePropertyOptionLabel, normalizePropertyOptions, normalizeUniqueIdPrefix, normalizeUrlPropertyValue, parseNumberPropertyPreset, parsePropertyDefConfigEnvelope, parsePropertyDefOptionsEnvelope, parseStoredNoteProperties, parseUniqueIdPropertyConfig, propertyOptionLabelKey, PROPERTY_DEF_CONFIG_KEY_PREFIX, PROPERTY_OPTION_COLORS, PROPERTY_OPTION_LABEL_MAX_LENGTH, PROPERTY_OPTION_LIMITS, savedViewFilterSchema, serializeNumberPropertyPreset, serializePropertyDefConfig, serializePropertyDefOptions, serializeRawNoteProperties, serializeUniqueIdPropertyConfig, STATUS_PROPERTY_GROUPS, tryParseRawNoteProperties, } from "../shared/index.js";
|
|
7
7
|
import { uuidv7 } from "uuidv7";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { defineCommand, defineReadModel, } from "../rpc.js";
|
|
@@ -78,6 +78,7 @@ const PROPERTY_DEF_ENSURE_INPUT = z.object({
|
|
|
78
78
|
type: z.enum(ACTIVE_PROPERTY_TYPES).nullable().optional(),
|
|
79
79
|
optionItems: z.array(PROPERTY_OPTION_INPUT_SCHEMA).max(PROPERTY_OPTION_LIMITS.items).optional(),
|
|
80
80
|
uniqueIdPrefix: z.string().max(64).nullable().optional(),
|
|
81
|
+
numberPreset: z.enum(NUMBER_PROPERTY_PRESETS).nullable().optional(),
|
|
81
82
|
config: PROPERTY_DEF_CONFIG_INPUT_SCHEMA.optional(),
|
|
82
83
|
description: z.string().max(2000).nullable().optional(),
|
|
83
84
|
icon: z.string().max(120).nullable().optional(),
|
|
@@ -487,6 +488,11 @@ function normalizeExplicitConfig(config, type) {
|
|
|
487
488
|
return normalized;
|
|
488
489
|
}
|
|
489
490
|
function optionsJsonForNewDefinition(type, input) {
|
|
491
|
+
if (type === "number") {
|
|
492
|
+
return input.numberPreset && input.numberPreset !== "number"
|
|
493
|
+
? serializeNumberPropertyPreset(input.numberPreset)
|
|
494
|
+
: null;
|
|
495
|
+
}
|
|
490
496
|
if (type === "unique_id") {
|
|
491
497
|
return serializeUniqueIdPropertyConfig({
|
|
492
498
|
prefix: normalizeUniqueIdPrefix(input.uniqueIdPrefix, normalizeOptionalString(input.name)),
|
|
@@ -1224,6 +1230,11 @@ function normalizeKnownPropertyValue(state, value, allowedArchivedIds) {
|
|
|
1224
1230
|
const normalized = normalizeNumberPropertyValue(value, state.config);
|
|
1225
1231
|
if (normalized === null)
|
|
1226
1232
|
throw new Error("PROPERTY_NUMBER_INVALID");
|
|
1233
|
+
// Presets set by dinox-app (rating 0–5 integers, progress 0–1) bind here too.
|
|
1234
|
+
const preset = parseNumberPropertyPreset(state.row.options);
|
|
1235
|
+
if (!isNumberPropertyValueAllowed(normalized, preset)) {
|
|
1236
|
+
throw new Error("PROPERTY_NUMBER_INVALID");
|
|
1237
|
+
}
|
|
1227
1238
|
return normalized;
|
|
1228
1239
|
}
|
|
1229
1240
|
case "date": {
|
|
@@ -862,10 +862,13 @@ function assertConditionSemantics(condition, semantic) {
|
|
|
862
862
|
normalizeDatePropertyValue(value, semantic.dateMode ?? "date") === null) {
|
|
863
863
|
throw new Error("SAVED_VIEW_INVALID_DATE_VALUE");
|
|
864
864
|
}
|
|
865
|
+
// Only property-backed selects carry an option list. System list fields such as
|
|
866
|
+
// `sys.tags` match free values (tag paths), so they must not be checked here.
|
|
865
867
|
if ((semantic.propertyType === "select" ||
|
|
866
868
|
semantic.propertyType === "multi_select") &&
|
|
869
|
+
semantic.options !== undefined &&
|
|
867
870
|
typeof value === "string" &&
|
|
868
|
-
!
|
|
871
|
+
!semantic.options.some((option) => option.id === value)) {
|
|
869
872
|
throw new Error("SAVED_VIEW_OPTION_NOT_FOUND");
|
|
870
873
|
}
|
|
871
874
|
}
|
|
@@ -1,2 +1,20 @@
|
|
|
1
1
|
export type ChatMessageAnchorFields = Record<string, unknown>;
|
|
2
2
|
export type SystemAgentKey = string;
|
|
3
|
+
export type ChatUIMessage = {
|
|
4
|
+
id: string;
|
|
5
|
+
role: 'system' | 'user' | 'assistant';
|
|
6
|
+
parts: Array<Record<string, unknown>>;
|
|
7
|
+
metadata?: Record<string, unknown>;
|
|
8
|
+
};
|
|
9
|
+
export interface ThreadRunCreateThreadRequest {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
kind?: string | null;
|
|
13
|
+
userId?: string | null;
|
|
14
|
+
projectId?: string | null;
|
|
15
|
+
cmdId?: string | null;
|
|
16
|
+
metaData?: Record<string, unknown> | null;
|
|
17
|
+
config?: Record<string, unknown> | null;
|
|
18
|
+
}
|
|
19
|
+
export type ThreadRunEventStatus = 'queued' | 'running' | 'waiting_approval' | 'finalizing' | 'succeeded' | 'failed' | 'cancelled' | 'interrupted' | 'expired';
|
|
20
|
+
export type ThreadRunUsageSnapshot = Record<string, unknown>;
|
|
@@ -155,6 +155,20 @@ export type UrlPropertyDefConfig = {
|
|
|
155
155
|
type: 'url';
|
|
156
156
|
schemes?: Array<'http' | 'https'>;
|
|
157
157
|
};
|
|
158
|
+
/**
|
|
159
|
+
* Display/input preset for `number` properties, shared with dinox-app. Stored in
|
|
160
|
+
* the definition's `options` column as `{ version: 1, number: { preset } }`
|
|
161
|
+
* (number defs have no select options, so the envelopes never collide).
|
|
162
|
+
* - progress: stored 0–1, shown as a percentage bar
|
|
163
|
+
* - rating: integer 0–5, shown as stars
|
|
164
|
+
*/
|
|
165
|
+
export declare const NUMBER_PROPERTY_PRESETS: readonly ["number", "progress", "rating"];
|
|
166
|
+
export type NumberPropertyPreset = (typeof NUMBER_PROPERTY_PRESETS)[number];
|
|
167
|
+
export declare const RATING_PROPERTY_MAX = 5;
|
|
168
|
+
export declare function parseNumberPropertyPreset(raw: string | null | undefined): NumberPropertyPreset | undefined;
|
|
169
|
+
export declare function serializeNumberPropertyPreset(preset: NumberPropertyPreset): string;
|
|
170
|
+
/** Range rules every client enforces for a preset (mirrors dinox-app). */
|
|
171
|
+
export declare function isNumberPropertyValueAllowed(value: unknown, preset?: NumberPropertyPreset): value is number;
|
|
158
172
|
export type PropertyDefConfig = DatePropertyDefConfig | NumberPropertyDefConfig | SelectPropertyDefConfig | MultiSelectPropertyDefConfig | UrlPropertyDefConfig;
|
|
159
173
|
export declare function isPropertyDefSource(value: unknown): value is PropertyDefSource;
|
|
160
174
|
/** Domain form of a synced property-definition catalog entry (camelCase). */
|
|
@@ -169,6 +183,8 @@ export interface PropertyDef {
|
|
|
169
183
|
options?: PropertyOption[];
|
|
170
184
|
/** Counter stored in `c_property_def.options` for `unique_id` (not the select envelope). */
|
|
171
185
|
uniqueId?: UniqueIdPropertyConfig;
|
|
186
|
+
/** `number` only: progress / rating presentation (absent = plain number). */
|
|
187
|
+
numberPreset?: NumberPropertyPreset;
|
|
172
188
|
/** Effective type configuration, including legacy defaults when no companion row exists. */
|
|
173
189
|
config?: PropertyDefConfig;
|
|
174
190
|
/** Explicit companion config exists but is malformed, unsupported, or type-mismatched. */
|
|
@@ -64,6 +64,47 @@ export function buildPropertyDefConfigKey(definitionId) {
|
|
|
64
64
|
throw new Error('INVALID_PROPERTY_DEF_ID');
|
|
65
65
|
return `${PROPERTY_DEF_CONFIG_KEY_PREFIX}${id}`;
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Display/input preset for `number` properties, shared with dinox-app. Stored in
|
|
69
|
+
* the definition's `options` column as `{ version: 1, number: { preset } }`
|
|
70
|
+
* (number defs have no select options, so the envelopes never collide).
|
|
71
|
+
* - progress: stored 0–1, shown as a percentage bar
|
|
72
|
+
* - rating: integer 0–5, shown as stars
|
|
73
|
+
*/
|
|
74
|
+
export const NUMBER_PROPERTY_PRESETS = ['number', 'progress', 'rating'];
|
|
75
|
+
export const RATING_PROPERTY_MAX = 5;
|
|
76
|
+
export function parseNumberPropertyPreset(raw) {
|
|
77
|
+
if (!raw || !raw.trim())
|
|
78
|
+
return undefined;
|
|
79
|
+
try {
|
|
80
|
+
const config = JSON.parse(raw);
|
|
81
|
+
if (!isRecord(config) || config.version !== 1 || !isRecord(config.number))
|
|
82
|
+
return undefined;
|
|
83
|
+
const preset = config.number.preset;
|
|
84
|
+
return NUMBER_PROPERTY_PRESETS.includes(preset)
|
|
85
|
+
? preset
|
|
86
|
+
: undefined;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function serializeNumberPropertyPreset(preset) {
|
|
93
|
+
if (!NUMBER_PROPERTY_PRESETS.includes(preset))
|
|
94
|
+
throw new Error('INVALID_NUMBER_PROPERTY_PRESET');
|
|
95
|
+
return JSON.stringify({ version: 1, number: { preset } });
|
|
96
|
+
}
|
|
97
|
+
/** Range rules every client enforces for a preset (mirrors dinox-app). */
|
|
98
|
+
export function isNumberPropertyValueAllowed(value, preset) {
|
|
99
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
100
|
+
return false;
|
|
101
|
+
if (preset === 'progress')
|
|
102
|
+
return value >= 0 && value <= 1;
|
|
103
|
+
if (preset === 'rating') {
|
|
104
|
+
return Number.isInteger(value) && value >= 0 && value <= RATING_PROPERTY_MAX;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
67
108
|
export function isPropertyDefSource(value) {
|
|
68
109
|
return value === 'manual' || value === 'ai';
|
|
69
110
|
}
|
|
@@ -936,6 +977,9 @@ export function serializePropertyDefConfig(config) {
|
|
|
936
977
|
throw new Error('INVALID_PROPERTY_DEF_CONFIG');
|
|
937
978
|
return JSON.stringify(normalized);
|
|
938
979
|
}
|
|
980
|
+
function numberPresetField(preset) {
|
|
981
|
+
return preset && preset !== 'number' ? { numberPreset: preset } : {};
|
|
982
|
+
}
|
|
939
983
|
/** Map a raw c_property_def row into the domain object (unknown type -> text). */
|
|
940
984
|
export function mapPropertyDef(record) {
|
|
941
985
|
const rawType = record.type ?? 'text';
|
|
@@ -960,6 +1004,7 @@ export function mapPropertyDef(record) {
|
|
|
960
1004
|
},
|
|
961
1005
|
}
|
|
962
1006
|
: {}),
|
|
1007
|
+
...(type === 'number' ? numberPresetField(parseNumberPropertyPreset(record.options)) : {}),
|
|
963
1008
|
config: parsedConfig.status === 'empty'
|
|
964
1009
|
? getDefaultPropertyDefConfig(type, 'legacy')
|
|
965
1010
|
: explicitConfigMatches
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ChatMessageAnchorFields } from './external-types.js';
|
|
2
|
-
import type { ActivePropertyType, NoteProperties, NotePropertiesPatch, NotePropertySlotOperation, PropertyDefConfig, PropertyDefSource, PropertyOptionInput } from './note-properties.js';
|
|
2
|
+
import type { ActivePropertyType, NoteProperties, NotePropertiesPatch, NotePropertySlotOperation, NumberPropertyPreset, PropertyDefConfig, PropertyDefSource, PropertyOptionInput } from './note-properties.js';
|
|
3
3
|
import type { SystemAgentKey } from './external-types.js';
|
|
4
4
|
export interface ResourceUserListInput {
|
|
5
5
|
userId: string;
|
|
@@ -284,6 +284,10 @@ export interface NoteSoftDeleteInput {
|
|
|
284
284
|
noteId: string;
|
|
285
285
|
updatedAt: string;
|
|
286
286
|
}
|
|
287
|
+
export interface NotePermanentDeleteInput {
|
|
288
|
+
/** Only notes already in the trash (`is_del = 1`) are removed; others are skipped. */
|
|
289
|
+
noteIds: string[];
|
|
290
|
+
}
|
|
287
291
|
export interface NoteCommentsReplaceInput {
|
|
288
292
|
noteId: string;
|
|
289
293
|
comments: string;
|
|
@@ -1141,6 +1145,8 @@ export interface PropertyDefEnsureInput {
|
|
|
1141
1145
|
optionItems?: PropertyOptionInput[];
|
|
1142
1146
|
/** Prefix for a new `unique_id` counter. Empty falls back to the property name. */
|
|
1143
1147
|
uniqueIdPrefix?: string | null;
|
|
1148
|
+
/** New `number` definitions only: progress / rating presentation. */
|
|
1149
|
+
numberPreset?: NumberPropertyPreset | null;
|
|
1144
1150
|
config?: PropertyDefConfig;
|
|
1145
1151
|
description?: string | null;
|
|
1146
1152
|
icon?: string | null;
|