@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.
Files changed (53) hide show
  1. package/README.md +17 -3
  2. package/dist/cli.js +2 -0
  3. package/dist/commands/gateway/index.d.ts +4 -0
  4. package/dist/commands/gateway/index.js +300 -0
  5. package/dist/commands/schema/renderSkills.js +4 -0
  6. package/dist/commands/schema/spec.js +2 -0
  7. package/dist/config/paths.d.ts +1 -0
  8. package/dist/config/paths.js +1 -1
  9. package/dist/daemon/server.d.ts +1 -0
  10. package/dist/daemon/server.js +41 -0
  11. package/dist/gateway/adapters/acp.d.ts +47 -0
  12. package/dist/gateway/adapters/acp.js +302 -0
  13. package/dist/gateway/adapters/echo.d.ts +12 -0
  14. package/dist/gateway/adapters/echo.js +51 -0
  15. package/dist/gateway/adapters/index.d.ts +9 -0
  16. package/dist/gateway/adapters/index.js +31 -0
  17. package/dist/gateway/assistantProjection.d.ts +32 -0
  18. package/dist/gateway/assistantProjection.js +193 -0
  19. package/dist/gateway/config.d.ts +60 -0
  20. package/dist/gateway/config.js +291 -0
  21. package/dist/gateway/conversationContext.d.ts +44 -0
  22. package/dist/gateway/conversationContext.js +75 -0
  23. package/dist/gateway/detect.d.ts +40 -0
  24. package/dist/gateway/detect.js +145 -0
  25. package/dist/gateway/deviceHost.d.ts +91 -0
  26. package/dist/gateway/deviceHost.js +641 -0
  27. package/dist/gateway/keepAwake.d.ts +24 -0
  28. package/dist/gateway/keepAwake.js +64 -0
  29. package/dist/gateway/mcpServer.d.ts +4 -0
  30. package/dist/gateway/mcpServer.js +138 -0
  31. package/dist/gateway/protocol.d.ts +116 -0
  32. package/dist/gateway/protocol.js +182 -0
  33. package/dist/gateway/runExecutor.d.ts +91 -0
  34. package/dist/gateway/runExecutor.js +422 -0
  35. package/dist/gateway/service.d.ts +30 -0
  36. package/dist/gateway/service.js +112 -0
  37. package/dist/gateway/streamPublisher.d.ts +33 -0
  38. package/dist/gateway/streamPublisher.js +93 -0
  39. package/dist/gateway/threadStore.d.ts +222 -0
  40. package/dist/gateway/threadStore.js +597 -0
  41. package/dist/gateway/types.d.ts +136 -0
  42. package/dist/gateway/types.js +10 -0
  43. package/dist/gateway/workspaceFolders.d.ts +33 -0
  44. package/dist/gateway/workspaceFolders.js +129 -0
  45. package/dist/vendor/electron/domain/properties.js +12 -1
  46. package/dist/vendor/electron/domain/saved-views.js +4 -1
  47. package/dist/vendor/electron/shared/external-types.d.ts +18 -0
  48. package/dist/vendor/electron/shared/note-properties.d.ts +16 -0
  49. package/dist/vendor/electron/shared/note-properties.js +45 -0
  50. package/dist/vendor/electron/shared/powersync-read-models.d.ts +7 -1
  51. package/dist/vendor/electron/shared/remote-device-agent.d.ts +373 -0
  52. package/dist/vendor/electron/shared/remote-device-agent.js +191 -0
  53. 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
- !(semantic.options ?? []).some((option) => option.id === value)) {
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;