nolo-cli 0.1.41 → 0.1.43

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 (136) hide show
  1. package/README.md +27 -12
  2. package/agent-runtime/agentThread.ts +222 -0
  3. package/agent-runtime/agentThreadAdmission.ts +74 -0
  4. package/agent-runtime/dialogWritePlan.ts +42 -0
  5. package/agent-runtime/hostAdapter.ts +1 -0
  6. package/agent-runtime/index.ts +43 -0
  7. package/agent-runtime/localLoop.ts +31 -2
  8. package/agent-runtime/localToolPolicy.ts +1 -0
  9. package/agent-runtime/localWorkspaceTools.ts +1258 -78
  10. package/agent-runtime/noloWorkspaceTools.ts +20 -1
  11. package/agent-runtime/runtimeToolPolicy.ts +1 -5
  12. package/agent-runtime/runtimeToolSurface.ts +1 -0
  13. package/agentAliases.ts +29 -9
  14. package/agentInternalCommandEntries.ts +11 -3
  15. package/agentMachineCommands.ts +1 -0
  16. package/agentRecordCommands.ts +2 -2
  17. package/agentRecordHelpers.ts +59 -6
  18. package/agentRunCommand.ts +84 -19
  19. package/ai/agent/buildSystemPrompt.ts +14 -0
  20. package/ai/agent/cliExecutor.ts +202 -43
  21. package/ai/agent/createAgentSchema.ts +2 -2
  22. package/ai/agent/liveAudioModel.ts +1 -1
  23. package/ai/agent/streamAgentChatTurn.ts +154 -20
  24. package/ai/agent/streamAgentChatTurnUtils.ts +63 -0
  25. package/ai/ai.locale.ts +70 -0
  26. package/ai/chat/inlineImageUrlsForCustomProvider.ts +7 -2
  27. package/ai/chat/sendOpenAICompletionsRequest.ts +25 -1
  28. package/ai/llm/kimi.ts +0 -1
  29. package/ai/llm/openrouterModels.ts +27 -2
  30. package/ai/memory/policy.ts +49 -0
  31. package/ai/memory/queryShared.ts +10 -4
  32. package/ai/memory/recentRelationshipRecap.ts +4 -0
  33. package/ai/memory/remember.ts +48 -104
  34. package/ai/memory/scope.ts +74 -0
  35. package/ai/memory/types.ts +6 -0
  36. package/ai/memory/understanding.ts +10 -18
  37. package/ai/policy/runtimePolicy.ts +95 -23
  38. package/ai/tools/agent/agentTools.ts +0 -21
  39. package/ai/tools/agent/startAgentDialogTool.ts +2 -1
  40. package/ai/tools/index.ts +2 -20
  41. package/ai/tools/prepareTools.ts +171 -1
  42. package/ai/tools/readXhsProfileTool.ts +80 -69
  43. package/ai/tools/table/createTableTool.ts +11 -0
  44. package/ai/tools/toolPacks.ts +19 -0
  45. package/ai/types.ts +3 -0
  46. package/app/favorite/deletedFavoriteProjection.ts +31 -0
  47. package/app/hooks/deleteDbKey.ts +24 -3
  48. package/app/i18n/translations/interface.locale.ts +48 -16
  49. package/app/types/appSummary.ts +4 -0
  50. package/app/utils/desktopAgentRuntimeTurnClient.ts +2 -0
  51. package/auth/adminPermissions.ts +64 -4
  52. package/auth/routes.ts +5 -0
  53. package/chat/chat.locale.ts +4 -0
  54. package/chat/dialog/actions/handleSendMessageAction.ts +7 -2
  55. package/chat/dialog/dialogAttachmentCleanup.ts +135 -0
  56. package/chat/dialog/dialogSlice.ts +117 -4
  57. package/chat/messages/activityCompletion.ts +97 -0
  58. package/chat/messages/imagePayloadPersistence.ts +22 -0
  59. package/chat/messages/messageContent.ts +20 -16
  60. package/chat/messages/messageSlice.ts +20 -1
  61. package/chat/messages/toolThunks.ts +32 -11
  62. package/chat/messages/types.ts +8 -1
  63. package/chat/messages/web/toolDisplayName.ts +686 -0
  64. package/cli/agentAliases.ts +29 -9
  65. package/cli/agentInternalCommandEntries.ts +11 -3
  66. package/cli/agentMachineCommands.ts +1 -0
  67. package/cli/agentRecordCommands.ts +2 -2
  68. package/cli/agentRecordHelpers.ts +59 -6
  69. package/cli/agentRunCommand.ts +84 -19
  70. package/cli/client/agentRun.ts +65 -253
  71. package/cli/client/localRuntimeAdapter.ts +179 -53
  72. package/cli/commandRegistry.ts +7 -4
  73. package/cli/dialogAttachmentCleanup.ts +151 -0
  74. package/cli/dialogCommands.ts +163 -61
  75. package/cli/dialogInternalCommandEntries.ts +1 -1
  76. package/cli/docCreateCommands.ts +345 -0
  77. package/cli/docInternalCommandEntries.ts +11 -0
  78. package/cli/docPageHelpers.ts +80 -0
  79. package/cli/docSkillHelpers.ts +67 -0
  80. package/cli/docSpaceHelpers.ts +46 -0
  81. package/cli/internalCommandEntries.ts +4 -0
  82. package/cli/machineCommands.ts +18 -34
  83. package/cli/memoryCommands.ts +253 -0
  84. package/cli/memoryInternalCommandEntries.ts +9 -0
  85. package/cli/qoderUsageProbe.ts +252 -0
  86. package/cli/scriptCommandEntries.ts +0 -3
  87. package/cli/secretScan.ts +31 -0
  88. package/cli/tableCommands.ts +248 -13
  89. package/cli/workflowInternalCommandEntries.ts +2 -3
  90. package/client/agentRun.test.ts +68 -84
  91. package/client/agentRun.ts +65 -253
  92. package/client/localRuntimeAdapter.test.ts +505 -74
  93. package/client/localRuntimeAdapter.ts +179 -53
  94. package/commandRegistry.ts +7 -4
  95. package/connector-experimental/capabilities.ts +10 -9
  96. package/create/space/sidebarVisibleTypes.ts +19 -0
  97. package/dialogAttachmentCleanup.ts +151 -0
  98. package/dialogCommands.ts +163 -61
  99. package/dialogInternalCommandEntries.ts +1 -1
  100. package/docCreateCommands.ts +345 -0
  101. package/docInternalCommandEntries.ts +11 -0
  102. package/docPageHelpers.ts +80 -0
  103. package/docSkillHelpers.ts +67 -0
  104. package/docSpaceHelpers.ts +46 -0
  105. package/integrations/google/models.ts +4 -4
  106. package/integrations/openai/generateOpenAIRequestBody.ts +0 -3
  107. package/integrations/xhs-reader/types.ts +36 -3
  108. package/integrations/xhs-reader/url.ts +24 -1
  109. package/internalCommandEntries.ts +4 -0
  110. package/machineCommands.ts +18 -34
  111. package/memoryCommands.ts +253 -0
  112. package/memoryInternalCommandEntries.ts +9 -0
  113. package/package.json +141 -114
  114. package/qoderUsageProbe.ts +252 -0
  115. package/render/contentIcon/types.ts +167 -0
  116. package/render/page/docSlice.ts +30 -3
  117. package/render/page/types.ts +2 -0
  118. package/render/table/createTableAction.ts +4 -0
  119. package/render/table/tableSlice.ts +61 -0
  120. package/render/table/types.ts +10 -0
  121. package/scriptCommandEntries.ts +0 -3
  122. package/secretScan.ts +31 -0
  123. package/server/handlers/agentRun/types.ts +24 -23
  124. package/tableCommands.ts +248 -13
  125. package/workflowInternalCommandEntries.ts +2 -3
  126. package/ai/tools/agent/taskRunTool.ts +0 -112
  127. package/cli/client/taskRunPrompt.ts +0 -27
  128. package/cli/taskRunCommand.ts +0 -265
  129. package/client/taskRunPrompt.ts +0 -27
  130. package/integrations/xhs-reader/analyze/profileAnalyzer.ts +0 -205
  131. package/integrations/xhs-reader/backends/playwrightProfileCollector.ts +0 -624
  132. package/integrations/xhs-reader/bridge/readXhsProfileWithBridge.ts +0 -227
  133. package/integrations/xhs-reader/client/xhsWebApiClient.ts +0 -229
  134. package/integrations/xhs-reader/normalize.ts +0 -390
  135. package/integrations/xhs-reader/orchestrator.ts +0 -178
  136. package/taskRunCommand.ts +0 -265
