@robota-sdk/agent-command 3.0.0-beta.100
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/CHANGELOG.md +1351 -0
- package/LICENSE +661 -0
- package/README.md +100 -0
- package/dist/node/index.cjs +121 -0
- package/dist/node/index.d.cts +1053 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1053 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +122 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +83 -0
- package/src/advisor/advisor-command-module.ts +47 -0
- package/src/advisor/advisor-command.test.ts +102 -0
- package/src/advisor/advisor-command.ts +55 -0
- package/src/advisor/advisor-cost-functional.test.ts +92 -0
- package/src/advisor/index.ts +6 -0
- package/src/agent/__tests__/agent-command.test.ts +597 -0
- package/src/agent/agent-command-module.ts +89 -0
- package/src/agent/agent-command-parser.ts +180 -0
- package/src/agent/agent-command.ts +255 -0
- package/src/agent/index.ts +7 -0
- package/src/background/__tests__/background-command-module.test.ts +261 -0
- package/src/background/background-command-module.ts +52 -0
- package/src/background/background-command.ts +65 -0
- package/src/background/index.ts +6 -0
- package/src/command-module-utils.test.ts +37 -0
- package/src/command-module-utils.ts +35 -0
- package/src/compact/__tests__/compact-command-module.test.ts +181 -0
- package/src/compact/compact-command-module.ts +59 -0
- package/src/compact/compact-command.ts +33 -0
- package/src/compact/index.ts +6 -0
- package/src/context/__tests__/context-command-module.test.ts +502 -0
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +82 -0
- package/src/context/context-command.ts +291 -0
- package/src/context/index.ts +6 -0
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +121 -0
- package/src/default/__tests__/default-command-modules.test.ts +192 -0
- package/src/default/__tests__/model-exposure.test.ts +269 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +106 -0
- package/src/default/__tests__/provider-environment-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +247 -0
- package/src/default/index.ts +5 -0
- package/src/devices/__tests__/devices-command-module.test.ts +299 -0
- package/src/devices/devices-command-module.ts +308 -0
- package/src/devices/devices-command-port.ts +143 -0
- package/src/devices/index.ts +14 -0
- package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
- package/src/doctor/__tests__/doctor-fixture.ts +146 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +484 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +323 -0
- package/src/doctor/doctor-node-deps.ts +88 -0
- package/src/doctor/doctor-redaction.ts +99 -0
- package/src/doctor/doctor-render.ts +70 -0
- package/src/doctor/doctor-repair.ts +136 -0
- package/src/doctor/doctor-runner.ts +122 -0
- package/src/doctor/doctor-settings-probe.ts +239 -0
- package/src/doctor/doctor-storage-probe.ts +187 -0
- package/src/doctor/doctor-types.ts +109 -0
- package/src/doctor/index.ts +32 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +124 -0
- package/src/editor/editor-command-module.ts +53 -0
- package/src/editor/editor-command.ts +58 -0
- package/src/editor/index.ts +7 -0
- package/src/editor/resolve-editor.ts +21 -0
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +203 -0
- package/src/effort/effort-command.ts +136 -0
- package/src/effort/index.ts +6 -0
- package/src/events/__tests__/events-command.test.ts +73 -0
- package/src/events/events-command-module.ts +49 -0
- package/src/events/events-command.ts +69 -0
- package/src/events/index.ts +2 -0
- package/src/exit/__tests__/exit-command-module.test.ts +61 -0
- package/src/exit/exit-command-module.ts +47 -0
- package/src/exit/exit-command.ts +26 -0
- package/src/exit/index.ts +6 -0
- package/src/fork/__tests__/fork-command.test.ts +184 -0
- package/src/fork/fork-command-module.ts +61 -0
- package/src/fork/fork-command.ts +162 -0
- package/src/fork/index.ts +8 -0
- package/src/git/__tests__/fake-git-port.ts +35 -0
- package/src/git/__tests__/git-command-module.test.ts +111 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/git-command-module.ts +138 -0
- package/src/git/git-commit.ts +254 -0
- package/src/git/index.ts +20 -0
- package/src/goal/__tests__/goal-command.test.ts +80 -0
- package/src/goal/goal-command-module.ts +49 -0
- package/src/goal/goal-command.ts +69 -0
- package/src/goal/index.ts +6 -0
- package/src/handoff/__tests__/handoff-command-module.test.ts +25 -0
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +49 -0
- package/src/handoff/handoff-command.ts +187 -0
- package/src/handoff/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +134 -0
- package/src/help/__tests__/help-command.test.ts +130 -0
- package/src/help/help-command-module.ts +47 -0
- package/src/help/help-command.ts +11 -0
- package/src/help/index.ts +6 -0
- package/src/index.ts +163 -0
- package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
- package/src/keybindings/index.ts +6 -0
- package/src/keybindings/keybindings-command-module.ts +88 -0
- package/src/language/__tests__/language-command-module.test.ts +126 -0
- package/src/language/index.ts +6 -0
- package/src/language/language-command-module.ts +55 -0
- package/src/language/language-command.ts +52 -0
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +895 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +242 -0
- package/src/mcp-activation/__tests__/mcp-skill-command.test.ts +141 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +123 -0
- package/src/mcp-activation/mcp-activation-command.ts +662 -0
- package/src/mcp-activation/mcp-model-notice.ts +99 -0
- package/src/mcp-activation/mcp-model-status.ts +104 -0
- package/src/memory/__tests__/memory-command-module.test.ts +476 -0
- package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
- package/src/memory/index.ts +6 -0
- package/src/memory/memory-command-module.ts +77 -0
- package/src/memory/memory-command.ts +326 -0
- package/src/mode/__tests__/mode-command-module.test.ts +230 -0
- package/src/mode/index.ts +6 -0
- package/src/mode/mode-command-module.ts +55 -0
- package/src/mode/mode-command.ts +67 -0
- package/src/model/__tests__/model-command-module.test.ts +80 -0
- package/src/model/__tests__/model-command.test.ts +265 -0
- package/src/model/index.ts +6 -0
- package/src/model/model-command-module.ts +56 -0
- package/src/model/model-command.ts +249 -0
- package/src/output-style/__tests__/output-style-command.test.ts +96 -0
- package/src/output-style/index.ts +6 -0
- package/src/output-style/output-style-command-module.ts +143 -0
- package/src/peers/__tests__/peers-command-module.test.ts +24 -0
- package/src/peers/__tests__/peers-command.test.ts +399 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +49 -0
- package/src/peers/peers-command.ts +220 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +276 -0
- package/src/permissions/index.ts +6 -0
- package/src/permissions/permissions-command-module.ts +55 -0
- package/src/permissions/permissions-command.ts +89 -0
- package/src/plan/__tests__/plan-command.test.ts +129 -0
- package/src/plan/index.ts +9 -0
- package/src/plan/plan-command-module.ts +49 -0
- package/src/plan/plan-command.ts +92 -0
- package/src/plugin/__tests__/plugin-active-turn.test.ts +100 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +300 -0
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/index.ts +7 -0
- package/src/plugin/plugin-command-module.ts +85 -0
- package/src/plugin/plugin-command.ts +271 -0
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +220 -0
- package/src/preset/index.ts +6 -0
- package/src/preset/preset-command-module.ts +80 -0
- package/src/preset/preset-command.ts +148 -0
- package/src/provider/__tests__/optional-provider-credentials.test.ts +130 -0
- package/src/provider/__tests__/org-policy.test.ts +174 -0
- package/src/provider/__tests__/provider-command-module.test.ts +884 -0
- package/src/provider/__tests__/provider-connection-setup.test.ts +567 -0
- package/src/provider/__tests__/provider-reference-switch.test.ts +113 -0
- package/src/provider/__tests__/provider-setup-flow.test.ts +244 -0
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +40 -0
- package/src/provider/index.ts +32 -0
- package/src/provider/provider-command-execution.ts +235 -0
- package/src/provider/provider-command-module.ts +82 -0
- package/src/provider/provider-command-profile-lifecycle.ts +239 -0
- package/src/provider/provider-command-profile-operations.ts +218 -0
- package/src/provider/provider-command-profile.ts +116 -0
- package/src/provider/provider-command-setup.ts +79 -0
- package/src/provider/provider-connection-setup.ts +287 -0
- package/src/provider/provider-connection-validation.ts +38 -0
- package/src/provider/provider-onboarding.ts +73 -0
- package/src/provider/provider-prompt-interaction.ts +28 -0
- package/src/provider/provider-setup-ask.ts +67 -0
- package/src/provider/provider-setup-flow.ts +326 -0
- package/src/provider/provider-startup.ts +225 -0
- package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
- package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
- package/src/remote-control/index.ts +8 -0
- package/src/remote-control/remote-control-command-module.ts +61 -0
- package/src/remote-control/remote-control-command.ts +138 -0
- package/src/reset/__tests__/reset-command-module.test.ts +63 -0
- package/src/reset/index.ts +2 -0
- package/src/reset/reset-command-module.ts +47 -0
- package/src/reset/reset-command.ts +10 -0
- package/src/rewind/__tests__/rewind-command-module.test.ts +287 -0
- package/src/rewind/index.ts +2 -0
- package/src/rewind/rewind-command-module.ts +56 -0
- package/src/rewind/rewind-command.ts +279 -0
- package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/sandbox-command-module.ts +56 -0
- package/src/sandbox/sandbox-command.ts +94 -0
- package/src/schedule/__tests__/loop-command-functional.test.ts +399 -0
- package/src/schedule/__tests__/loop-command.test.ts +594 -0
- package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
- package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
- package/src/schedule/__tests__/schedule-command.test.ts +247 -0
- package/src/schedule/__tests__/schedule-redos.test.ts +114 -0
- package/src/schedule/index.ts +12 -0
- package/src/schedule/loop-command.ts +427 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +184 -0
- package/src/schedule/schedule-command.ts +161 -0
- package/src/schedule/schedule-spec-parser.ts +70 -0
- package/src/session/__tests__/model-pricing.test.ts +66 -0
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +589 -0
- package/src/session/index.ts +17 -0
- package/src/session/model-pricing.ts +26 -0
- package/src/session/session-command-module.ts +197 -0
- package/src/session/session-command.ts +272 -0
- package/src/settings/index.ts +7 -0
- package/src/settings/settings-command-module.ts +47 -0
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +118 -0
- package/src/shell/index.ts +8 -0
- package/src/shell/resolve-shell.ts +25 -0
- package/src/shell/shell-command-module.ts +53 -0
- package/src/shell/shell-command.ts +48 -0
- package/src/shell/spawn-inherited.ts +32 -0
- package/src/skills/__tests__/skill-read-command.test.ts +64 -0
- package/src/skills/__tests__/skills-command-module.test.ts +200 -0
- package/src/skills/index.ts +7 -0
- package/src/skills/skill-read-command.ts +60 -0
- package/src/skills/skills-command-module.ts +76 -0
- package/src/skills/skills-command.ts +108 -0
- package/src/statusline/__tests__/statusline-command-module.test.ts +96 -0
- package/src/statusline/index.ts +6 -0
- package/src/statusline/statusline-command-module.ts +55 -0
- package/src/statusline/statusline-command.ts +80 -0
- package/src/terminal-client/__tests__/terminal-client-commands.test.ts +216 -0
- package/src/terminal-client/index.ts +5 -0
- package/src/terminal-client/terminal-client-commands.ts +109 -0
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +221 -0
- package/src/user-local/__tests__/user-local-command.test.ts +180 -0
- package/src/user-local/index.ts +13 -0
- package/src/user-local/user-local-command-constants.ts +5 -0
- package/src/user-local/user-local-command-module.ts +65 -0
- package/src/user-local/user-local-command.ts +214 -0
- package/src/user-local/user-local-memory-command.ts +160 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3282 §4c review finding: `/memory` in a served (GUI) session throws instead of answering, because
|
|
3
|
+
* `executeMemoryCommand` never caught what `context.getMemoryStore()` throws when the host composed no
|
|
4
|
+
* memory store (off by default, or unavailable on this host — `WorkspaceAuthorityRequiredError`). The
|
|
5
|
+
* uncaught rejection reaches the transport's `command` handler
|
|
6
|
+
* (`session-message-handler.ts`'s `.then(_, error => deliver({ type: 'protocol_error', ... }))`), which
|
|
7
|
+
* the GUI renders as a `protocol_error` toast — the "red toast" the audit found — instead of the same
|
|
8
|
+
* plain `command_result` every other unavailable-command case gets (mirrors `rewind-command.ts`'s
|
|
9
|
+
* `formatError`, which already catches `EditCheckpointsUnavailableError` the same way).
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
|
|
13
|
+
import { WorkspaceAuthorityRequiredError } from '@robota-sdk/agent-framework';
|
|
14
|
+
|
|
15
|
+
import { executeMemoryCommand } from '../memory-command.js';
|
|
16
|
+
|
|
17
|
+
import type { ICommandHostMemory, ICommandHostWorkspace } from '@robota-sdk/agent-framework';
|
|
18
|
+
|
|
19
|
+
function contextWithUnavailableMemory(): ICommandHostMemory & ICommandHostWorkspace {
|
|
20
|
+
return {
|
|
21
|
+
getUsedMemoryReferences: () => [],
|
|
22
|
+
recordMemoryEvent: () => {},
|
|
23
|
+
getMemoryStore: () => {
|
|
24
|
+
throw new WorkspaceAuthorityRequiredError("Project memory isn't available for this folder.");
|
|
25
|
+
},
|
|
26
|
+
getCwd: () => '/workspace',
|
|
27
|
+
getCommandInvocationSource: () => 'user',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('executeMemoryCommand — unavailable memory store', () => {
|
|
32
|
+
it('answers list with a plain failure instead of rejecting', async () => {
|
|
33
|
+
const result = await executeMemoryCommand(contextWithUnavailableMemory(), 'list');
|
|
34
|
+
expect(result.success).toBe(false);
|
|
35
|
+
expect(result.message).toBe("Project memory isn't available for this folder.");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('answers show, pending, used and add the same way — never a throw', async () => {
|
|
39
|
+
const context = contextWithUnavailableMemory();
|
|
40
|
+
for (const args of ['show', 'pending', 'used', 'add project topic some durable fact']) {
|
|
41
|
+
await expect(executeMemoryCommand(context, args)).resolves.toMatchObject({
|
|
42
|
+
success: false,
|
|
43
|
+
message: "Project memory isn't available for this folder.",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('still lets an ordinary Error through as a plain failure, not a throw', async () => {
|
|
49
|
+
const context: ICommandHostMemory & ICommandHostWorkspace = {
|
|
50
|
+
getUsedMemoryReferences: () => [],
|
|
51
|
+
recordMemoryEvent: () => {},
|
|
52
|
+
getMemoryStore: () => {
|
|
53
|
+
throw new Error('unexpected store failure');
|
|
54
|
+
},
|
|
55
|
+
getCwd: () => '/workspace',
|
|
56
|
+
getCommandInvocationSource: () => 'user',
|
|
57
|
+
};
|
|
58
|
+
await expect(executeMemoryCommand(context, 'list')).resolves.toEqual({
|
|
59
|
+
success: false,
|
|
60
|
+
message: 'unexpected store failure',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MEMORY_COMMAND_ARGUMENT_HINT,
|
|
3
|
+
MEMORY_COMMAND_DESCRIPTION,
|
|
4
|
+
buildMemoryCommandSubcommands,
|
|
5
|
+
} from '@robota-sdk/agent-framework';
|
|
6
|
+
|
|
7
|
+
import { executeMemoryCommand } from './memory-command.js';
|
|
8
|
+
|
|
9
|
+
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
10
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Model-invocable: remembering and recalling project conventions is the model's own work. The
|
|
14
|
+
* subcommand flags (in `buildMemoryCommandSubcommands`) keep `approve`/`reject` user-only, because
|
|
15
|
+
* pending candidates exist so the user reviews what the model proposed to remember, and the command
|
|
16
|
+
* refuses the model's `add` in plan mode, which writes nothing.
|
|
17
|
+
*/
|
|
18
|
+
const MEMORY_COMMAND_MODEL_DESCRIPTION =
|
|
19
|
+
'Read and write this project’s durable memory. Use `list` or `show [topic]` to look up stored ' +
|
|
20
|
+
'conventions, preferences or references before asking the user again; `add <type> <topic> <text>` ' +
|
|
21
|
+
'to save a durable preference, project convention, feedback item or reference worth reusing ' +
|
|
22
|
+
'across sessions (never secrets, credentials or transient facts); `pending` to see candidates ' +
|
|
23
|
+
'awaiting the user’s review; `used` to report which memory items informed this turn. Bare lists ' +
|
|
24
|
+
'topics. In plan mode `add` is refused: save after plan mode ends. Returns the requested topics ' +
|
|
25
|
+
'or entries, or a confirmation of what was saved. Approving ' +
|
|
26
|
+
'or rejecting a pending candidate is the user’s review: suggest `/memory approve <id>` or ' +
|
|
27
|
+
'`/memory reject <id>`. Correcting or forgetting saved knowledge is also user-only: ' +
|
|
28
|
+
'suggest `/memory correct <type> <topic> <text>` to replace every entry in a topic or ' +
|
|
29
|
+
'`/memory forget <topic>` to remove it from active recall. These operations block automatic ' +
|
|
30
|
+
'restoration of that topic; historical transcripts and backups remain.';
|
|
31
|
+
|
|
32
|
+
export function createMemoryCommandEntry(): ICommand {
|
|
33
|
+
return {
|
|
34
|
+
name: 'memory',
|
|
35
|
+
displayName: 'Memory',
|
|
36
|
+
description: MEMORY_COMMAND_DESCRIPTION,
|
|
37
|
+
modelDescription: MEMORY_COMMAND_MODEL_DESCRIPTION,
|
|
38
|
+
source: 'memory',
|
|
39
|
+
argumentHint: MEMORY_COMMAND_ARGUMENT_HINT,
|
|
40
|
+
modelInvocable: true,
|
|
41
|
+
safety: 'write',
|
|
42
|
+
subcommands: buildMemoryCommandSubcommands(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createMemorySystemCommand(): ISystemCommand {
|
|
47
|
+
const entry = createMemoryCommandEntry();
|
|
48
|
+
return {
|
|
49
|
+
name: entry.name,
|
|
50
|
+
displayName: entry.displayName,
|
|
51
|
+
description: entry.description,
|
|
52
|
+
...(entry.modelDescription !== undefined ? { modelDescription: entry.modelDescription } : {}),
|
|
53
|
+
requiresPermission: false,
|
|
54
|
+
userInvocable: true,
|
|
55
|
+
modelInvocable: true,
|
|
56
|
+
argumentHint: entry.argumentHint,
|
|
57
|
+
safety: entry.safety,
|
|
58
|
+
subcommands: entry.subcommands,
|
|
59
|
+
execute: executeMemoryCommand,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class MemoryCommandSource implements ICommandSource {
|
|
64
|
+
readonly name = 'memory';
|
|
65
|
+
|
|
66
|
+
getCommands(): ICommand[] {
|
|
67
|
+
return [createMemoryCommandEntry()];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createMemoryCommandModule(): ICommandModule {
|
|
72
|
+
return {
|
|
73
|
+
name: 'agent-command-memory',
|
|
74
|
+
commandSources: [new MemoryCommandSource()],
|
|
75
|
+
systemCommands: [createMemorySystemCommand()],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MEMORY_COMMAND_USAGE,
|
|
3
|
+
approvePendingMemoryCandidate,
|
|
4
|
+
createCommandMemoryStores,
|
|
5
|
+
hasSensitiveCommandMemoryContent,
|
|
6
|
+
isCommandMemoryType,
|
|
7
|
+
listCommandUsedMemoryReferences,
|
|
8
|
+
recordCommandMemoryEvent,
|
|
9
|
+
} from '@robota-sdk/agent-framework';
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
IAppendMemoryInput,
|
|
13
|
+
ICommandHostAdapterAccess,
|
|
14
|
+
ICommandHostMemory,
|
|
15
|
+
ICommandHostSessionAccess,
|
|
16
|
+
ICommandHostWorkspace,
|
|
17
|
+
IMemoryStore,
|
|
18
|
+
} from '@robota-sdk/agent-framework';
|
|
19
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
20
|
+
import type { IMemoryEvent } from '@robota-sdk/agent-interface-session';
|
|
21
|
+
|
|
22
|
+
const SUBCOMMAND_INDEX = 0;
|
|
23
|
+
const TYPE_INDEX = 1;
|
|
24
|
+
const TOPIC_INDEX = 2;
|
|
25
|
+
const TEXT_START_INDEX = 3;
|
|
26
|
+
|
|
27
|
+
function usage(): ICommandResult {
|
|
28
|
+
return {
|
|
29
|
+
message: MEMORY_COMMAND_USAGE,
|
|
30
|
+
success: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatError(error: Error | string): ICommandResult {
|
|
35
|
+
return {
|
|
36
|
+
message: error instanceof Error ? error.message : String(error),
|
|
37
|
+
success: false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `context.getMemoryStore()` throws `WorkspaceAuthorityRequiredError` when the host composed no
|
|
43
|
+
* memory store — off by default, or unavailable on this host (mirrors `EditCheckpointsUnavailableError`
|
|
44
|
+
* for `/rewind`, `rewind-command.ts`'s `formatError`). Uncaught, that throw reaches the GUI as a raw
|
|
45
|
+
* `protocol_error` (a red toast) instead of the SAME plain `command_result` every other unavailable-
|
|
46
|
+
* command case gets — this is the one call in the command that can throw before any subcommand branch
|
|
47
|
+
* runs, so it is guarded once, here, rather than in each branch below.
|
|
48
|
+
*/
|
|
49
|
+
function getMemoryStoreOrError(
|
|
50
|
+
context: ICommandHostMemory,
|
|
51
|
+
): { ok: true; store: IMemoryStore } | { ok: false; result: ICommandResult } {
|
|
52
|
+
try {
|
|
53
|
+
return { ok: true, store: createCommandMemoryStores(context) };
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return { ok: false, result: formatError(error instanceof Error ? error : String(error)) };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function formatList(store: IMemoryStore): Promise<ICommandResult> {
|
|
60
|
+
const summary = await store.list();
|
|
61
|
+
const topics =
|
|
62
|
+
summary.topics.length > 0
|
|
63
|
+
? summary.topics.map((topic) => `- ${topic.name}: ${topic.path}`).join('\n')
|
|
64
|
+
: '(none)';
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
message: [
|
|
68
|
+
`Memory index: ${summary.indexPath}`,
|
|
69
|
+
`Topics directory: ${summary.topicsPath}`,
|
|
70
|
+
'Topics:',
|
|
71
|
+
topics,
|
|
72
|
+
].join('\n'),
|
|
73
|
+
success: true,
|
|
74
|
+
data: {
|
|
75
|
+
indexPath: summary.indexPath,
|
|
76
|
+
topicsPath: summary.topicsPath,
|
|
77
|
+
topicCount: summary.topics.length,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function formatShow(store: IMemoryStore, topic?: string): Promise<ICommandResult> {
|
|
83
|
+
if (!topic || topic === 'index') {
|
|
84
|
+
const memory = await store.loadStartupMemory();
|
|
85
|
+
return {
|
|
86
|
+
message: memory.content || '(empty memory index)',
|
|
87
|
+
success: true,
|
|
88
|
+
data: {
|
|
89
|
+
path: memory.path,
|
|
90
|
+
lineCount: memory.lineCount,
|
|
91
|
+
truncated: memory.truncated,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const content = await store.readTopic(topic);
|
|
97
|
+
return {
|
|
98
|
+
message: content || `(empty memory topic: ${topic})`,
|
|
99
|
+
success: true,
|
|
100
|
+
data: { topic },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseAdd(args: readonly string[]): IAppendMemoryInput | undefined {
|
|
105
|
+
const type = args[TYPE_INDEX];
|
|
106
|
+
const topic = args[TOPIC_INDEX];
|
|
107
|
+
const text = args.slice(TEXT_START_INDEX).join(' ').trim();
|
|
108
|
+
|
|
109
|
+
if (!type || !isCommandMemoryType(type) || !topic || text.length === 0) return undefined;
|
|
110
|
+
return { type, topic, text };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function formatPending(store: IMemoryStore): Promise<ICommandResult> {
|
|
114
|
+
const records = await store.listPending('pending');
|
|
115
|
+
const lines =
|
|
116
|
+
records.length > 0
|
|
117
|
+
? records.map(
|
|
118
|
+
(record) =>
|
|
119
|
+
`- ${record.id} ${record.type}/${record.topic} confidence=${record.confidence}: ${record.text}`,
|
|
120
|
+
)
|
|
121
|
+
: ['(no pending memory candidates)'];
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
message: ['Pending memory candidates:', ...lines].join('\n'),
|
|
125
|
+
success: true,
|
|
126
|
+
data: { count: records.length },
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function recordEvent(context: ICommandHostMemory, event: Omit<IMemoryEvent, 'at'>): void {
|
|
131
|
+
recordCommandMemoryEvent(context, event);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function approvePending(
|
|
135
|
+
context: ICommandHostMemory,
|
|
136
|
+
store: IMemoryStore,
|
|
137
|
+
id: string | undefined,
|
|
138
|
+
): Promise<ICommandResult> {
|
|
139
|
+
if (!id) return usage();
|
|
140
|
+
try {
|
|
141
|
+
const { record, saved } = await approvePendingMemoryCandidate(store, id);
|
|
142
|
+
recordEvent(context, {
|
|
143
|
+
type: 'memory_candidate_approved',
|
|
144
|
+
candidateId: record.id,
|
|
145
|
+
topic: record.topic,
|
|
146
|
+
reason: 'approved-by-user',
|
|
147
|
+
});
|
|
148
|
+
recordEvent(context, {
|
|
149
|
+
type: 'memory_candidate_saved',
|
|
150
|
+
candidateId: record.id,
|
|
151
|
+
topic: record.topic,
|
|
152
|
+
reason: saved.deduplicated ? 'deduplicated' : 'approved-and-saved',
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
message: saved.deduplicated
|
|
156
|
+
? `Saved memory candidate ${id} was already present in ${saved.topicPath}`
|
|
157
|
+
: `Saved memory candidate ${id} to ${saved.topicPath}`,
|
|
158
|
+
success: true,
|
|
159
|
+
data: {
|
|
160
|
+
id,
|
|
161
|
+
status: record.status,
|
|
162
|
+
topic: saved.topic,
|
|
163
|
+
topicPath: saved.topicPath,
|
|
164
|
+
deduplicated: saved.deduplicated,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return formatError(error instanceof Error ? error : String(error));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function rejectPending(
|
|
173
|
+
context: ICommandHostMemory,
|
|
174
|
+
store: IMemoryStore,
|
|
175
|
+
id: string | undefined,
|
|
176
|
+
): Promise<ICommandResult> {
|
|
177
|
+
if (!id) return usage();
|
|
178
|
+
try {
|
|
179
|
+
const record = await store.markPending(id, 'rejected', 'rejected-by-user');
|
|
180
|
+
recordEvent(context, {
|
|
181
|
+
type: 'memory_candidate_rejected',
|
|
182
|
+
candidateId: record.id,
|
|
183
|
+
topic: record.topic,
|
|
184
|
+
reason: 'rejected-by-user',
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
message: `Rejected memory candidate ${id}`,
|
|
188
|
+
success: true,
|
|
189
|
+
data: { id, status: record.status },
|
|
190
|
+
};
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return formatError(error instanceof Error ? error : String(error));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function formatUsed(context: ICommandHostMemory): ICommandResult {
|
|
197
|
+
const references = listCommandUsedMemoryReferences(context);
|
|
198
|
+
const lines =
|
|
199
|
+
references.length > 0
|
|
200
|
+
? references.map((reference) => {
|
|
201
|
+
const suffix = reference.truncated ? ' truncated=true' : '';
|
|
202
|
+
return `- ${reference.topic} score=${reference.score}${suffix}: ${reference.path}`;
|
|
203
|
+
})
|
|
204
|
+
: ['(no memory used in current turn)'];
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
message: ['Used memory references:', ...lines].join('\n'),
|
|
208
|
+
success: true,
|
|
209
|
+
data: { count: references.length, references: [...references] },
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The host's view of the session's permission mode, when it has one. */
|
|
214
|
+
type TMemoryCommandModeAccess = ICommandHostAdapterAccess &
|
|
215
|
+
Partial<Pick<ICommandHostSessionAccess, 'getSession'>>;
|
|
216
|
+
|
|
217
|
+
function isPlanMode(context: TMemoryCommandModeAccess): boolean {
|
|
218
|
+
const adapter = context.getCommandHostAdapters?.().permissionMode;
|
|
219
|
+
const mode = adapter?.getPermissionMode() ?? context.getSession?.().getPermissionMode();
|
|
220
|
+
return mode === 'plan';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function executeMemoryCommand(
|
|
224
|
+
context: ICommandHostMemory & ICommandHostWorkspace & TMemoryCommandModeAccess,
|
|
225
|
+
rawArgs: string,
|
|
226
|
+
): Promise<ICommandResult> {
|
|
227
|
+
const args = rawArgs.trim().split(/\s+/).filter(Boolean);
|
|
228
|
+
const subcommand = args[SUBCOMMAND_INDEX] ?? 'list';
|
|
229
|
+
const source = context.getCommandInvocationSource();
|
|
230
|
+
if (
|
|
231
|
+
(subcommand === 'correct' || subcommand === 'forget') &&
|
|
232
|
+
source !== 'user' && source !== 'remote'
|
|
233
|
+
) {
|
|
234
|
+
return {
|
|
235
|
+
success: false,
|
|
236
|
+
message:
|
|
237
|
+
'Memory correction and forgetting: only the user can run /memory correct or /memory forget.',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
// SELFHOST-008 P1R: the single injected durable-memory port (or fs default) — authoritative for all
|
|
241
|
+
// `/memory` operations, so a surface that swaps the store is honored here too (no split-brain).
|
|
242
|
+
const resolved = getMemoryStoreOrError(context);
|
|
243
|
+
if (!resolved.ok) return resolved.result;
|
|
244
|
+
const store = resolved.store;
|
|
245
|
+
|
|
246
|
+
if (subcommand === 'correct' || subcommand === 'forget') {
|
|
247
|
+
try {
|
|
248
|
+
if (subcommand === 'correct') {
|
|
249
|
+
const input = parseAdd(args);
|
|
250
|
+
if (!input) return usage();
|
|
251
|
+
if (hasSensitiveCommandMemoryContent(input.text))
|
|
252
|
+
return { success: false, message: 'Refusing to save sensitive memory content.' };
|
|
253
|
+
if (!store.replaceTopic)
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
message: 'This memory backend does not support topic correction.',
|
|
257
|
+
};
|
|
258
|
+
const result = await store.replaceTopic(input);
|
|
259
|
+
return {
|
|
260
|
+
success: true,
|
|
261
|
+
message: `Replaced all active memory in ${result.topicPath}. Automatic additions to this topic now require user correction. Historical transcripts and backups remain.`,
|
|
262
|
+
data: { ...result },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
if (!args[TYPE_INDEX] || args.length !== 2) return usage();
|
|
266
|
+
if (!store.forgetTopic)
|
|
267
|
+
return {
|
|
268
|
+
success: false,
|
|
269
|
+
message: 'This memory backend does not support forgetting topics.',
|
|
270
|
+
};
|
|
271
|
+
const result = await store.forgetTopic(args[TYPE_INDEX]);
|
|
272
|
+
return {
|
|
273
|
+
success: true,
|
|
274
|
+
message: `Forgot active memory topic ${result.topic}. Source topic removed; automatic restoration is blocked. Historical transcripts and backups remain.`,
|
|
275
|
+
data: { ...result },
|
|
276
|
+
};
|
|
277
|
+
} catch (error) {
|
|
278
|
+
return formatError(error instanceof Error ? error : String(error));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (subcommand === 'list') return formatList(store);
|
|
283
|
+
if (subcommand === 'show') return formatShow(store, args[TYPE_INDEX]);
|
|
284
|
+
if (subcommand === 'pending') return formatPending(store);
|
|
285
|
+
if (subcommand === 'approve') return approvePending(context, store, args[TYPE_INDEX]);
|
|
286
|
+
if (subcommand === 'reject') return rejectPending(context, store, args[TYPE_INDEX]);
|
|
287
|
+
if (subcommand === 'used') return formatUsed(context);
|
|
288
|
+
if (subcommand === 'add') {
|
|
289
|
+
// Plan mode changes nothing, and saved memory is read back into every later session. The user
|
|
290
|
+
// can still save by hand; the model waits until plan mode ends.
|
|
291
|
+
if (context.getCommandInvocationSource() === 'model' && isPlanMode(context)) {
|
|
292
|
+
return {
|
|
293
|
+
message: 'Plan mode saves no memory. Save it after plan mode ends.',
|
|
294
|
+
success: false,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const input = parseAdd(args);
|
|
298
|
+
if (!input) return usage();
|
|
299
|
+
if (hasSensitiveCommandMemoryContent(input.text)) {
|
|
300
|
+
return {
|
|
301
|
+
message: 'Refusing to save sensitive memory content.',
|
|
302
|
+
success: false,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
let result;
|
|
306
|
+
try {
|
|
307
|
+
result = await store.append(input);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
return formatError(error instanceof Error ? error : String(error));
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
message: result.deduplicated
|
|
313
|
+
? `${input.type} memory already exists in ${result.topicPath}`
|
|
314
|
+
: `Saved ${input.type} memory to ${result.topicPath}`,
|
|
315
|
+
success: true,
|
|
316
|
+
data: {
|
|
317
|
+
indexPath: result.indexPath,
|
|
318
|
+
topicPath: result.topicPath,
|
|
319
|
+
topic: result.topic,
|
|
320
|
+
deduplicated: result.deduplicated,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return usage();
|
|
326
|
+
}
|