nolo-cli 0.1.33 → 0.1.35

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 (84) hide show
  1. package/README.md +15 -5
  2. package/agent-runtime/agentRecordConfig.ts +8 -0
  3. package/agent-runtime/hostAdapter.ts +9 -1
  4. package/agent-runtime/index.ts +10 -0
  5. package/agent-runtime/localLoop.ts +49 -2
  6. package/agent-runtime/localToolPolicy.ts +71 -22
  7. package/agent-runtime/localWorkspaceTools.ts +5 -2
  8. package/agent-runtime/openAiCompatibleProvider.ts +7 -3
  9. package/agent-runtime/openAiCompatibleProviderConfig.ts +15 -26
  10. package/agent-runtime/platformChatProvider.ts +48 -71
  11. package/agent-runtime/providerResolution.ts +242 -0
  12. package/agent-runtime/runtimeToolPolicy.ts +33 -0
  13. package/agent-runtime/shellCommandPolicy.ts +120 -0
  14. package/agentCommandSupport.ts +4 -0
  15. package/agentInternalCommandEntries.ts +2 -0
  16. package/agentListCommands.ts +74 -12
  17. package/agentListHelpers.ts +56 -1
  18. package/agentPullCommand.ts +6 -11
  19. package/agentRecordCommands.ts +87 -2
  20. package/agentRecordHelpers.ts +132 -9
  21. package/agentRuntimeLocal.ts +3 -0
  22. package/ai/tools/toolNameAliases.ts +70 -0
  23. package/cli/agentCommandSupport.ts +4 -0
  24. package/cli/agentInternalCommandEntries.ts +2 -0
  25. package/cli/agentListCommands.ts +74 -12
  26. package/cli/agentListHelpers.ts +56 -1
  27. package/cli/agentPullCommand.ts +6 -11
  28. package/cli/agentRecordCommands.ts +87 -2
  29. package/cli/agentRecordHelpers.ts +132 -9
  30. package/cli/agentRuntimeLocal.ts +3 -0
  31. package/cli/cliCommandDispatch.ts +2 -2
  32. package/cli/cliEnvHelpers.ts +80 -12
  33. package/cli/cliSpaceHelpers.ts +39 -0
  34. package/cli/client/agentRun.ts +1 -0
  35. package/cli/client/localRuntimeAdapter.ts +44 -7
  36. package/cli/commandRegistry.ts +2 -2
  37. package/cli/connectorWebSocketTarget.ts +10 -3
  38. package/cli/dialogCommands.ts +446 -0
  39. package/cli/dialogInternalCommandEntries.ts +13 -0
  40. package/cli/globalRecordOperations.ts +258 -0
  41. package/cli/internalCommandEntries.ts +2 -0
  42. package/cli/localRuntimeAuthority.ts +182 -0
  43. package/cli/localRuntimeDb.ts +2 -4
  44. package/cli/machineCommands.ts +1 -1
  45. package/cli/machineWsRunDispatch.ts +29 -12
  46. package/cli/machineWsSession.ts +2 -0
  47. package/cli/offlineMarxistsAgentCommand.ts +1 -1
  48. package/cli/runtimeDoctorCommands.ts +65 -9
  49. package/cli/tableCommands.ts +1 -1
  50. package/cli/taskRunCommand.ts +1 -1
  51. package/cliCommandDispatch.ts +2 -2
  52. package/cliEnvHelpers.ts +80 -12
  53. package/cliSpaceHelpers.ts +39 -0
  54. package/client/agentRun.ts +1 -0
  55. package/client/localProviderResolver.test.ts +26 -2
  56. package/client/localRuntimeAdapter.test.ts +287 -3
  57. package/client/localRuntimeAdapter.ts +44 -7
  58. package/commandRegistry.ts +2 -2
  59. package/connectorWebSocketTarget.ts +10 -3
  60. package/database/server/authorityStoreTypes.ts +31 -0
  61. package/database/server/cliAuthorityBrokerClient.ts +227 -0
  62. package/database/server/cliAuthorityBrokerServer.ts +283 -0
  63. package/database/server/cliAuthorityBrokerTypes.ts +43 -0
  64. package/database/server/cliAuthorityStoreDriver.ts +98 -0
  65. package/database/server/legacyServerDb.ts +58 -0
  66. package/database/server/levelAuthorityStore.ts +95 -0
  67. package/database/tombstones.ts +52 -0
  68. package/database/userDataMerge.ts +95 -0
  69. package/dialogCommands.ts +446 -0
  70. package/dialogInternalCommandEntries.ts +13 -0
  71. package/globalRecordOperations.ts +258 -0
  72. package/internalCommandEntries.ts +2 -0
  73. package/localRuntimeAuthority.ts +182 -0
  74. package/localRuntimeDb.ts +2 -4
  75. package/machineCommands.ts +1 -1
  76. package/machineWsRunDispatch.ts +29 -12
  77. package/machineWsSession.ts +2 -0
  78. package/offlineMarxistsAgentCommand.ts +1 -1
  79. package/package.json +24 -4
  80. package/runtimeDoctorCommands.ts +65 -9
  81. package/tableCommands.ts +1 -1
  82. package/taskRunCommand.ts +1 -1
  83. package/database/server/db.ts +0 -118
  84. package/database/server/ensureDbOpen.ts +0 -12