@@ -0,0 +1,80 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { ulid } from "ulid";
3
+
4
+ function getCliArg(args: string[], flag: string): string | undefined {
5
+ const idx = args.indexOf(flag);
6
+ return idx !== -1 ? args[idx + 1] : undefined;
7
+ }
8
+
9
+ export function readBodyArg(args: string[], fallback = "") {
10
+ const bodyFile = getCliArg(args, "--body-file");
11
+ if (bodyFile !== undefined) {
12
+ return readFileSync(bodyFile, "utf8");
13
+ }
14
+ return getCliArg(args, "--body") ?? fallback;
15
+ }
16
+
17
+ function textToSlate(text: string) {
18
+ return text.split("\n").map((line) => ({
19
+ type: "paragraph",
20
+ children: [{ text: line }],
21
+ }));
22
+ }
23
+
24
+ export function createPageId() {
25
+ return ulid();
26
+ }
27
+
28
+ export function buildPageKey(userId: string, pageId: string) {
29
+ return `page-${userId}-${pageId}`;
30
+ }
31
+
32
+ export function buildPageRecord(args: {
33
+ dbKey: string;
34
+ pageId: string;
35
+ title: string;
36
+ spaceId: string | null;
37
+ content: string;
38
+ existing?: Record<string, any> | null;
39
+ meta?: Record<string, any> | null;
40
+ slateData?: Record<string, any>[] | null;
41
+ }) {
42
+ const { dbKey, pageId, title, spaceId, content, existing, meta, slateData } = args;
43
+ const now = Date.now();
44
+ const createdAt = typeof existing?.createdAt === "number" ? existing.createdAt : now;
45
+ const created =
46
+ typeof existing?.created === "string" ? existing.created : new Date(createdAt).toISOString();
47
+
48
+ const nextRecord: Record<string, any> = {
49
+ ...(existing ?? {}),
50
+ id: existing?.id ?? pageId,
51
+ dbKey,
52
+ type: "page",
53
+ title,
54
+ spaceId,
55
+ content,
56
+ updatedAt: now,
57
+ createdAt,
58
+ created,
59
+ };
60
+
61
+ if (meta !== undefined) {
62
+ if (meta === null) {
63
+ delete nextRecord.meta;
64
+ } else {
65
+ nextRecord.meta = meta;
66
+ }
67
+ }
68
+
69
+ if (slateData !== undefined) {
70
+ if (slateData === null) {
71
+ delete nextRecord.slateData;
72
+ } else {
73
+ nextRecord.slateData = slateData;
74
+ }
75
+ } else if (!existing?.slateData) {
76
+ nextRecord.slateData = textToSlate(content);
77
+ }
78
+
79
+ return nextRecord;
80
+ }
@@ -0,0 +1,67 @@
1
+ import {
2
+ buildSkillDocMarkdown,
3
+ type SkillDocConfig,
4
+ } from "../ai/skills/skillDocProtocol";
5
+ import { buildPageKey, buildPageRecord } from "./docPageHelpers";
6
+
7
+ function normalizeSkillSeed(input: string) {
8
+ return input
9
+ .trim()
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
12
+ .replace(/^-+|-+$/g, "");
13
+ }
14
+
15
+ function deterministicId(prefix: string, seed: string): string {
16
+ let h = 0x811c9dc5;
17
+ for (let i = 0; i < seed.length; i++) {
18
+ h ^= seed.charCodeAt(i);
19
+ h = (h * 0x01000193) >>> 0;
20
+ }
21
+ const suffix = h.toString(36).toUpperCase().padStart(14, "0");
22
+ return (prefix + suffix).slice(0, 26);
23
+ }
24
+
25
+ export function buildSkillDocId(input: string) {
26
+ return deterministicId("01SK", normalizeSkillSeed(input) || input);
27
+ }
28
+
29
+ export function buildSkillPageKey(userId: string, skillId: string) {
30
+ return buildPageKey(userId, skillId);
31
+ }
32
+
33
+ export function buildSkillPageRecord(args: {
34
+ dbKey: string;
35
+ skillId: string;
36
+ title: string;
37
+ spaceId: string | null;
38
+ body: string;
39
+ skillConfig: SkillDocConfig;
40
+ existing?: Record<string, any> | null;
41
+ }) {
42
+ const { dbKey, skillId, title, spaceId, body, skillConfig, existing } = args;
43
+ return {
44
+ ...buildPageRecord({
45
+ dbKey,
46
+ pageId: skillId,
47
+ title,
48
+ spaceId,
49
+ content: buildSkillDocMarkdown({
50
+ body,
51
+ skillConfig,
52
+ }),
53
+ existing,
54
+ meta: {
55
+ ...(existing?.meta ?? {}),
56
+ kind: "skill",
57
+ skillConfig,
58
+ },
59
+ slateData: null,
60
+ }),
61
+ };
62
+ }
63
+
64
+ export function parseJsonArg<T>(raw: string | undefined, fallback: T): T {
65
+ if (raw === undefined) return fallback;
66
+ return JSON.parse(raw) as T;
67
+ }
@@ -0,0 +1,46 @@
1
+ import { createSpaceKey } from "../create/space/spaceKeys";
2
+ import { readDbRecord, writeAgentRecord } from "./agentRecordHelpers";
3
+
4
+ export async function ensurePageAttachedToSpace(params: {
5
+ baseUrl: string;
6
+ userId: string;
7
+ authToken: string;
8
+ spaceId: string;
9
+ contentKey: string;
10
+ title: string;
11
+ skillSummary?: Record<string, any> | null;
12
+ }) {
13
+ const { baseUrl, userId, authToken, spaceId, contentKey, title, skillSummary } = params;
14
+ const spaceKey = createSpaceKey.space(spaceId);
15
+ const spaceRecord = await readDbRecord({
16
+ dbKey: spaceKey,
17
+ authToken,
18
+ fetchImpl: fetch,
19
+ serverUrl: baseUrl,
20
+ });
21
+ const now = Date.now();
22
+
23
+ await writeAgentRecord({
24
+ agentKey: spaceKey,
25
+ authToken,
26
+ fetchImpl: fetch,
27
+ serverUrl: baseUrl,
28
+ userId,
29
+ record: {
30
+ ...spaceRecord,
31
+ contents: {
32
+ ...(spaceRecord.contents ?? {}),
33
+ [contentKey]: {
34
+ ...(spaceRecord.contents?.[contentKey] ?? {}),
35
+ title,
36
+ type: "page",
37
+ contentKey,
38
+ ...(skillSummary !== undefined ? { skillSummary } : {}),
39
+ updatedAt: now,
40
+ createdAt: spaceRecord.contents?.[contentKey]?.createdAt ?? now,
41
+ },
42
+ },
43
+ updatedAt: now,
44
+ },
45
+ });
46
+ }
@@ -1,6 +1,8 @@
1
1
  import { getAgentInternalCommandEntries } from "./agentInternalCommandEntries";
