nolo-cli 0.1.41 → 0.1.42
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 +27 -12
- package/agent-runtime/agentThread.ts +222 -0
- package/agent-runtime/agentThreadAdmission.ts +74 -0
- package/agent-runtime/dialogWritePlan.ts +42 -0
- package/agent-runtime/hostAdapter.ts +1 -0
- package/agent-runtime/index.ts +43 -0
- package/agent-runtime/localLoop.ts +31 -2
- package/agent-runtime/localToolPolicy.ts +1 -0
- package/agent-runtime/localWorkspaceTools.ts +1258 -78
- package/agent-runtime/noloWorkspaceTools.ts +20 -1
- package/agent-runtime/runtimeToolPolicy.ts +1 -5
- package/agent-runtime/runtimeToolSurface.ts +1 -0
- package/agentAliases.ts +29 -9
- package/agentInternalCommandEntries.ts +11 -3
- package/agentMachineCommands.ts +1 -0
- package/agentRecordCommands.ts +2 -2
- package/agentRecordHelpers.ts +59 -6
- package/agentRunCommand.ts +84 -19
- package/ai/agent/buildSystemPrompt.ts +14 -0
- package/ai/agent/cliExecutor.ts +202 -43
- package/ai/agent/createAgentSchema.ts +2 -2
- package/ai/agent/liveAudioModel.ts +1 -1
- package/ai/agent/streamAgentChatTurn.ts +154 -20
- package/ai/agent/streamAgentChatTurnUtils.ts +63 -0
- package/ai/ai.locale.ts +70 -0
- package/ai/chat/inlineImageUrlsForCustomProvider.ts +7 -2
- package/ai/chat/sendOpenAICompletionsRequest.ts +25 -1
- package/ai/llm/kimi.ts +0 -1
- package/ai/llm/openrouterModels.ts +27 -2
- package/ai/memory/policy.ts +49 -0
- package/ai/memory/queryShared.ts +10 -4
- package/ai/memory/recentRelationshipRecap.ts +4 -0
- package/ai/memory/remember.ts +48 -104
- package/ai/memory/scope.ts +74 -0
- package/ai/memory/types.ts +6 -0
- package/ai/memory/understanding.ts +10 -18
- package/ai/policy/runtimePolicy.ts +95 -23
- package/ai/tools/agent/agentTools.ts +0 -21
- package/ai/tools/agent/startAgentDialogTool.ts +2 -1
- package/ai/tools/index.ts +2 -20
- package/ai/tools/prepareTools.ts +171 -1
- package/ai/tools/readXhsProfileTool.ts +80 -69
- package/ai/tools/table/createTableTool.ts +11 -0
- package/ai/tools/toolPacks.ts +19 -0
- package/ai/types.ts +3 -0
- package/app/favorite/deletedFavoriteProjection.ts +31 -0
- package/app/hooks/deleteDbKey.ts +24 -3
- package/app/i18n/translations/interface.locale.ts +48 -16
- package/app/types/appSummary.ts +4 -0
- package/app/utils/desktopAgentRuntimeTurnClient.ts +2 -0
- package/auth/adminPermissions.ts +64 -4
- package/auth/routes.ts +5 -0
- package/chat/chat.locale.ts +4 -0
- package/chat/dialog/actions/handleSendMessageAction.ts +7 -2
- package/chat/dialog/dialogAttachmentCleanup.ts +135 -0
- package/chat/dialog/dialogSlice.ts +117 -4
- package/chat/messages/activityCompletion.ts +97 -0
- package/chat/messages/imagePayloadPersistence.ts +22 -0
- package/chat/messages/messageContent.ts +20 -16
- package/chat/messages/messageSlice.ts +20 -1
- package/chat/messages/toolThunks.ts +32 -11
- package/chat/messages/types.ts +8 -1
- package/chat/messages/web/toolDisplayName.ts +686 -0
- package/cli/agentAliases.ts +29 -9
- package/cli/agentInternalCommandEntries.ts +11 -3
- package/cli/agentMachineCommands.ts +1 -0
- package/cli/agentRecordCommands.ts +2 -2
- package/cli/agentRecordHelpers.ts +59 -6
- package/cli/agentRunCommand.ts +84 -19
- package/cli/client/agentRun.ts +65 -253
- package/cli/client/localRuntimeAdapter.ts +179 -53
- package/cli/commandRegistry.ts +7 -4
- package/cli/dialogAttachmentCleanup.ts +151 -0
- package/cli/dialogCommands.ts +163 -61
- package/cli/dialogInternalCommandEntries.ts +1 -1
- package/cli/docCreateCommands.ts +345 -0
- package/cli/docInternalCommandEntries.ts +11 -0
- package/cli/docPageHelpers.ts +80 -0
- package/cli/docSkillHelpers.ts +67 -0
- package/cli/docSpaceHelpers.ts +46 -0
- package/cli/internalCommandEntries.ts +4 -0
- package/cli/machineCommands.ts +18 -34
- package/cli/memoryCommands.ts +253 -0
- package/cli/memoryInternalCommandEntries.ts +9 -0
- package/cli/qoderUsageProbe.ts +252 -0
- package/cli/scriptCommandEntries.ts +0 -3
- package/cli/secretScan.ts +31 -0
- package/cli/tableCommands.ts +248 -13
- package/cli/workflowInternalCommandEntries.ts +2 -3
- package/client/agentRun.test.ts +68 -84
- package/client/agentRun.ts +65 -253
- package/client/localRuntimeAdapter.test.ts +505 -74
- package/client/localRuntimeAdapter.ts +179 -53
- package/commandRegistry.ts +7 -4
- package/connector-experimental/capabilities.ts +10 -9
- package/create/space/sidebarVisibleTypes.ts +19 -0
- package/dialogAttachmentCleanup.ts +151 -0
- package/dialogCommands.ts +163 -61
- package/dialogInternalCommandEntries.ts +1 -1
- package/docCreateCommands.ts +345 -0
- package/docInternalCommandEntries.ts +11 -0
- package/docPageHelpers.ts +80 -0
- package/docSkillHelpers.ts +67 -0
- package/docSpaceHelpers.ts +46 -0
- package/integrations/google/models.ts +4 -4
- package/integrations/openai/generateOpenAIRequestBody.ts +0 -3
- package/integrations/xhs-reader/types.ts +36 -3
- package/integrations/xhs-reader/url.ts +24 -1
- package/internalCommandEntries.ts +4 -0
- package/machineCommands.ts +18 -34
- package/memoryCommands.ts +253 -0
- package/memoryInternalCommandEntries.ts +9 -0
- package/package.json +133 -114
- package/qoderUsageProbe.ts +252 -0
- package/render/contentIcon/types.ts +167 -0
- package/render/page/docSlice.ts +30 -3
- package/render/page/types.ts +2 -0
- package/render/table/createTableAction.ts +4 -0
- package/render/table/tableSlice.ts +61 -0
- package/render/table/types.ts +10 -0
- package/scriptCommandEntries.ts +0 -3
- package/secretScan.ts +31 -0
- package/server/handlers/agentRun/types.ts +24 -23
- package/tableCommands.ts +248 -13
- package/workflowInternalCommandEntries.ts +2 -3
- package/ai/tools/agent/taskRunTool.ts +0 -112
- package/cli/client/taskRunPrompt.ts +0 -27
- package/cli/taskRunCommand.ts +0 -265
- package/client/taskRunPrompt.ts +0 -27
- package/integrations/xhs-reader/analyze/profileAnalyzer.ts +0 -205
- package/integrations/xhs-reader/backends/playwrightProfileCollector.ts +0 -624
- package/integrations/xhs-reader/bridge/readXhsProfileWithBridge.ts +0 -227
- package/integrations/xhs-reader/client/xhsWebApiClient.ts +0 -229
- package/integrations/xhs-reader/normalize.ts +0 -390
- package/integrations/xhs-reader/orchestrator.ts +0 -178
- package/taskRunCommand.ts +0 -265
package/dialogCommands.ts
CHANGED
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
readDbRecordFromServers,
|
|
14
14
|
readLiveDbRecordAfterTombstoneMerge,
|
|
15
15
|
} from "./globalRecordOperations";
|
|
16
|
+
import {
|
|
17
|
+
deleteDialogAttachmentCandidates,
|
|
18
|
+
planDialogAttachmentCleanup,
|
|
19
|
+
} from "./dialogAttachmentCleanup";
|
|
16
20
|
|
|
17
21
|
type ReadSource = "http" | "local-db-fallback";
|
|
18
22
|
|
|
@@ -45,6 +49,7 @@ const VALUE_FLAGS = new Set([
|
|
|
45
49
|
"--space",
|
|
46
50
|
"--space-id",
|
|
47
51
|
"--token",
|
|
52
|
+
"--machine-key",
|
|
48
53
|
"--user",
|
|
49
54
|
]);
|
|
50
55
|
|
|
@@ -64,6 +69,21 @@ function readFirstPositional(args: string[]) {
|
|
|
64
69
|
return undefined;
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
function readAllPositional(args: string[]) {
|
|
73
|
+
const positionals: string[] = [];
|
|
74
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
75
|
+
const value = args[index];
|
|
76
|
+
if (VALUE_FLAGS.has(value)) {
|
|
77
|
+
index += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!value.startsWith("-")) {
|
|
81
|
+
positionals.push(value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return positionals;
|
|
85
|
+
}
|
|
86
|
+
|
|
67
87
|
function readLimit(args: string[], fallback: number) {
|
|
68
88
|
const raw = readOption(args, "--limit");
|
|
69
89
|
const parsed = raw ? Number(raw) : fallback;
|
|
@@ -194,15 +214,20 @@ Lists merge global server candidates and ignore newer tombstones.
|
|
|
194
214
|
|
|
195
215
|
function printDeleteUsage(output: { write(chunk: string): unknown }) {
|
|
196
216
|
output.write(`Usage:
|
|
197
|
-
nolo dialog delete <dialogId|dialogKey|dialogUrl
|
|
217
|
+
nolo dialog delete <dialogId|dialogKey|dialogUrl...> [--yes] [--include-attachments] [--include-referenced-attachments] [--json]
|
|
198
218
|
|
|
199
219
|
Options:
|
|
200
|
-
--yes
|
|
201
|
-
--
|
|
202
|
-
--
|
|
203
|
-
|
|
220
|
+
--yes Actually delete. Without this, the command is a dry-run.
|
|
221
|
+
--include-attachments Also delete files explicitly owned by this dialog/messages.
|
|
222
|
+
--include-referenced-attachments
|
|
223
|
+
Also delete same-account user-owned files referenced by this dialog.
|
|
224
|
+
--json Print machine-readable JSON.
|
|
225
|
+
--server <url> Prefer this server and include it in global deletion.
|
|
226
|
+
--token <jwt> Override AUTH_TOKEN.
|
|
204
227
|
|
|
205
228
|
Deletes are global across the selected/profile server plus known Nolo cluster peers.
|
|
229
|
+
Attachment cleanup is conservative by default. Use --include-referenced-attachments only
|
|
230
|
+
when you intentionally want to remove user-owned files referenced by the dialog too.
|
|
206
231
|
`);
|
|
207
232
|
}
|
|
208
233
|
|
|
@@ -927,8 +952,8 @@ export async function runDialogDeleteCommand(
|
|
|
927
952
|
return 0;
|
|
928
953
|
}
|
|
929
954
|
|
|
930
|
-
const
|
|
931
|
-
if (
|
|
955
|
+
const rawDialogs = readAllPositional(args);
|
|
956
|
+
if (rawDialogs.length === 0) {
|
|
932
957
|
printDeleteUsage(output);
|
|
933
958
|
return 1;
|
|
934
959
|
}
|
|
@@ -948,71 +973,148 @@ export async function runDialogDeleteCommand(
|
|
|
948
973
|
const fallbackFetchImpl = deps.fallbackFetchImpl;
|
|
949
974
|
const serverUrl = resolveServerUrl(args, env);
|
|
950
975
|
const serverUrls = resolveServerCandidates(args, env, serverUrl);
|
|
951
|
-
const resolved = resolveDialogInput(rawDialog, userId);
|
|
952
976
|
const shouldDelete = hasFlag(args, "--yes");
|
|
977
|
+
const includeReferencedAttachments = hasFlag(args, "--include-referenced-attachments");
|
|
978
|
+
const includeAttachments = hasFlag(args, "--include-attachments") || includeReferencedAttachments;
|
|
953
979
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
authToken,
|
|
957
|
-
dbKey: resolved.dbKey,
|
|
958
|
-
fallbackFetchImpl,
|
|
959
|
-
fetchImpl,
|
|
960
|
-
label: "dialog record",
|
|
961
|
-
serverUrls,
|
|
962
|
-
});
|
|
963
|
-
const dialog = normalizeDialogRecord(readResult.record, resolved.dbKey);
|
|
964
|
-
if (!dialog) {
|
|
965
|
-
throw new Error(`dialog record is unreadable: ${resolved.dbKey}`);
|
|
966
|
-
}
|
|
980
|
+
let hasErrors = false;
|
|
981
|
+
const payloads: any[] = [];
|
|
967
982
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
983
|
+
for (const rawDialog of rawDialogs) {
|
|
984
|
+
try {
|
|
985
|
+
const resolved = resolveDialogInput(rawDialog, userId);
|
|
986
|
+
const readResult = await readDbRecordFromServers({
|
|
971
987
|
authToken,
|
|
972
988
|
dbKey: resolved.dbKey,
|
|
973
989
|
fallbackFetchImpl,
|
|
974
990
|
fetchImpl,
|
|
991
|
+
label: "dialog record",
|
|
975
992
|
serverUrls,
|
|
976
993
|
});
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
994
|
+
const dialog = normalizeDialogRecord(readResult.record, resolved.dbKey);
|
|
995
|
+
if (!dialog) {
|
|
996
|
+
throw new Error(`dialog record is unreadable: ${resolved.dbKey}`);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const attachmentPlan = includeAttachments
|
|
1000
|
+
? await planDialogAttachmentCleanup({
|
|
1001
|
+
authToken,
|
|
1002
|
+
dialogId: resolved.dialogId,
|
|
1003
|
+
ownerId: resolved.ownerId,
|
|
1004
|
+
includeUserOwnedReferenced: includeReferencedAttachments,
|
|
1005
|
+
fallbackFetchImpl,
|
|
1006
|
+
fetchImpl,
|
|
1007
|
+
serverUrl: readResult.serverUrl,
|
|
1008
|
+
serverUrls,
|
|
1009
|
+
})
|
|
1010
|
+
: null;
|
|
1011
|
+
|
|
1012
|
+
const attachmentDeleteResults =
|
|
1013
|
+
includeAttachments && shouldDelete && attachmentPlan
|
|
1014
|
+
? await deleteDialogAttachmentCandidates({
|
|
1015
|
+
authToken,
|
|
1016
|
+
candidates: attachmentPlan.deleteCandidates,
|
|
1017
|
+
fallbackFetchImpl,
|
|
1018
|
+
fetchImpl,
|
|
1019
|
+
serverUrls,
|
|
1020
|
+
})
|
|
1021
|
+
: [];
|
|
1022
|
+
|
|
1023
|
+
let deleteResults: Awaited<ReturnType<typeof deleteDbRecordOnServers>> = [];
|
|
1024
|
+
if (shouldDelete) {
|
|
1025
|
+
deleteResults = await deleteDbRecordOnServers({
|
|
1026
|
+
authToken,
|
|
1027
|
+
dbKey: resolved.dbKey,
|
|
1028
|
+
fallbackFetchImpl,
|
|
1029
|
+
fetchImpl,
|
|
1030
|
+
serverUrls,
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
const failedDeletes = deleteResults.filter((result) => !result.ok);
|
|
1034
|
+
const failedAttachmentDeletes = attachmentDeleteResults.flatMap((result) =>
|
|
1035
|
+
result.deleteResults
|
|
1036
|
+
.filter((deleteResult) => !deleteResult.ok)
|
|
1037
|
+
.map((deleteResult) => ({
|
|
1038
|
+
fileId: result.fileId,
|
|
1039
|
+
fileDbKey: result.fileDbKey,
|
|
1040
|
+
...deleteResult,
|
|
1041
|
+
}))
|
|
1042
|
+
);
|
|
991
1043
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1044
|
+
if (failedDeletes.length > 0 || failedAttachmentDeletes.length > 0) {
|
|
1045
|
+
hasErrors = true;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const payload = {
|
|
1049
|
+
dbKey: resolved.dbKey,
|
|
1050
|
+
dialogId: resolved.dialogId,
|
|
1051
|
+
title: dialog.title,
|
|
1052
|
+
readServerUrl: readResult.serverUrl,
|
|
1053
|
+
targetServers: serverUrls,
|
|
1054
|
+
dryRun: !shouldDelete,
|
|
1055
|
+
deleted: shouldDelete,
|
|
1056
|
+
includeAttachments,
|
|
1057
|
+
includeReferencedAttachments,
|
|
1058
|
+
...(attachmentPlan ? { attachmentPlan } : {}),
|
|
1059
|
+
...(readResult.failures.length ? { readFailures: readResult.failures } : {}),
|
|
1060
|
+
...(shouldDelete ? { deleteResults } : {}),
|
|
1061
|
+
...(attachmentDeleteResults.length ? { attachmentDeleteResults } : {}),
|
|
1062
|
+
};
|
|
1063
|
+
payloads.push(payload);
|
|
1064
|
+
|
|
1065
|
+
if (!hasFlag(args, "--json")) {
|
|
1066
|
+
output.write(`dialog: ${dialog.title}\n`);
|
|
1067
|
+
output.write(`dbKey: ${resolved.dbKey}\n`);
|
|
1068
|
+
output.write(`readServer: ${readResult.serverUrl}\n`);
|
|
1069
|
+
output.write(`targetServers: ${serverUrls.join(", ")}\n`);
|
|
1070
|
+
if (attachmentPlan) {
|
|
1071
|
+
output.write(
|
|
1072
|
+
`attachments: ${attachmentPlan.deleteCandidates.length} delete candidates, ${attachmentPlan.retainedCandidates.length} retained, ${attachmentPlan.bytesToDelete} bytes planned\n`
|
|
1073
|
+
);
|
|
1074
|
+
for (const candidate of attachmentPlan.retainedCandidates) {
|
|
1075
|
+
output.write(`retained attachment: ${candidate.fileId} (${candidate.reason})\n`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
output.write(
|
|
1079
|
+
shouldDelete
|
|
1080
|
+
? failedDeletes.length > 0
|
|
1081
|
+
? `Deleted dialog on ${deleteResults.length - failedDeletes.length}/${deleteResults.length} servers. Failed: ${failedDeletes.map((result) => `${result.serverUrl} (${result.error})`).join(", ")}\n`
|
|
1082
|
+
: "Deleted dialog on all target servers. Server-side delete cascades dialog messages.\n"
|
|
1083
|
+
: includeAttachments
|
|
1084
|
+
? "Dry-run only. Add --yes to delete this dialog, its messages, and delete-candidate attachments.\n"
|
|
1085
|
+
: "Dry-run only. Add --yes to delete this dialog and its messages.\n"
|
|
1086
|
+
);
|
|
1087
|
+
if (failedAttachmentDeletes.length > 0) {
|
|
1088
|
+
output.write(
|
|
1089
|
+
`Attachment delete failures: ${failedAttachmentDeletes.map((result) => `${result.fileDbKey}@${result.serverUrl} (${result.error})`).join(", ")}\n`
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
if (rawDialogs.length > 1) {
|
|
1093
|
+
output.write("\n");
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
hasErrors = true;
|
|
1098
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1099
|
+
if (hasFlag(args, "--json")) {
|
|
1100
|
+
payloads.push({
|
|
1101
|
+
rawDialog,
|
|
1102
|
+
error: errorMsg,
|
|
1103
|
+
});
|
|
1104
|
+
} else {
|
|
1105
|
+
output.write(`[nolo] dialog delete failed for ${rawDialog}: ${errorMsg}\n`);
|
|
1106
|
+
if (rawDialogs.length > 1) {
|
|
1107
|
+
output.write("\n");
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
996
1110
|
}
|
|
1111
|
+
}
|
|
997
1112
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
output.write(
|
|
1001
|
-
output.write(
|
|
1002
|
-
output.write(
|
|
1003
|
-
shouldDelete
|
|
1004
|
-
? failedDeletes.length > 0
|
|
1005
|
-
? `Deleted dialog on ${deleteResults.length - failedDeletes.length}/${deleteResults.length} servers. Failed: ${failedDeletes.map((result) => `${result.serverUrl} (${result.error})`).join(", ")}\n`
|
|
1006
|
-
: "Deleted dialog on all target servers. Server-side delete cascades dialog messages.\n"
|
|
1007
|
-
: "Dry-run only. Add --yes to delete this dialog and its messages.\n"
|
|
1008
|
-
);
|
|
1009
|
-
return failedDeletes.length > 0 ? 1 : 0;
|
|
1010
|
-
} catch (error) {
|
|
1011
|
-
output.write(
|
|
1012
|
-
`[nolo] dialog delete failed: ${
|
|
1013
|
-
error instanceof Error ? error.message : String(error)
|
|
1014
|
-
}\n`
|
|
1015
|
-
);
|
|
1016
|
-
return 1;
|
|
1113
|
+
if (hasFlag(args, "--json")) {
|
|
1114
|
+
const finalPayload = rawDialogs.length === 1 ? payloads[0] : payloads;
|
|
1115
|
+
output.write(JSON.stringify(finalPayload, null, 2));
|
|
1116
|
+
output.write("\n");
|
|
1017
1117
|
}
|
|
1118
|
+
|
|
1119
|
+
return hasErrors ? 1 : 0;
|
|
1018
1120
|
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
export function getDialogInternalCommandEntries(): CommandEntry[] {
|
|
11
11
|
return [
|
|
12
12
|
createEnvCommand(["dialog", "list"], "List dialogs", runDialogListCommand),
|
|
13
|
-
createEnvCommand(["dialog", "delete"], "Delete
|
|
13
|
+
createEnvCommand(["dialog", "delete"], "Delete one or more dialogs", runDialogDeleteCommand),
|
|
14
14
|
createEnvCommand(["dialog", "read"], "Read a dialog", runDialogReadCommand),
|
|
15
15
|
createEnvCommand(["dialog", "status"], "Read compact dialog status", runDialogStatusCommand),
|
|
16
16
|
];
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { NOLO_CLUSTER_SERVERS } from "./database/config";
|
|
2
|
+
import { DEFAULT_LOCAL_API_ORIGIN } from "./core/localOrigins";
|
|
3
|
+
import { buildSkillSummaryMarker } from "./ai/skills/skillSummaryMarker";
|
|
4
|
+
import { writeAgentRecord } from "./agentRecordHelpers";
|
|
5
|
+
import {
|
|
6
|
+
parseUserIdFromAuthToken,
|
|
7
|
+
readOption,
|
|
8
|
+
resolveAuthToken,
|
|
9
|
+
resolveServerUrl,
|
|
10
|
+
} from "./cliEnvHelpers";
|
|
11
|
+
import {
|
|
12
|
+
buildPageKey,
|
|
13
|
+
buildPageRecord,
|
|
14
|
+
createPageId,
|
|
15
|
+
readBodyArg,
|
|
16
|
+
} from "./docPageHelpers";
|
|
17
|
+
import {
|
|
18
|
+
buildSkillDocId,
|
|
19
|
+
buildSkillPageKey,
|
|
20
|
+
buildSkillPageRecord,
|
|
21
|
+
parseJsonArg,
|
|
22
|
+
} from "./docSkillHelpers";
|
|
23
|
+
import { ensurePageAttachedToSpace } from "./docSpaceHelpers";
|
|
24
|
+
import { findPotentialSecrets, formatSecretFindings } from "./secretScan";
|
|
25
|
+
|
|
26
|
+
type DocKind = "page" | "skill";
|
|
27
|
+
|
|
28
|
+
type DocCreateDeps = {
|
|
29
|
+
env: NodeJS.ProcessEnv;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const VALUE_FLAGS = new Set([
|
|
33
|
+
"--title",
|
|
34
|
+
"--description",
|
|
35
|
+
"--body",
|
|
36
|
+
"--body-file",
|
|
37
|
+
"--id",
|
|
38
|
+
"--space",
|
|
39
|
+
"--sync",
|
|
40
|
+
"--server",
|
|
41
|
+
"--server-url",
|
|
42
|
+
"--token",
|
|
43
|
+
"--tools",
|
|
44
|
+
"--required-skills",
|
|
45
|
+
"--recommended-skills",
|
|
46
|
+
"--preferred-agents",
|
|
47
|
+
"--trigger-mode",
|
|
48
|
+
"--budget-tier",
|
|
49
|
+
"--prompt-patch",
|
|
50
|
+
"--kind",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
function hasHelpArg(args: string[]) {
|
|
54
|
+
return args.includes("--help") || args.includes("-h");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hasFlag(args: string[], flag: string) {
|
|
58
|
+
return args.includes(flag);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function printDocCreateUsage(output: { write(chunk: string): unknown }) {
|
|
62
|
+
output.write(`Usage:
|
|
63
|
+
nolo doc create --title <title> [--body ... | --body-file path] [--id ...] [--space ...]
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
--description <text> Optional page description stored in meta.
|
|
67
|
+
--sync local,main,us Choose write targets. Values may also be full URLs.
|
|
68
|
+
--local-only Write only to the local server target.
|
|
69
|
+
--no-local Exclude local targets when --sync includes them.
|
|
70
|
+
--dry-run Build and validate without writing.
|
|
71
|
+
--json Print machine-readable JSON.
|
|
72
|
+
--allow-secrets Allow bodies that look like credentials.
|
|
73
|
+
--server <url> Default write target when --sync is omitted.
|
|
74
|
+
--token <jwt> Auth token. Required for writes.
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function printSkillDocCreateUsage(output: { write(chunk: string): unknown }) {
|
|
79
|
+
output.write(`Usage:
|
|
80
|
+
nolo skill-doc create --title <title> --description <text> [--body ... | --body-file path]
|
|
81
|
+
nolo skill create-doc --title <title> --description <text> [--body ... | --body-file path]
|
|
82
|
+
|
|
83
|
+
Options:
|
|
84
|
+
--tools '["readDoc"]' Tool names this skill expects.
|
|
85
|
+
--required-skills '["skill-a"]' Required skill references.
|
|
86
|
+
--recommended-skills '["skill-b"]' Recommended skill references.
|
|
87
|
+
--preferred-agents '["agent-key"]' Preferred agent keys.
|
|
88
|
+
--trigger-mode explicit|required|recommended
|
|
89
|
+
--budget-tier low|medium|high
|
|
90
|
+
--prompt-patch <text>
|
|
91
|
+
--page | --kind page Create a normal page instead of a skill-backed page.
|
|
92
|
+
--sync local,main,us Choose write targets. Values may also be full URLs.
|
|
93
|
+
--dry-run Build and validate without writing.
|
|
94
|
+
--json Print machine-readable JSON.
|
|
95
|
+
--token <jwt> Auth token. Required for writes.
|
|
96
|
+
`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeBaseUrl(base: string) {
|
|
100
|
+
return base.trim().replace(/\/+$/, "");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function localBaseFromEnv(env: NodeJS.ProcessEnv) {
|
|
104
|
+
return normalizeBaseUrl(env.SCRIPT_LOCAL_BASE_URL || DEFAULT_LOCAL_API_ORIGIN);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function targetToBase(target: string, env: NodeJS.ProcessEnv) {
|
|
108
|
+
const normalized = target.trim().toLowerCase();
|
|
109
|
+
if (normalized === "local") return localBaseFromEnv(env);
|
|
110
|
+
if (normalized === "main") return normalizeBaseUrl(NOLO_CLUSTER_SERVERS[0]);
|
|
111
|
+
if (normalized === "us") return normalizeBaseUrl(NOLO_CLUSTER_SERVERS[1]);
|
|
112
|
+
return normalizeBaseUrl(target);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseSyncTargets(raw: string | undefined, env: NodeJS.ProcessEnv) {
|
|
116
|
+
if (!raw) return undefined;
|
|
117
|
+
return raw
|
|
118
|
+
.split(",")
|
|
119
|
+
.map((value) => value.trim())
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.filter((value) => value !== "none")
|
|
122
|
+
.map((value) => targetToBase(value, env));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isLocalBaseUrl(baseUrl: string) {
|
|
126
|
+
try {
|
|
127
|
+
const hostname = new URL(baseUrl).hostname;
|
|
128
|
+
return hostname === "localhost" || hostname === "127.0.0.1";
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resolveWriteTargets(args: string[], env: NodeJS.ProcessEnv) {
|
|
135
|
+
const explicitSync = parseSyncTargets(readOption(args, "--sync"), env);
|
|
136
|
+
const targets = hasFlag(args, "--local-only")
|
|
137
|
+
? [localBaseFromEnv(env)]
|
|
138
|
+
: explicitSync ?? [resolveServerUrl(args, env)];
|
|
139
|
+
const filtered = hasFlag(args, "--no-local")
|
|
140
|
+
? targets.filter((target) => !isLocalBaseUrl(target))
|
|
141
|
+
: targets;
|
|
142
|
+
return Array.from(new Set(filtered.map(normalizeBaseUrl)));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readTitle(args: string[]) {
|
|
146
|
+
return readOption(args, "--title") ?? args.find((value, index) => {
|
|
147
|
+
if (index === 0) return false;
|
|
148
|
+
if (VALUE_FLAGS.has(args[index - 1])) return false;
|
|
149
|
+
return !value.startsWith("-");
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveDocKind(args: string[], fallback: DocKind): DocKind {
|
|
154
|
+
if (hasFlag(args, "--page") || readOption(args, "--kind") === "page") return "page";
|
|
155
|
+
return fallback;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function requireTokenUser(args: string[], env: NodeJS.ProcessEnv, dryRun: boolean) {
|
|
159
|
+
const authToken = resolveAuthToken(args, env);
|
|
160
|
+
const userId = parseUserIdFromAuthToken(authToken) || env.USER_ID || "";
|
|
161
|
+
if (!authToken && !dryRun) {
|
|
162
|
+
throw new Error("doc create requires an auth token. Pass --token or set AUTH_TOKEN.");
|
|
163
|
+
}
|
|
164
|
+
if (!userId && !dryRun) {
|
|
165
|
+
throw new Error("auth token does not contain userId. Pass a user-scoped token.");
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
authToken,
|
|
169
|
+
userId: userId || "dry-run",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function writePageRecord(args: {
|
|
174
|
+
authToken: string;
|
|
175
|
+
dbKey: string;
|
|
176
|
+
record: Record<string, any>;
|
|
177
|
+
serverUrl: string;
|
|
178
|
+
spaceId: string | null;
|
|
179
|
+
title: string;
|
|
180
|
+
userId: string;
|
|
181
|
+
skillSummary?: Record<string, any> | null;
|
|
182
|
+
}) {
|
|
183
|
+
await writeAgentRecord({
|
|
184
|
+
agentKey: args.dbKey,
|
|
185
|
+
authToken: args.authToken,
|
|
186
|
+
fetchImpl: fetch,
|
|
187
|
+
serverUrl: args.serverUrl,
|
|
188
|
+
userId: args.userId,
|
|
189
|
+
record: args.record,
|
|
190
|
+
});
|
|
191
|
+
if (args.spaceId) {
|
|
192
|
+
await ensurePageAttachedToSpace({
|
|
193
|
+
baseUrl: args.serverUrl,
|
|
194
|
+
userId: args.userId,
|
|
195
|
+
authToken: args.authToken,
|
|
196
|
+
spaceId: args.spaceId,
|
|
197
|
+
contentKey: args.dbKey,
|
|
198
|
+
title: args.title,
|
|
199
|
+
skillSummary: args.skillSummary,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function runCreateCommand(
|
|
205
|
+
args: string[],
|
|
206
|
+
deps: DocCreateDeps,
|
|
207
|
+
options: { defaultKind: DocKind; usage: (output: { write(chunk: string): unknown }) => void }
|
|
208
|
+
) {
|
|
209
|
+
if (hasHelpArg(args)) {
|
|
210
|
+
options.usage(process.stdout);
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const title = readTitle(args);
|
|
215
|
+
const description = readOption(args, "--description") ?? "";
|
|
216
|
+
const docKind = resolveDocKind(args, options.defaultKind);
|
|
217
|
+
const shouldDryRun = hasFlag(args, "--dry-run");
|
|
218
|
+
const shouldOutputJson = hasFlag(args, "--json");
|
|
219
|
+
const allowSecrets = hasFlag(args, "--allow-secrets");
|
|
220
|
+
const spaceId = readOption(args, "--space") ?? null;
|
|
221
|
+
|
|
222
|
+
if (!title || (docKind === "skill" && !description)) {
|
|
223
|
+
options.usage(process.stderr);
|
|
224
|
+
return 1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const body = readBodyArg(args, "");
|
|
228
|
+
const secretFindings = findPotentialSecrets(body);
|
|
229
|
+
if (secretFindings.length && !allowSecrets) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
[
|
|
232
|
+
"Document body appears to contain credentials or secrets. Pass --allow-secrets to write it anyway.",
|
|
233
|
+
formatSecretFindings(secretFindings),
|
|
234
|
+
].join("\n")
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const { authToken, userId } = requireTokenUser(args, deps.env, shouldDryRun);
|
|
239
|
+
const targets = resolveWriteTargets(args, deps.env);
|
|
240
|
+
if (!targets.length) {
|
|
241
|
+
throw new Error("No write targets resolved. Check --sync / --local-only / --no-local.");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const explicitId = readOption(args, "--id");
|
|
245
|
+
const docId = explicitId ?? (docKind === "skill" ? buildSkillDocId(title) : createPageId());
|
|
246
|
+
const dbKey = docKind === "skill" ? buildSkillPageKey(userId, docId) : buildPageKey(userId, docId);
|
|
247
|
+
const record =
|
|
248
|
+
docKind === "skill"
|
|
249
|
+
? buildSkillPageRecord({
|
|
250
|
+
dbKey,
|
|
251
|
+
skillId: docId,
|
|
252
|
+
title,
|
|
253
|
+
spaceId,
|
|
254
|
+
body,
|
|
255
|
+
skillConfig: {
|
|
256
|
+
version: "0.1",
|
|
257
|
+
kind: "skill",
|
|
258
|
+
id: docId,
|
|
259
|
+
name: title,
|
|
260
|
+
description,
|
|
261
|
+
...(parseJsonArg<string[]>(readOption(args, "--tools"), []).length
|
|
262
|
+
? { toolNames: parseJsonArg<string[]>(readOption(args, "--tools"), []) }
|
|
263
|
+
: {}),
|
|
264
|
+
...(parseJsonArg<string[]>(readOption(args, "--required-skills"), []).length
|
|
265
|
+
? { requiredSkills: parseJsonArg<string[]>(readOption(args, "--required-skills"), []) }
|
|
266
|
+
: {}),
|
|
267
|
+
...(parseJsonArg<string[]>(readOption(args, "--recommended-skills"), []).length
|
|
268
|
+
? { recommendedSkills: parseJsonArg<string[]>(readOption(args, "--recommended-skills"), []) }
|
|
269
|
+
: {}),
|
|
270
|
+
...(parseJsonArg<string[]>(readOption(args, "--preferred-agents"), []).length
|
|
271
|
+
? { preferredAgents: parseJsonArg<string[]>(readOption(args, "--preferred-agents"), []) }
|
|
272
|
+
: {}),
|
|
273
|
+
...(readOption(args, "--trigger-mode")
|
|
274
|
+
? { triggerMode: readOption(args, "--trigger-mode") as "explicit" | "required" | "recommended" }
|
|
275
|
+
: {}),
|
|
276
|
+
...(readOption(args, "--budget-tier")
|
|
277
|
+
? { budgetTier: readOption(args, "--budget-tier") as "low" | "medium" | "high" }
|
|
278
|
+
: {}),
|
|
279
|
+
...(readOption(args, "--prompt-patch") ? { promptPatch: readOption(args, "--prompt-patch") } : {}),
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
: buildPageRecord({
|
|
283
|
+
dbKey,
|
|
284
|
+
pageId: docId,
|
|
285
|
+
title,
|
|
286
|
+
spaceId,
|
|
287
|
+
content: body,
|
|
288
|
+
meta: description ? { description } : undefined,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const skillSummary = docKind === "skill" ? buildSkillSummaryMarker(record.meta) : undefined;
|
|
292
|
+
const summary = {
|
|
293
|
+
dryRun: shouldDryRun,
|
|
294
|
+
kind: docKind,
|
|
295
|
+
title,
|
|
296
|
+
dbKey,
|
|
297
|
+
id: docId,
|
|
298
|
+
userId,
|
|
299
|
+
spaceId,
|
|
300
|
+
syncTargets: targets,
|
|
301
|
+
writtenTargets: [] as string[],
|
|
302
|
+
secretFindingCount: secretFindings.length,
|
|
303
|
+
secretFindings,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
if (!shouldDryRun) {
|
|
307
|
+
for (const serverUrl of targets) {
|
|
308
|
+
await writePageRecord({
|
|
309
|
+
authToken,
|
|
310
|
+
dbKey,
|
|
311
|
+
record,
|
|
312
|
+
serverUrl,
|
|
313
|
+
spaceId,
|
|
314
|
+
title,
|
|
315
|
+
userId,
|
|
316
|
+
skillSummary,
|
|
317
|
+
});
|
|
318
|
+
summary.writtenTargets.push(serverUrl);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (shouldOutputJson) {
|
|
323
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
324
|
+
} else if (shouldDryRun) {
|
|
325
|
+
process.stdout.write(`dry-run ok: ${dbKey}\n`);
|
|
326
|
+
} else {
|
|
327
|
+
process.stdout.write(`created ${docKind}: ${dbKey}\n`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function runDocCreateCommand(args: string[], deps: DocCreateDeps) {
|
|
334
|
+
return runCreateCommand(args, deps, {
|
|
335
|
+
defaultKind: "page",
|
|
336
|
+
usage: printDocCreateUsage,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export async function runSkillDocCreateCommand(args: string[], deps: DocCreateDeps) {
|
|
341
|
+
return runCreateCommand(args, deps, {
|
|
342
|
+
defaultKind: "skill",
|
|
343
|
+
usage: printSkillDocCreateUsage,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CommandEntry } from "./cliCommandTypes";
|
|
2
|
+
import { createEnvCommand } from "./cliCommandFactories";
|
|
3
|
+
import { runDocCreateCommand, runSkillDocCreateCommand } from "./docCreateCommands";
|
|
4
|
+
|
|
5
|
+
export function getDocInternalCommandEntries(): CommandEntry[] {
|
|
6
|
+
return [
|
|
7
|
+
createEnvCommand(["doc", "create"], "Create a normal doc", runDocCreateCommand),
|
|
8
|
+
createEnvCommand(["skill-doc", "create"], "Create a skill-backed doc", runSkillDocCreateCommand),
|
|
9
|
+
createEnvCommand(["skill", "create-doc"], "Create a skill-backed doc", runSkillDocCreateCommand),
|
|
10
|
+
];
|
|
11
|
+
}
|