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.
- package/README.md +15 -5
- package/agent-runtime/agentRecordConfig.ts +8 -0
- package/agent-runtime/hostAdapter.ts +9 -1
- package/agent-runtime/index.ts +10 -0
- package/agent-runtime/localLoop.ts +49 -2
- package/agent-runtime/localToolPolicy.ts +71 -22
- package/agent-runtime/localWorkspaceTools.ts +5 -2
- package/agent-runtime/openAiCompatibleProvider.ts +7 -3
- package/agent-runtime/openAiCompatibleProviderConfig.ts +15 -26
- package/agent-runtime/platformChatProvider.ts +48 -71
- package/agent-runtime/providerResolution.ts +242 -0
- package/agent-runtime/runtimeToolPolicy.ts +33 -0
- package/agent-runtime/shellCommandPolicy.ts +120 -0
- package/agentCommandSupport.ts +4 -0
- package/agentInternalCommandEntries.ts +2 -0
- package/agentListCommands.ts +74 -12
- package/agentListHelpers.ts +56 -1
- package/agentPullCommand.ts +6 -11
- package/agentRecordCommands.ts +87 -2
- package/agentRecordHelpers.ts +132 -9
- package/agentRuntimeLocal.ts +3 -0
- package/ai/tools/toolNameAliases.ts +70 -0
- package/cli/agentCommandSupport.ts +4 -0
- package/cli/agentInternalCommandEntries.ts +2 -0
- package/cli/agentListCommands.ts +74 -12
- package/cli/agentListHelpers.ts +56 -1
- package/cli/agentPullCommand.ts +6 -11
- package/cli/agentRecordCommands.ts +87 -2
- package/cli/agentRecordHelpers.ts +132 -9
- package/cli/agentRuntimeLocal.ts +3 -0
- package/cli/cliCommandDispatch.ts +2 -2
- package/cli/cliEnvHelpers.ts +80 -12
- package/cli/cliSpaceHelpers.ts +39 -0
- package/cli/client/agentRun.ts +1 -0
- package/cli/client/localRuntimeAdapter.ts +44 -7
- package/cli/commandRegistry.ts +2 -2
- package/cli/connectorWebSocketTarget.ts +10 -3
- package/cli/dialogCommands.ts +446 -0
- package/cli/dialogInternalCommandEntries.ts +13 -0
- package/cli/globalRecordOperations.ts +258 -0
- package/cli/internalCommandEntries.ts +2 -0
- package/cli/localRuntimeAuthority.ts +182 -0
- package/cli/localRuntimeDb.ts +2 -4
- package/cli/machineCommands.ts +1 -1
- package/cli/machineWsRunDispatch.ts +29 -12
- package/cli/machineWsSession.ts +2 -0
- package/cli/offlineMarxistsAgentCommand.ts +1 -1
- package/cli/runtimeDoctorCommands.ts +65 -9
- package/cli/tableCommands.ts +1 -1
- package/cli/taskRunCommand.ts +1 -1
- package/cliCommandDispatch.ts +2 -2
- package/cliEnvHelpers.ts +80 -12
- package/cliSpaceHelpers.ts +39 -0
- package/client/agentRun.ts +1 -0
- package/client/localProviderResolver.test.ts +26 -2
- package/client/localRuntimeAdapter.test.ts +287 -3
- package/client/localRuntimeAdapter.ts +44 -7
- package/commandRegistry.ts +2 -2
- package/connectorWebSocketTarget.ts +10 -3
- package/database/server/authorityStoreTypes.ts +31 -0
- package/database/server/cliAuthorityBrokerClient.ts +227 -0
- package/database/server/cliAuthorityBrokerServer.ts +283 -0
- package/database/server/cliAuthorityBrokerTypes.ts +43 -0
- package/database/server/cliAuthorityStoreDriver.ts +98 -0
- package/database/server/legacyServerDb.ts +58 -0
- package/database/server/levelAuthorityStore.ts +95 -0
- package/database/tombstones.ts +52 -0
- package/database/userDataMerge.ts +95 -0
- package/dialogCommands.ts +446 -0
- package/dialogInternalCommandEntries.ts +13 -0
- package/globalRecordOperations.ts +258 -0
- package/internalCommandEntries.ts +2 -0
- package/localRuntimeAuthority.ts +182 -0
- package/localRuntimeDb.ts +2 -4
- package/machineCommands.ts +1 -1
- package/machineWsRunDispatch.ts +29 -12
- package/machineWsSession.ts +2 -0
- package/offlineMarxistsAgentCommand.ts +1 -1
- package/package.json +24 -4
- package/runtimeDoctorCommands.ts +65 -9
- package/tableCommands.ts +1 -1
- package/taskRunCommand.ts +1 -1
- package/database/server/db.ts +0 -118
- package/database/server/ensureDbOpen.ts +0 -12
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
parsePlatformChatCompletionData,
|
|
17
17
|
parsePlatformChatCompletionResponse,
|
|
18
18
|
resolvePlatformChatProviderConfig,
|
|
19
|
+
resolveRequestedRuntimeToolNames,
|
|
19
20
|
shouldUsePlatformChatProvider,
|
|
20
21
|
} from "../agentRuntimeLocal";
|
|
21
22
|
import type { AgentRuntimeChatMessage } from "../../agent-runtime";
|
|
@@ -169,6 +170,12 @@ async function defaultLoopbackRequest(input: FetchInput, init?: FetchInit) {
|
|
|
169
170
|
});
|
|
170
171
|
});
|
|
171
172
|
req.on("error", reject);
|
|
173
|
+
init?.signal?.addEventListener("abort", () => {
|
|
174
|
+
req.destroy(
|
|
175
|
+
init.signal?.reason instanceof Error ? init.signal.reason : new Error("request aborted")
|
|
176
|
+
);
|
|
177
|
+
reject(init.signal?.reason ?? new Error("request aborted"));
|
|
178
|
+
}, { once: true });
|
|
172
179
|
if (body) req.write(body);
|
|
173
180
|
req.end();
|
|
174
181
|
});
|
|
@@ -191,6 +198,7 @@ async function fetchWithTransientRetry(
|
|
|
191
198
|
}
|
|
192
199
|
return await fetchImpl(input, init);
|
|
193
200
|
} catch (error) {
|
|
201
|
+
if (init?.signal?.aborted) throw error;
|
|
194
202
|
if (!isTransientFetchError(error)) throw error;
|
|
195
203
|
lastError = error;
|
|
196
204
|
if (attempt < TRANSIENT_FETCH_MAX_ATTEMPTS) {
|
|
@@ -201,6 +209,18 @@ async function fetchWithTransientRetry(
|
|
|
201
209
|
throw lastError;
|
|
202
210
|
}
|
|
203
211
|
|
|
212
|
+
function buildRequestTimeoutSignal(timeoutMs?: number) {
|
|
213
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
|
|
217
|
+
return AbortSignal.timeout(timeoutMs);
|
|
218
|
+
}
|
|
219
|
+
const controller = new AbortController();
|
|
220
|
+
setTimeout(() => controller.abort(new Error(`request timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
221
|
+
return controller.signal;
|
|
222
|
+
}
|
|
223
|
+
|
|
204
224
|
function buildOpenAiTools(args: { toolNames?: string[]; env: EnvLike }) {
|
|
205
225
|
const toolset = buildLocalWorkspaceToolsetForEnv(args);
|
|
206
226
|
return [
|
|
@@ -428,8 +448,11 @@ export function createCliLocalRuntimeAdapter(
|
|
|
428
448
|
store: await resolveStore(deps),
|
|
429
449
|
userId,
|
|
430
450
|
});
|
|
451
|
+
const requestedToolNames = agentConfig
|
|
452
|
+
? resolveRequestedRuntimeToolNames({ agentConfig })
|
|
453
|
+
: [];
|
|
431
454
|
activeAgentToolNames = buildLocalPolicyToolNames({
|
|
432
|
-
toolNames:
|
|
455
|
+
toolNames: requestedToolNames,
|
|
433
456
|
env: deps.env,
|
|
434
457
|
});
|
|
435
458
|
await activateAgentWorkspaceSession({ agentRef, agentConfig });
|
|
@@ -455,9 +478,12 @@ export function createCliLocalRuntimeAdapter(
|
|
|
455
478
|
});
|
|
456
479
|
return {
|
|
457
480
|
model: providerConfig.model,
|
|
458
|
-
complete: async (messages) => {
|
|
481
|
+
complete: async (messages, options) => {
|
|
482
|
+
const requestedToolNames = resolveRequestedRuntimeToolNames({
|
|
483
|
+
agentConfig,
|
|
484
|
+
});
|
|
459
485
|
const tools = buildOpenAiTools({
|
|
460
|
-
toolNames:
|
|
486
|
+
toolNames: requestedToolNames,
|
|
461
487
|
env: deps.env,
|
|
462
488
|
});
|
|
463
489
|
const request = buildPlatformChatCompletionRequest({
|
|
@@ -465,7 +491,11 @@ export function createCliLocalRuntimeAdapter(
|
|
|
465
491
|
messages,
|
|
466
492
|
tools,
|
|
467
493
|
});
|
|
468
|
-
const
|
|
494
|
+
const signal = buildRequestTimeoutSignal(options?.timeoutMs);
|
|
495
|
+
const res = await fetchWithTransientRetry(fetchImpl, request.url, {
|
|
496
|
+
...request.init,
|
|
497
|
+
...(signal ? { signal } : {}),
|
|
498
|
+
}, {
|
|
469
499
|
sleep: deps.sleep,
|
|
470
500
|
loopbackRequest,
|
|
471
501
|
});
|
|
@@ -488,9 +518,12 @@ export function createCliLocalRuntimeAdapter(
|
|
|
488
518
|
});
|
|
489
519
|
return {
|
|
490
520
|
model: providerConfig.model,
|
|
491
|
-
complete: async (messages) => {
|
|
521
|
+
complete: async (messages, options) => {
|
|
522
|
+
const requestedToolNames = resolveRequestedRuntimeToolNames({
|
|
523
|
+
agentConfig,
|
|
524
|
+
});
|
|
492
525
|
const tools = buildOpenAiTools({
|
|
493
|
-
toolNames:
|
|
526
|
+
toolNames: requestedToolNames,
|
|
494
527
|
env: deps.env,
|
|
495
528
|
});
|
|
496
529
|
const request = buildOpenAiCompatibleChatCompletionRequest({
|
|
@@ -498,7 +531,11 @@ export function createCliLocalRuntimeAdapter(
|
|
|
498
531
|
messages,
|
|
499
532
|
tools,
|
|
500
533
|
});
|
|
501
|
-
const
|
|
534
|
+
const signal = buildRequestTimeoutSignal(options?.timeoutMs);
|
|
535
|
+
const res = await fetchWithTransientRetry(fetchImpl, request.url, {
|
|
536
|
+
...request.init,
|
|
537
|
+
...(signal ? { signal } : {}),
|
|
538
|
+
}, {
|
|
502
539
|
sleep: deps.sleep,
|
|
503
540
|
loopbackRequest,
|
|
504
541
|
});
|
package/cli/commandRegistry.ts
CHANGED
|
@@ -49,8 +49,8 @@ export function renderHelpText() {
|
|
|
49
49
|
" nolo connect",
|
|
50
50
|
" nolo connect --watch",
|
|
51
51
|
" nolo connect --ws",
|
|
52
|
-
" nolo connect --ws --server-url https://api.nolo.chat --
|
|
53
|
-
" nolo connect --daemon --server-url https://api.nolo.chat --
|
|
52
|
+
" nolo connect --ws --server-url https://api.nolo.chat --machine-key sk_machine_xxx",
|
|
53
|
+
" nolo connect --daemon --server-url https://api.nolo.chat --machine-key sk_machine_xxx",
|
|
54
54
|
" nolo machine status",
|
|
55
55
|
" nolo doctor",
|
|
56
56
|
" nolo doctor runtime",
|
|
@@ -49,14 +49,21 @@ function parseRetryAfterMs(response: Response, bodyRetryAfterMs?: unknown) {
|
|
|
49
49
|
export async function resolveConnectorWebSocketTarget(input: {
|
|
50
50
|
serverUrl: string;
|
|
51
51
|
machineId: string;
|
|
52
|
+
connectorSurface?: string;
|
|
52
53
|
headers: Record<string, string>;
|
|
53
54
|
fetchImpl?: typeof fetch;
|
|
54
55
|
}) {
|
|
55
56
|
const directWsUrl = new URL(input.serverUrl);
|
|
56
57
|
directWsUrl.protocol = directWsUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
57
|
-
directWsUrl.pathname = "/api/connector/ws";
|
|
58
|
-
directWsUrl.search = "";
|
|
59
|
-
directWsUrl.searchParams.set("machineId", input.machineId);
|
|
58
|
+
directWsUrl.pathname = "/api/connector/ws";
|
|
59
|
+
directWsUrl.search = "";
|
|
60
|
+
directWsUrl.searchParams.set("machineId", input.machineId);
|
|
61
|
+
if (input.connectorSurface?.trim()) {
|
|
62
|
+
directWsUrl.searchParams.set(
|
|
63
|
+
"connectorSurface",
|
|
64
|
+
input.connectorSurface.trim().toLowerCase()
|
|
65
|
+
);
|
|
66
|
+
}
|
|
60
67
|
|
|
61
68
|
const probeUrl = new URL(input.serverUrl);
|
|
62
69
|
probeUrl.pathname = "/api/connector/ws";
|
|
@@ -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
|
+
}
|