2
2
  import { getDialogInternalCommandEntries } from "./dialogInternalCommandEntries";
3
+ import { getDocInternalCommandEntries } from "./docInternalCommandEntries";
3
4
  import type { CommandEntry } from "./cliCommandTypes";
5
+ import { getMemoryInternalCommandEntries } from "./memoryInternalCommandEntries";
4
6
  import { getSystemInternalCommandEntries } from "./systemInternalCommandEntries";
5
7
  import { getWorkflowInternalCommandEntries } from "./workflowInternalCommandEntries";
6
8
 
@@ -8,6 +10,8 @@ export function getInternalCommandEntries(renderHelpText: () => string): Command
8
10
  return [
9
11
  ...getAgentInternalCommandEntries(),
10
12
  ...getDialogInternalCommandEntries(),
13
+ ...getDocInternalCommandEntries(),
14
+ ...getMemoryInternalCommandEntries(),
11
15
  ...getWorkflowInternalCommandEntries(),
12
16
  ...getSystemInternalCommandEntries(renderHelpText),
13
17
  ];
@@ -132,50 +132,34 @@ function detectLaunchableMachineInfo() {
132
132
  return detectMachineInfo({ probeLaunchable: true });
133
133
  }
134
134
 
135
- function inferTaskRunRole(agentKey: string, agentConfig: any) {
136
- const haystack = [
137
- agentKey,
138
- agentConfig?.name,
139
- agentConfig?.prompt,
140
- ].filter(Boolean).join("\n").toLowerCase();
141
- if (haystack.includes("frontend") || haystack.includes("前端")) return "frontend";
142
- if (haystack.includes("fullstack") || haystack.includes("全栈")) return "fullstack";
143
- if (haystack.includes("review") || haystack.includes("审查")) return "reviewer";
144
- if (haystack.includes("project-manager") || haystack.includes("项目经理")) return "pm";
145
- return "codex";
135
+ function findTaskRowSubjectRef(runtimeContext: any) {
136
+ const subjectRefs = Array.isArray(runtimeContext?.subjectRefs) ? runtimeContext.subjectRefs : [];
137
+ for (const ref of subjectRefs) {
138
+ if (!ref || typeof ref !== "object" || Array.isArray(ref)) continue;
139
+ if (ref.kind !== "table-row") continue;
140
+ const id = typeof ref.id === "string" ? ref.id.trim() : "";
141
+ if (id) return id;
142
+ }
143
+ return "";
146
144
  }
147
145
 
148
- function buildTaskRunBridgePrompt(args: {
146
+ function buildTaskEvidencePrompt(args: {
149
147
  agentKey: string;
150
148
  agentConfig: any;
151
149
  runtimeContext: any;
152
150
  }) {
153
- const taskRun = args.runtimeContext?.taskRun;
154
- const rowDbKey = typeof taskRun?.rowDbKey === "string" ? taskRun.rowDbKey.trim() : "";
151
+ const rowDbKey = findTaskRowSubjectRef(args.runtimeContext);
155
152
  if (!rowDbKey) return "";
156
- const workItemId = typeof taskRun?.workItemId === "string" ? taskRun.workItemId.trim() : "";
157
- const role = inferTaskRunRole(args.agentKey, args.agentConfig);
158
153
  return [
159
- "--- Nolo task-run tool bridge ---",
154
+ "--- Nolo task evidence context ---",
160
155
  "This CLI runtime does not receive server-side function tools directly.",
161
- "Use the local task-run bridge command for durable task state writes. Do not edit meta.taskRun with table or database commands.",
156
+ "Use the task row subjectRef and linked dialog/activity evidence as the durable task context.",
162
157
  "Run commands from the repository root. The runner already provides server URL and auth in the environment.",
163
158
  "Release boundary: after review passes, AI/reviewer/Codex may advance alpha for verification. Do not merge, push, or release main/release unless the human owner explicitly authorizes it in the current task context.",
164
- "Cost boundary: read taskRun.control from context. Prefer low-cost/free-window models for routine work, and reserve Codex for supervision, quality gates, complex unblocking, and main/release decisions.",
165
- "Handoff context: start from read-context, then inspect existing refs such as workItems.dialogIds, workItems.outcome, review.dialogId, activityRefs, agentRuns.dialogId/resultSummary, and artifactRefs. Use dialog read/search for exact evidence; do not infer completion from handoff text alone.",
166
- `Read context: bun packages/cli/index.ts task-run read-context --row-dbkey ${JSON.stringify(rowDbKey)}`,
167
- ...(workItemId
168
- ? [
169
- `Claim work item: bun packages/cli/index.ts task-run claim-work-item --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --agent-key ${JSON.stringify(args.agentKey)} --role ${role}`,
170
- `Set blocker: bun packages/cli/index.ts task-run set-blocker --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --layer tool --code <CODE> --message <MESSAGE>`,
171
- `Clear resolved blocker: bun packages/cli/index.ts task-run clear-blocker --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --blocker-id <BLOCKER_ID>`,
172
- `Submit outcome: bun packages/cli/index.ts task-run submit-outcome --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --summary <SUMMARY>`,
173
- `Request review: bun packages/cli/index.ts task-run request-review --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --reviewer-agent-key <AGENT_KEY> --message <MESSAGE>`,
174
- `Record review: bun packages/cli/index.ts task-run record-review --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --review-status <passed|needs_changes|blocked> --reviewer-agent-key ${JSON.stringify(args.agentKey)} --findings <FINDINGS>`,
175
- `Close work item: bun packages/cli/index.ts task-run close-work-item --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --summary <SUMMARY>`,
176
- ]
177
- : []),
178
- "If a bridge command fails, report the exact command and error.",
159
+ "Handoff context: inspect activityRefs/latestActivityRef, dialog checkpoints, artifacts, commits, and test evidence. Use dialog read/search for exact evidence; do not infer completion from handoff text alone.",
160
+ `Read task row: bun packages/cli/index.ts table query --table meta-0e95801d90-NOLOTASKBOARD --row ${JSON.stringify(rowDbKey)} --include-activity --output json`,
161
+ "Then read linked activity dialog ids with: bun packages/cli/index.ts dialog read <dialogId>",
162
+ "Report progress, blockers, worktree, branch, commit/diff, tests, and unverified items in the dialog.",
179
163
  ].join("\n");
180
164
  }
181
165
 
@@ -186,7 +170,7 @@ function buildConnectorCliPrompt(agentConfig: any, userInput: string, bridgeArgs
186
170
  const policy = permissionPolicy ?? resolveMachineRunPermissionPolicy(agentConfig);
187
171
  return [
188
172
  typeof agentConfig?.prompt === "string" ? agentConfig.prompt.trim() : "",
189
- bridgeArgs ? buildTaskRunBridgePrompt({
173
+ bridgeArgs ? buildTaskEvidencePrompt({
190
174
  agentKey: bridgeArgs.agentKey,
191
175
  agentConfig,
192
176
  runtimeContext: bridgeArgs.runtimeContext,
@@ -0,0 +1,253 @@
1
+ import type { AgentCommandDeps } from "./agentCommandSupport";
2
+ import {
3
+ readOption,
4
+ resolveAuthToken,
5
+ resolveServerCandidates,
6
+ resolveServerUrl,
7
+ } from "./cliEnvHelpers";
8
+
9
+ const VALUE_FLAGS = new Set([
10
+ "--facet",
11
+ "--id",
12
+ "--ids",
13
+ "--kind",
14
+ "--limit",
15
+ "--pattern-prefix",
16
+ "--server",
17
+ "--server-url",
18
+ "--source-dialog",
19
+ "--subject",
20
+ "--subject-id",
21
+ "--subject-type",
22
+ "--tag",
23
+ "--token",
24
+ ]);
25
+
26
+ function hasFlag(args: string[], flag: string) {
27
+ return args.includes(flag);
28
+ }
29
+
30
+ function printMemoryDeleteUsage(output: { write(chunk: string): unknown }) {
31
+ output.write(`Usage:
32
+ nolo memory delete --source-dialog <dialogId> --yes [--json]
33
+ nolo memory delete --tag <tag> --kind <kind> --yes
34
+ nolo memory delete --pattern-prefix <prefix> --limit <n> --yes
35
+
36
+ Options:
37
+ --id <memoryId> Delete a specific memory id. Repeatable.
38
+ --ids <a,b,c> Delete specific memory ids.
39
+ --source-dialog <dialogId> Delete memories created from one dialog.
40
+ --tag <tag> Require a tag. Repeatable.
41
+ --kind <kind> Filter episodic / semantic / procedural. Repeatable.
42
+ --facet <facet> Filter preference / tension / unfinished / goal / style. Repeatable.
43
+ --subject-type <type> Filter user / agent / space / project / system.
44
+ --subject <subjectId> Filter subjectId.
45
+ --pattern-prefix <prefix> Filter patternKey prefix.
46
+ --limit <n> Maximum memories to delete.
47
+ --yes Actually delete. Without this, command is a dry-run.
48
+ --json Print machine-readable JSON.
49
+ --server <url> Prefer this server and include known Nolo peers.
50
+ --token <jwt> Override AUTH_TOKEN.
51
+
52
+ At least one filter is required; this command will not delete all memory implicitly.
53
+ `);
54
+ }
55
+
56
+ function readRepeated(args: string[], flag: string) {
57
+ const values: string[] = [];
58
+ for (let index = 0; index < args.length; index += 1) {
59
+ if (args[index] !== flag) continue;
60
+ const value = args[index + 1];
61
+ if (typeof value === "string" && value.trim()) values.push(value.trim());
62
+ }
63
+ return values;
64
+ }
65
+
66
+ function readCsvValues(args: string[], flag: string) {
67
+ const raw = readRepeated(args, flag).join(",");
68
+ return raw
69
+ .split(",")
70
+ .map((value) => value.trim())
71
+ .filter(Boolean);
72
+ }
73
+
74
+ function readLimit(args: string[]) {
75
+ const raw = readOption(args, "--limit");
76
+ if (!raw) return undefined;
77
+ if (!/^\d+$/.test(raw)) throw new Error(`invalid --limit: ${raw}`);
78
+ const parsed = Number(raw);
79
+ return parsed > 0 ? parsed : undefined;
80
+ }
81
+
82
+ function assertNoUnknownFlags(args: string[]) {
83
+ for (let index = 0; index < args.length; index += 1) {
84
+ const value = args[index];
85
+ if (!value.startsWith("-")) continue;
86
+ if (value === "--help" || value === "-h" || value === "--yes" || value === "--json") continue;
87
+ if (VALUE_FLAGS.has(value)) {
88
+ index += 1;
89
+ continue;
90
+ }
91
+ throw new Error(`unknown option: ${value}`);
92
+ }
93
+ }
94
+
95
+ function buildDeleteBody(args: string[]) {
96
+ const ids = [...readRepeated(args, "--id"), ...readCsvValues(args, "--ids")];
97
+ const kinds = readRepeated(args, "--kind");
98
+ const facets = readRepeated(args, "--facet");
99
+ const tags = readRepeated(args, "--tag");
100
+ const sourceDialogId = readOption(args, "--source-dialog")?.trim();
101
+ const subjectType = readOption(args, "--subject-type")?.trim();
102
+ const subjectId = (readOption(args, "--subject") ?? readOption(args, "--subject-id"))?.trim();
103
+ const patternKeyPrefix = readOption(args, "--pattern-prefix")?.trim();
104
+ const limit = readLimit(args);
105
+
106
+ const body: Record<string, unknown> = {};
107
+ if (ids.length) body.ids = [...new Set(ids)];
108
+ if (kinds.length) body.kinds = [...new Set(kinds)];
109
+ if (facets.length) body.facets = [...new Set(facets)];
110
+ if (tags.length) body.tags = [...new Set(tags)];
111
+ if (sourceDialogId) body.sourceDialogId = sourceDialogId;
112
+ if (subjectType) body.subjectType = subjectType;
113
+ if (subjectId) body.subjectId = subjectId;
114
+ if (patternKeyPrefix) body.patternKeyPrefix = patternKeyPrefix;
115
+ if (typeof limit === "number") body.limit = limit;
116
+ return body;
117
+ }
118
+
119
+ function hasDeleteFilter(body: Record<string, unknown>) {
120
+ return Object.keys(body).some((key) => key !== "limit");
121
+ }
122
+
123
+ async function postMemoryDelete(args: {
124
+ authToken: string;
125
+ body: Record<string, unknown>;
126
+ fetchImpl: typeof fetch;
127
+ serverUrl: string;
128
+ }) {
129
+ const res = await args.fetchImpl(`${args.serverUrl}/api/memory/delete`, {
130
+ method: "POST",
131
+ headers: {
132
+ Authorization: `Bearer ${args.authToken}`,
133
+ "Content-Type": "application/json",
134
+ },
135
+ body: JSON.stringify(args.body),
136
+ });
137
+ const text = await res.text();
138
+ let payload: any = null;
139
+ try {
140
+ payload = text ? JSON.parse(text) : null;
141
+ } catch {
142
+ payload = { raw: text };
143
+ }
144
+ if (!res.ok || payload?.error) {
145
+ const message =
146
+ payload?.error?.message ??
147
+ payload?.message ??
148
+ payload?.error ??
149
+ `HTTP ${res.status}`;
150
+ throw new Error(`${message}`);
151
+ }
152
+ return payload;
153
+ }
154
+
155
+ export async function runMemoryDeleteCommand(
156
+ args: string[],
157
+ deps: AgentCommandDeps = {}
158
+ ) {
159
+ const env = deps.env ?? process.env;
160
+ const output = deps.output ?? process.stdout;
161
+ if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
162
+ printMemoryDeleteUsage(output);
163
+ return 0;
164
+ }
165
+
166
+ try {
167
+ assertNoUnknownFlags(args);
168
+ const authToken = resolveAuthToken(args, env);
169
+ if (!authToken) {
170
+ output.write("[nolo] memory delete requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
171
+ return 1;
172
+ }
173
+
174
+ const body = buildDeleteBody(args);
175
+ if (!hasDeleteFilter(body)) {
176
+ output.write("[nolo] memory delete requires at least one filter; use --help for examples.\n");
177
+ return 1;
178
+ }
179
+
180
+ const shouldDelete = hasFlag(args, "--yes");
181
+ const wantJson = hasFlag(args, "--json");
182
+ const fetchImpl = deps.fetchImpl ?? fetch;
183
+ const serverUrl = resolveServerUrl(args, env);
184
+ const serverUrls = resolveServerCandidates(args, env, serverUrl);
185
+
186
+ if (!shouldDelete) {
187
+ const payload = {
188
+ dryRun: true,
189
+ deleted: false,
190
+ targetServers: serverUrls,
191
+ request: body,
192
+ nextStep: "rerun with --yes to delete matching memories",
193
+ };
194
+ if (wantJson) {
195
+ output.write(`${JSON.stringify(payload, null, 2)}\n`);
196
+ } else {
197
+ output.write("dryRun: true\n");
198
+ output.write(`targetServers: ${serverUrls.join(", ")}\n`);
199
+ output.write(`request: ${JSON.stringify(body)}\n`);
200
+ output.write("rerun with --yes to delete matching memories\n");
201
+ }
202
+ return 0;
203
+ }
204
+
205
+ const deleteResults = [];
206
+ for (const target of serverUrls) {
207
+ try {
208
+ const result = await postMemoryDelete({
209
+ authToken,
210
+ body,
211
+ fetchImpl,
212
+ serverUrl: target,
213
+ });
214
+ deleteResults.push({ serverUrl: target, ok: true, result });
215
+ } catch (error) {
216
+ deleteResults.push({
217
+ serverUrl: target,
218
+ ok: false,
219
+ error: error instanceof Error ? error.message : String(error),
220
+ });
221
+ }
222
+ }
223
+
224
+ const deletedCount = deleteResults.reduce(
225
+ (sum, item) => sum + (item.ok ? Number(item.result?.deletedCount ?? 0) : 0),
226
+ 0
227
+ );
228
+ const payload = {
229
+ dryRun: false,
230
+ deleted: true,
231
+ deletedCount,
232
+ request: body,
233
+ targetServers: serverUrls,
234
+ deleteResults,
235
+ };
236
+ if (wantJson) {
237
+ output.write(`${JSON.stringify(payload, null, 2)}\n`);
238
+ } else {
239
+ output.write(`deletedCount: ${deletedCount}\n`);
240
+ for (const result of deleteResults) {
241
+ output.write(
242
+ result.ok
243
+ ? `${result.serverUrl}: ok deleted=${result.result?.deletedCount ?? 0}\n`
244
+ : `${result.serverUrl}: failed ${result.error}\n`
245
+ );
246
+ }
247
+ }
248
+ return deleteResults.some((result) => result.ok) ? 0 : 1;
249
+ } catch (error) {
250
+ output.write(`[nolo] memory delete failed: ${error instanceof Error ? error.message : String(error)}\n`);
251
+ return 1;
252
+ }
253
+ }
@@ -0,0 +1,9 @@
1
+ import type { CommandEntry } from "./cliCommandTypes";
2
+ import { createEnvCommand } from "./cliCommandFactories";
3
+ import { runMemoryDeleteCommand } from "./memoryCommands";
4
+
5
+ export function getMemoryInternalCommandEntries(): CommandEntry[] {
6
+ return [
7
+ createEnvCommand(["memory", "delete"], "Delete long-term memories by filter", runMemoryDeleteCommand),
8
+ ];
9
+ }