@@ -0,0 +1,95 @@
1
+ import { Level } from "level";
2
+
3
+ import type {
4
+ AuthorityBatchOperation,
5
+ AuthorityBatchWriter,
6
+ AuthorityIteratorOptions,
7
+ AuthorityStore,
8
+ } from "./authorityStoreTypes";
9
+
10
+ const safeJsonEncoding = {
11
+ name: "safe-json",
12
+ format: "utf8" as const,
13
+ encode: (data: any) => JSON.stringify(data),
14
+ decode: (data: string) => {
15
+ try {
16
+ return JSON.parse(data);
17
+ } catch {
18
+ console.warn("[LevelDB] Corrupted value:", data?.substring?.(0, 60));
19
+ return null;
20
+ }
21
+ },
22
+ };
23
+
24
+ type LevelBatchWriterLike = {
25
+ put(key: string, value: unknown): void;
26
+ del(key: string): void;
27
+ write(): Promise<void>;
28
+ };
29
+
30
+ type LevelLike = {
31
+ readonly location?: string;
32
+ readonly status?: string;
33
+ open(): Promise<void>;
34
+ close(): Promise<void>;
35
+ get(key: string): Promise<unknown>;
36
+ put(key: string, value: unknown): Promise<void>;
37
+ del(key: string): Promise<void>;
38
+ batch(ops: AuthorityBatchOperation[]): Promise<void>;
39
+ batch(): LevelBatchWriterLike;
40
+ iterator(
41
+ options?: AuthorityIteratorOptions
42
+ ): AsyncIterableIterator<[string, unknown]>;
43
+ };
44
+
45
+ export function createLevelAuthorityStore(
46
+ source: string | LevelLike
47
+ ): AuthorityStore {
48
+ const levelDb: LevelLike = typeof source === "string"
49
+ ? new Level<string, any>(source, { valueEncoding: safeJsonEncoding })
50
+ : source;
51
+
52
+ return {
53
+ get location() {
54
+ return levelDb.location;
55
+ },
56
+ get status() {
57
+ return (levelDb as any).status;
58
+ },
59
+ async open() {
60
+ await levelDb.open();
61
+ },
62
+ async close() {
63
+ await levelDb.close();
64
+ },
65
+ async get<T = any>(key: string) {
66
+ return levelDb.get(key) as Promise<T>;
67
+ },
68
+ async put(key: string, value: unknown) {
69
+ await levelDb.put(key, value);
70
+ },
71
+ async del(key: string) {
72
+ await levelDb.del(key);
73
+ },
74
+ async batchWrite(ops: AuthorityBatchOperation[]) {
75
+ await levelDb.batch(ops);
76
+ },
77
+ createBatch(): AuthorityBatchWriter {
78
+ const batch = levelDb.batch();
79
+ return {
80
+ put(key: string, value: unknown) {
81
+ batch.put(key, value);
82
+ },
83
+ del(key: string) {
84
+ batch.del(key);
85
+ },
86
+ async write() {
87
+ await batch.write();
88
+ },
89
+ };
90
+ },
91
+ iterator(options: AuthorityIteratorOptions = {}) {
92
+ return levelDb.iterator(options) as AsyncIterableIterator<[string, unknown]>;
93
+ },
94
+ };
95
+ }
@@ -0,0 +1,52 @@
1
+ export const getRecordTimestamp = (record: any): number => {
2
+ if (!record || typeof record !== "object") return 0;
3
+
4
+ const candidates = [
5
+ record.updatedAt,
6
+ record.updated_at,
7
+ record.createdAt,
8
+ record.created,
9
+ record?.meta?.createdAt,
10
+ ];
11
+
12
+ for (const candidate of candidates) {
13
+ if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) {
14
+ return candidate;
15
+ }
16
+ if (typeof candidate === "string" && candidate.trim()) {
17
+ const timestamp = Date.parse(candidate);
18
+ if (Number.isFinite(timestamp) && timestamp > 0) {
19
+ return timestamp;
20
+ }
21
+ }
22
+ }
23
+
24
+ return 0;
25
+ };
26
+
27
+ export const isTombstoneRecord = (record: any): boolean => {
28
+ if (!record || typeof record !== "object") return false;
29
+ const deletedAt = record.deletedAt;
30
+ if (typeof deletedAt === "string") return deletedAt.trim().length > 0;
31
+ return Boolean(deletedAt);
32
+ };
33
+
34
+ export const shouldReplaceWithNextRecord = (
35
+ nextRecord: any,
36
+ currentRecord: any
37
+ ): boolean => {
38
+ const nextTs = getRecordTimestamp(nextRecord);
39
+ const currentTs = getRecordTimestamp(currentRecord);
40
+
41
+ if (nextTs !== currentTs) return nextTs > currentTs;
42
+ return isTombstoneRecord(nextRecord) && !isTombstoneRecord(currentRecord);
43
+ };
44
+
45
+ export const buildTombstoneRecord = <T extends Record<string, any>>(
46
+ record: T,
47
+ nowIso: string
48
+ ): T & { deletedAt: string; updatedAt: string } => ({
49
+ ...record,
50
+ deletedAt: nowIso,
51
+ updatedAt: nowIso,
52
+ });
@@ -0,0 +1,95 @@
1
+ import {
2
+ getRecordTimestamp,
3
+ isTombstoneRecord,
4
+ shouldReplaceWithNextRecord,
5
+ } from "./tombstones";
6
+
7
+ interface MergeableUserDataItem {
8
+ id?: string;
9
+ dbKey?: string;
10
+ contentKey?: string;
11
+ appKey?: string;
12
+ appId?: string;
13
+ [key: string]: any;
14
+ }
15
+
16
+ export const getUserDataItemTimestamp = (dataItem: MergeableUserDataItem): number =>
17
+ getRecordTimestamp(dataItem);
18
+
19
+ const getItemKey = (item: MergeableUserDataItem): string | null => {
20
+ // dbKey 必须优先于 id:dbKey 是全局唯一的 "type-userId-id" 格式,
21
+ // 而 id 可能只是 ULID 短串,与本地 tombstone 的 dbKey 不匹配会导致
22
+ // merge 时 tombstone 无法覆盖远端活记录。
23
+ const candidates = [
24
+ typeof item.dbKey === "string" ? item.dbKey : undefined,
25
+ item.id,
26
+ typeof item.contentKey === "string" ? item.contentKey : undefined,
27
+ typeof item.appKey === "string" ? item.appKey : undefined,
28
+ typeof item.appId === "string" ? item.appId : undefined,
29
+ ];
30
+
31
+ for (const candidate of candidates) {
32
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
33
+ return candidate;
34
+ }
35
+ }
36
+
37
+ return null;
38
+ };
39
+
40
+ export const mergeAndDedupUserData = (
41
+ localData: MergeableUserDataItem[],
42
+ remoteResults: any[],
43
+ options: { includeDeleted?: boolean } = {}
44
+ ): MergeableUserDataItem[] => {
45
+ const uniqueMap = new Map<string, MergeableUserDataItem>();
46
+
47
+ const mergeRecordMetadata = (
48
+ currentItem: MergeableUserDataItem,
49
+ nextItem: MergeableUserDataItem
50
+ ): MergeableUserDataItem => {
51
+ if (
52
+ typeof currentItem.serverOrigin !== "string" &&
53
+ typeof nextItem.serverOrigin === "string" &&
54
+ nextItem.serverOrigin.trim().length > 0
55
+ ) {
56
+ return {
57
+ ...currentItem,
58
+ serverOrigin: nextItem.serverOrigin,
59
+ };
60
+ }
61
+
62
+ return currentItem;
63
+ };
64
+
65
+ const addToMap = (item: MergeableUserDataItem) => {
66
+ const itemKey = getItemKey(item);
67
+ if (!itemKey) return;
68
+
69
+ const existing = uniqueMap.get(itemKey);
70
+ if (!existing) {
71
+ uniqueMap.set(itemKey, item);
72
+ return;
73
+ }
74
+
75
+ if (shouldReplaceWithNextRecord(item, existing)) {
76
+ uniqueMap.set(itemKey, item);
77
+ return;
78
+ }
79
+
80
+ uniqueMap.set(itemKey, mergeRecordMetadata(existing, item));
81
+ };
82
+
83
+ localData.forEach(addToMap);
84
+ remoteResults.forEach((result) => {
85
+ const items = result?.data?.data;
86
+ if (Array.isArray(items)) {
87
+ items.forEach(addToMap);
88
+ }
89
+ });
90
+
91
+ const merged = Array.from(uniqueMap.values());
92
+ return options.includeDeleted
93
+ ? merged
94
+ : merged.filter((item) => !isTombstoneRecord(item));
95
+ };
@@ -0,0 +1,446 @@
1
+ import type { AgentCommandDeps } from "./agentCommandSupport";
2
+ import { buildSpaceLookup, getSpaceContentKeys } from "./cliSpaceHelpers";
3
+ import {
4
+ parseUserIdFromAuthToken,
5
+ readOption,
6
+ resolveAuthToken,
7
+ resolveServerCandidates,
8
+ resolveServerUrl,
9
+ } from "./cliEnvHelpers";
10
+ import {
11
+ deleteDbRecordOnServers,
12
+ listUserRecordsFromServers,
13
+ readDbRecordFromServers,
14
+ readLiveDbRecordAfterTombstoneMerge,
15
+ } from "./globalRecordOperations";
16
+
17
+ type ListedDialog = {
18
+ id: string;
19
+ dbKey: string;
20
+ title: string;
21
+ status: string | null;
22
+ updatedAt: string | number | null;
23
+ createdAt: string | number | null;
24
+ spaceId: string | null;
25
+ triggerType: string | null;
26
+ primaryAgentKey: string | null;
27
+ cybots: string[];
28
+ };
29
+
30
+ const DIALOG_ID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
31
+ const DIALOG_PATH_RE = /^\/(?:space\/([^/]+)\/)?dialog-(.+)-([0-9A-HJKMNP-TV-Z]{26})\/?$/i;
32
+ const VALUE_FLAGS = new Set([
33
+ "--limit",
34
+ "--server",
35
+ "--server-url",
36
+ "--space",
37
+ "--space-id",
38
+ "--token",
39
+ ]);
40
+
41
+ function hasFlag(args: string[], flag: string) {
42
+ return args.includes(flag);
43
+ }
44
+
45
+ function readFirstPositional(args: string[]) {
46
+ for (let index = 0; index < args.length; index += 1) {
47
+ const value = args[index];
48
+ if (VALUE_FLAGS.has(value)) {
49
+ index += 1;
50
+ continue;
51
+ }
52
+ if (!value.startsWith("-")) return value;
53
+ }
54
+ return undefined;
55
+ }
56
+
57
+ function readLimit(args: string[], fallback: number) {
58
+ const raw = readOption(args, "--limit");
59
+ const parsed = raw ? Number(raw) : fallback;
60
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
61
+ }
62
+
63
+ function getDialogIdFromKey(dbKey: string) {
64
+ const index = dbKey.lastIndexOf("-");
65
+ return index >= 0 ? dbKey.slice(index + 1) : dbKey;
66
+ }
67
+
68
+ function resolveDialogInput(rawInput: string, userId: string) {
69
+ const raw = rawInput.trim();
70
+ if (raw.startsWith("http://") || raw.startsWith("https://")) {
71
+ const url = new URL(raw);
72
+ const match = url.pathname.match(DIALOG_PATH_RE);
73
+ if (!match) {
74
+ throw new Error(`Unsupported dialog URL path: ${url.pathname}`);
75
+ }
76
+ const [, spaceId, ownerId, dialogId] = match;
77
+ return {
78
+ dbKey: `dialog-${ownerId}-${dialogId}`,
79
+ dialogId,
80
+ ownerId,
81
+ spaceId: spaceId ? decodeURIComponent(spaceId) : null,
82
+ };
83
+ }
84
+
85
+ if (raw.startsWith("dialog-")) {
86
+ const dialogId = getDialogIdFromKey(raw);
87
+ return {
88
+ dbKey: raw,
89
+ dialogId,
90
+ ownerId: raw.slice("dialog-".length, Math.max("dialog-".length, raw.length - dialogId.length - 1)),
91
+ spaceId: null,
92
+ };
93
+ }
94
+
95
+ if (!DIALOG_ID_RE.test(raw)) {
96
+ throw new Error(`Unsupported dialog id: ${raw}`);
97
+ }
98
+
99
+ return {
100
+ dbKey: `dialog-${userId}-${raw}`,
101
+ dialogId: raw,
102
+ ownerId: userId,
103
+ spaceId: null,
104
+ };
105
+ }
106
+
107
+ function normalizeDialogRecord(record: any, fallbackDbKey?: string): ListedDialog | null {
108
+ const dbKey =
109
+ typeof record?.dbKey === "string" && record.dbKey.trim()
110
+ ? record.dbKey.trim()
111
+ : fallbackDbKey ?? "";
112
+ const id =
113
+ typeof record?.id === "string" && record.id.trim()
114
+ ? record.id.trim()
115
+ : dbKey
116
+ ? getDialogIdFromKey(dbKey)
117
+ : "";
118
+ if (!dbKey || !id) return null;
119
+
120
+ return {
121
+ id,
122
+ dbKey,
123
+ title:
124
+ typeof record?.title === "string" && record.title.trim()
125
+ ? record.title.trim()
126
+ : typeof record?.taskLabel === "string" && record.taskLabel.trim()
127
+ ? record.taskLabel.trim()
128
+ : "(untitled)",
129
+ status: typeof record?.status === "string" ? record.status : null,
130
+ updatedAt:
131
+ typeof record?.updatedAt === "string" || typeof record?.updatedAt === "number"
132
+ ? record.updatedAt
133
+ : typeof record?.updated_at === "string" || typeof record?.updated_at === "number"
134
+ ? record.updated_at
135
+ : null,
136
+ createdAt:
137
+ typeof record?.createdAt === "string" || typeof record?.createdAt === "number"
138
+ ? record.createdAt
139
+ : typeof record?.created === "string" || typeof record?.created === "number"
140
+ ? record.created
141
+ : null,
142
+ spaceId: typeof record?.spaceId === "string" && record.spaceId.trim() ? record.spaceId : null,
143
+ triggerType: typeof record?.triggerType === "string" ? record.triggerType : null,
144
+ primaryAgentKey:
145
+ typeof record?.primaryAgentKey === "string" && record.primaryAgentKey.trim()
146
+ ? record.primaryAgentKey
147
+ : null,
148
+ cybots: Array.isArray(record?.cybots)
149
+ ? record.cybots.filter((agent: unknown): agent is string => typeof agent === "string")
150
+ : [],
151
+ };
152
+ }
153
+
154
+ function sortDialogs(dialogs: ListedDialog[]) {
155
+ return dialogs.sort((a, b) => {
156
+ const ta = a.updatedAt ?? a.createdAt ?? 0;
157
+ const tb = b.updatedAt ?? b.createdAt ?? 0;
158
+ const left = typeof ta === "number" ? ta : Date.parse(String(ta)) || 0;
159
+ const right = typeof tb === "number" ? tb : Date.parse(String(tb)) || 0;
160
+ return right - left;
161
+ });
162
+ }
163
+
164
+ function isScheduledDialog(record: ListedDialog) {
165
+ return record.triggerType === "scheduled_run";
166
+ }
167
+
168
+ function printListUsage(output: { write(chunk: string): unknown }) {
169
+ output.write(`Usage:
170
+ nolo dialog list [--space <spaceId|spaceUrl>] [--limit 100] [--json] [--ids-only]
171
+
172
+ Options:
173
+ --space <space> List current user's dialogs attached to one space.
174
+ --limit <n> Maximum dialogs to return. Default: 100.
175
+ --include-scheduled Include scheduled automation run dialogs.
176
+ --json Print machine-readable JSON.
177
+ --ids-only Print only dialog ids.
178
+ --server <url> Override NOLO_SERVER/BASE_URL.
179
+ --token <jwt> Override AUTH_TOKEN.
180
+
181
+ Lists merge global server candidates and ignore newer tombstones.
182
+ `);
183
+ }
184
+
185
+ function printDeleteUsage(output: { write(chunk: string): unknown }) {
186
+ output.write(`Usage:
187
+ nolo dialog delete <dialogId|dialogKey|dialogUrl> [--yes] [--json]
188
+
189
+ Options:
190
+ --yes Actually delete. Without this, the command is a dry-run.
191
+ --json Print machine-readable JSON.
192
+ --server <url> Prefer this server and include it in global deletion.
193
+ --token <jwt> Override AUTH_TOKEN.
194
+
195
+ Deletes are global across the selected/profile server plus known Nolo cluster peers.
196
+ `);
197
+ }
198
+
199
+ async function readSpaceDialogRecords(args: {
200
+ authToken: string;
201
+ fetchImpl: typeof fetch;
202
+ fallbackFetchImpl?: typeof fetch;
203
+ serverUrls: string[];
204
+ spaceInput: string;
205
+ userId: string;
206
+ }) {
207
+ const { spaceId, spaceKey } = buildSpaceLookup(args.spaceInput);
208
+ const spaceRead = await readLiveDbRecordAfterTombstoneMerge({
209
+ authToken: args.authToken,
210
+ dbKey: spaceKey,
211
+ fallbackFetchImpl: args.fallbackFetchImpl,
212
+ fetchImpl: args.fetchImpl,
213
+ label: "space record",
214
+ serverUrls: args.serverUrls,
215
+ });
216
+ const contentKeys = [...getSpaceContentKeys(spaceRead.record)]
217
+ .filter((key) => key.startsWith(`dialog-${args.userId}-`));
218
+ const contentKeySet = new Set(contentKeys);
219
+ const dialogResult = await listUserRecordsFromServers({
220
+ authToken: args.authToken,
221
+ fallbackFetchImpl: args.fallbackFetchImpl,
222
+ fetchImpl: args.fetchImpl,
223
+ label: "dialog query",
224
+ serverUrls: args.serverUrls,
225
+ type: "dialog",
226
+ userId: args.userId,
227
+ });
228
+ const records = dialogResult.records.filter((record) =>
229
+ typeof record?.dbKey === "string" && contentKeySet.has(record.dbKey)
230
+ );
231
+ const failures = [...spaceRead.failures, ...dialogResult.failures];
232
+ return { spaceId, records, failures };
233
+ }
234
+
235
+ export async function runDialogListCommand(
236
+ args: string[],
237
+ deps: AgentCommandDeps = {}
238
+ ) {
239
+ const env = deps.env ?? process.env;
240
+ const output = deps.output ?? process.stdout;
241
+ if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
242
+ printListUsage(output);
243
+ return 0;
244
+ }
245
+
246
+ const authToken = resolveAuthToken(args, env);
247
+ if (!authToken) {
248
+ output.write("[nolo] dialog list requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
249
+ return 1;
250
+ }
251
+ const userId = parseUserIdFromAuthToken(authToken);
252
+ if (!userId) {
253
+ output.write("[nolo] dialog list could not read userId from AUTH_TOKEN.\n");
254
+ return 1;
255
+ }
256
+
257
+ const fetchImpl = deps.fetchImpl ?? fetch;
258
+ const fallbackFetchImpl = deps.fallbackFetchImpl;
259
+ const serverUrl = resolveServerUrl(args, env);
260
+ const serverUrls = resolveServerCandidates(args, env, serverUrl);
261
+ const limit = readLimit(args, 100);
262
+ const spaceInput = readOption(args, "--space") ?? readOption(args, "--space-id");
263
+ const includeScheduled = hasFlag(args, "--include-scheduled");
264
+
265
+ try {
266
+ let source: "user-data" | "space" = "user-data";
267
+ let resolvedSpaceId: string | null = null;
268
+ let queryFailures: Array<{ serverUrl: string; error: string }> = [];
269
+ let records: any[];
270
+ if (spaceInput) {
271
+ source = "space";
272
+ const result = await readSpaceDialogRecords({
273
+ authToken,
274
+ fallbackFetchImpl,
275
+ fetchImpl,
276
+ serverUrls,
277
+ spaceInput,
278
+ userId,
279
+ });
280
+ resolvedSpaceId = result.spaceId;
281
+ records = result.records;
282
+ queryFailures = result.failures;
283
+ } else {
284
+ const result = await listUserRecordsFromServers({
285
+ authToken,
286
+ fallbackFetchImpl,
287
+ fetchImpl,
288
+ label: "dialog query",
289
+ serverUrls,
290
+ type: "dialog",
291
+ userId,
292
+ });
293
+ records = result.records;
294
+ queryFailures = result.failures;
295
+ }
296
+
297
+ const dialogs = sortDialogs(
298
+ records
299
+ .map((record) => normalizeDialogRecord(record))
300
+ .filter((dialog): dialog is ListedDialog => dialog != null)
301
+ .filter((dialog) => includeScheduled || !isScheduledDialog(dialog))
302
+ ).slice(0, limit);
303
+
304
+ if (hasFlag(args, "--ids-only")) {
305
+ output.write(`${dialogs.map((dialog) => dialog.id).join("\n")}\n`);
306
+ return 0;
307
+ }
308
+
309
+ if (hasFlag(args, "--json")) {
310
+ output.write(JSON.stringify({
311
+ userId,
312
+ ...(resolvedSpaceId ? { spaceId: resolvedSpaceId } : {}),
313
+ source,
314
+ targetServers: serverUrls,
315
+ ...(queryFailures.length ? { serverFailures: queryFailures } : {}),
316
+ total: dialogs.length,
317
+ dialogs,
318
+ }, null, 2));
319
+ output.write("\n");
320
+ return 0;
321
+ }
322
+
323
+ output.write(`userId: ${userId}\n`);
324
+ if (resolvedSpaceId) output.write(`spaceId: ${resolvedSpaceId}\n`);
325
+ output.write(`source: ${source}\n`);
326
+ output.write(`targetServers: ${serverUrls.join(", ")}\n`);
327
+ if (queryFailures.length) {
328
+ output.write(`serverFailures: ${queryFailures.length}\n`);
329
+ }
330
+ output.write(`total dialogs: ${dialogs.length}\n`);
331
+ for (const dialog of dialogs) {
332
+ output.write(
333
+ `\n${dialog.title}\nid=${dialog.id}\nstatus=${dialog.status ?? "-"}\nupdatedAt=${dialog.updatedAt ?? "-"}\ndbKey=${dialog.dbKey}\n`
334
+ );
335
+ }
336
+ return 0;
337
+ } catch (error) {
338
+ output.write(
339
+ `[nolo] dialog list failed: ${
340
+ error instanceof Error ? error.message : String(error)
341
+ }\n`
342
+ );
343
+ return 1;
344
+ }
345
+ }
346
+
347
+ export async function runDialogDeleteCommand(
348
+ args: string[],
349
+ deps: AgentCommandDeps = {}
350
+ ) {
351
+ const env = deps.env ?? process.env;
352
+ const output = deps.output ?? process.stdout;
353
+ if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
354
+ printDeleteUsage(output);
355
+ return 0;
356
+ }
357
+
358
+ const rawDialog = readFirstPositional(args);
359
+ if (!rawDialog) {
360
+ printDeleteUsage(output);
361
+ return 1;
362
+ }
363
+
364
+ const authToken = resolveAuthToken(args, env);
365
+ if (!authToken) {
366
+ output.write("[nolo] dialog delete requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
367
+ return 1;
368
+ }
369
+ const userId = parseUserIdFromAuthToken(authToken);
370
+ if (!userId) {
371
+ output.write("[nolo] dialog delete could not read userId from AUTH_TOKEN.\n");
372
+ return 1;
373
+ }
374
+
375
+ const fetchImpl = deps.fetchImpl ?? fetch;
376
+ const fallbackFetchImpl = deps.fallbackFetchImpl;
377
+ const serverUrl = resolveServerUrl(args, env);
378
+ const serverUrls = resolveServerCandidates(args, env, serverUrl);
379
+ const resolved = resolveDialogInput(rawDialog, userId);
380
+ const shouldDelete = hasFlag(args, "--yes");
381
+
382
+ try {
383
+ const readResult = await readDbRecordFromServers({
384
+ authToken,
385
+ dbKey: resolved.dbKey,
386
+ fallbackFetchImpl,
387
+ fetchImpl,
388
+ label: "dialog record",
389
+ serverUrls,
390
+ });
391
+ const dialog = normalizeDialogRecord(readResult.record, resolved.dbKey);
392
+ if (!dialog) {
393
+ throw new Error(`dialog record is unreadable: ${resolved.dbKey}`);
394
+ }
395
+
396
+ let deleteResults: Awaited<ReturnType<typeof deleteDbRecordOnServers>> = [];
397
+ if (shouldDelete) {
398
+ deleteResults = await deleteDbRecordOnServers({
399
+ authToken,
400
+ dbKey: resolved.dbKey,
401
+ fallbackFetchImpl,
402
+ fetchImpl,
403
+ serverUrls,
404
+ });
405
+ }
406
+ const failedDeletes = deleteResults.filter((result) => !result.ok);
407
+
408
+ const payload = {
409
+ dbKey: resolved.dbKey,
410
+ dialogId: resolved.dialogId,
411
+ title: dialog.title,
412
+ readServerUrl: readResult.serverUrl,
413
+ targetServers: serverUrls,
414
+ dryRun: !shouldDelete,
415
+ deleted: shouldDelete,
416
+ ...(readResult.failures.length ? { readFailures: readResult.failures } : {}),
417
+ ...(shouldDelete ? { deleteResults } : {}),
418
+ };
419
+
420
+ if (hasFlag(args, "--json")) {
421
+ output.write(JSON.stringify(payload, null, 2));
422
+ output.write("\n");
423
+ return failedDeletes.length > 0 ? 1 : 0;
424
+ }
425
+
426
+ output.write(`dialog: ${dialog.title}\n`);
427
+ output.write(`dbKey: ${resolved.dbKey}\n`);
428
+ output.write(`readServer: ${readResult.serverUrl}\n`);
429
+ output.write(`targetServers: ${serverUrls.join(", ")}\n`);
430
+ output.write(
431
+ shouldDelete
432
+ ? failedDeletes.length > 0
433
+ ? `Deleted dialog on ${deleteResults.length - failedDeletes.length}/${deleteResults.length} servers. Failed: ${failedDeletes.map((result) => `${result.serverUrl} (${result.error})`).join(", ")}\n`
434
+ : "Deleted dialog on all target servers. Server-side delete cascades dialog messages.\n"
435
+ : "Dry-run only. Add --yes to delete this dialog and its messages.\n"
436
+ );
437
+ return failedDeletes.length > 0 ? 1 : 0;
438
+ } catch (error) {
439
+ output.write(
440
+ `[nolo] dialog delete failed: ${
441
+ error instanceof Error ? error.message : String(error)
442
+ }\n`
443
+ );
444
+ return 1;
445
+ }
446
+ }
@@ -0,0 +1,13 @@
1
+ import type { CommandEntry } from "./cliCommandTypes";
2
+ import { createEnvCommand } from "./cliCommandFactories";
3
+ import {
4
+ runDialogDeleteCommand,
5
+ runDialogListCommand,
6
+ } from "./dialogCommands";
7
+
8
+ export function getDialogInternalCommandEntries(): CommandEntry[] {
9
+ return [
10
+ createEnvCommand(["dialog", "list"], "List dialogs", runDialogListCommand),
11
+ createEnvCommand(["dialog", "delete"], "Delete a dialog", runDialogDeleteCommand),
12
+ ];
13
+ }