@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,143 @@
|
|
|
1
|
+
import { selectAction } from '@robota-sdk/agent-core';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ICommandHostAdapterAccess,
|
|
5
|
+
ICommandHostSessionAccess,
|
|
6
|
+
ICommandHostUserInteraction,
|
|
7
|
+
ICommandModule,
|
|
8
|
+
ISystemCommand,
|
|
9
|
+
ICommandOutputStyleRegistryAdapter,
|
|
10
|
+
} from '@robota-sdk/agent-framework';
|
|
11
|
+
import type { ICommand, ICommandResult, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
12
|
+
|
|
13
|
+
const OUTPUT_STYLE_DESCRIPTION = 'List or switch the active provider-neutral response style';
|
|
14
|
+
const OUTPUT_STYLE_ARGUMENT_HINT = 'list | <style-id>';
|
|
15
|
+
|
|
16
|
+
function getRegistry(
|
|
17
|
+
context: ICommandHostAdapterAccess,
|
|
18
|
+
): ICommandOutputStyleRegistryAdapter | undefined {
|
|
19
|
+
return context.getCommandHostAdapters?.().outputStyleRegistry;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatStyleList(active: string, registry: ICommandOutputStyleRegistryAdapter): string {
|
|
23
|
+
const lines = registry.listOutputStyles().map((style) => {
|
|
24
|
+
const marker = style.id === active ? '* ' : ' ';
|
|
25
|
+
const cost = style.tokenCost ? ` [input cost: ${style.tokenCost}]` : '';
|
|
26
|
+
return `${marker}${style.id} — ${style.name}: ${style.description}${cost}`;
|
|
27
|
+
});
|
|
28
|
+
return ['Available output styles:', ...lines].join('\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function listResult(
|
|
32
|
+
context: ICommandHostSessionAccess,
|
|
33
|
+
registry: ICommandOutputStyleRegistryAdapter,
|
|
34
|
+
): ICommandResult {
|
|
35
|
+
const active = context.getActiveOutputStyleId();
|
|
36
|
+
const outputStyles = registry.listOutputStyles();
|
|
37
|
+
return {
|
|
38
|
+
success: true,
|
|
39
|
+
message: formatStyleList(active, registry),
|
|
40
|
+
data: { outputStyles, active },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function chooseStyle(
|
|
45
|
+
context: ICommandHostUserInteraction,
|
|
46
|
+
registry: ICommandOutputStyleRegistryAdapter,
|
|
47
|
+
): Promise<string | undefined> {
|
|
48
|
+
const interaction = context.getUserInteraction();
|
|
49
|
+
if (!interaction) return undefined;
|
|
50
|
+
const options = registry.listOutputStyles().map((style) => ({
|
|
51
|
+
value: style.id,
|
|
52
|
+
label: style.id,
|
|
53
|
+
description: style.description,
|
|
54
|
+
}));
|
|
55
|
+
const response = await interaction.ask(
|
|
56
|
+
selectAction('output-style', 'Select an output style', options),
|
|
57
|
+
);
|
|
58
|
+
return response.type === 'answer' ? response.values[0] : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function executeOutputStyleCommand(
|
|
62
|
+
context: ICommandHostAdapterAccess & ICommandHostSessionAccess & ICommandHostUserInteraction,
|
|
63
|
+
args: string,
|
|
64
|
+
): Promise<ICommandResult> {
|
|
65
|
+
const registry = getRegistry(context);
|
|
66
|
+
if (!registry) {
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
message: 'Output styles are not available in this environment.',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let styleId = args.trim();
|
|
74
|
+
if (styleId === 'list') return listResult(context, registry);
|
|
75
|
+
if (!styleId) {
|
|
76
|
+
styleId = (await chooseStyle(context, registry)) ?? '';
|
|
77
|
+
if (!styleId) return listResult(context, registry);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const style = registry.getOutputStyle(styleId);
|
|
81
|
+
if (!style) {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
message: `Unknown output style "${styleId}". Available: ${
|
|
85
|
+
registry
|
|
86
|
+
.listOutputStyles()
|
|
87
|
+
.map((entry) => entry.id)
|
|
88
|
+
.join(', ') || '(none)'
|
|
89
|
+
}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
success: true,
|
|
95
|
+
message: `Switching output style to ${style.name}...`,
|
|
96
|
+
data: { outputStyle: style.id },
|
|
97
|
+
hostActions: [{ type: 'output-style-change', styleId: style.id }],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function createOutputStyleCommandEntry(): ICommand {
|
|
102
|
+
return {
|
|
103
|
+
name: 'output-style',
|
|
104
|
+
displayName: 'Output Style',
|
|
105
|
+
description: OUTPUT_STYLE_DESCRIPTION,
|
|
106
|
+
argumentHint: OUTPUT_STYLE_ARGUMENT_HINT,
|
|
107
|
+
source: 'output-style',
|
|
108
|
+
userInvocable: true,
|
|
109
|
+
// User-only: the user's presentation preference.
|
|
110
|
+
modelInvocable: false,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createOutputStyleSystemCommand(): ISystemCommand {
|
|
115
|
+
const entry = createOutputStyleCommandEntry();
|
|
116
|
+
return {
|
|
117
|
+
name: entry.name,
|
|
118
|
+
displayName: entry.displayName,
|
|
119
|
+
description: entry.description,
|
|
120
|
+
argumentHint: entry.argumentHint,
|
|
121
|
+
requiresPermission: false,
|
|
122
|
+
userInvocable: true,
|
|
123
|
+
modelInvocable: false,
|
|
124
|
+
lifecycle: 'inline',
|
|
125
|
+
execute: executeOutputStyleCommand,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export class OutputStyleCommandSource implements ICommandSource {
|
|
130
|
+
readonly name = 'output-style';
|
|
131
|
+
|
|
132
|
+
getCommands(): ICommand[] {
|
|
133
|
+
return [createOutputStyleCommandEntry()];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createOutputStyleCommandModule(): ICommandModule {
|
|
138
|
+
return {
|
|
139
|
+
name: 'agent-command-output-style',
|
|
140
|
+
commandSources: [new OutputStyleCommandSource()],
|
|
141
|
+
systemCommands: [createOutputStyleSystemCommand()],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createPeersCommandModule } from '../peers-command-module.js';
|
|
4
|
+
|
|
5
|
+
describe('/peers command module', () => {
|
|
6
|
+
it('is user-only: never model-invocable, and palette metadata matches the executable', () => {
|
|
7
|
+
const module = createPeersCommandModule();
|
|
8
|
+
const palette = module.commandSources?.[0]?.getCommands()[0];
|
|
9
|
+
const executable = module.systemCommands?.[0];
|
|
10
|
+
expect(executable?.modelInvocable).toBe(false);
|
|
11
|
+
expect(palette?.modelInvocable).toBe(false);
|
|
12
|
+
expect(executable?.userInvocable).toBe(true);
|
|
13
|
+
expect(executable?.description).toBe(palette?.description);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('tells the model it covers linked devices, what it returns, and what to suggest', () => {
|
|
17
|
+
const description =
|
|
18
|
+
createPeersCommandModule().commandSources?.[0]?.getCommands()[0]?.description;
|
|
19
|
+
expect(description).toMatch(/linked devices/);
|
|
20
|
+
expect(description).toMatch(/returns/i);
|
|
21
|
+
expect(description).toMatch(/user-only/i);
|
|
22
|
+
expect(description).toContain('`/peers`');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PEER-004 (#1863) — `/peers` is the surface that makes local discovery observable.
|
|
3
|
+
*
|
|
4
|
+
* Discovery landed as two leaves — a guarded rendezvous directory and a registry whose liveness is
|
|
5
|
+
* settled by pid AND process start time — and measured on this tree before this command existed, no
|
|
6
|
+
* source outside those modules and their tests called either one. A discovery layer nobody can look
|
|
7
|
+
* at is one nobody can catch being wrong, so what these cases pin is the READING: what an operator
|
|
8
|
+
* is told, and in particular what they are told when the answer is "I could not determine".
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
|
|
13
|
+
import { executePeersCommand } from '../peers-command.js';
|
|
14
|
+
|
|
15
|
+
import type { ICommandHostAdapterAccess, ICommandHostAdapters } from '@robota-sdk/agent-framework';
|
|
16
|
+
|
|
17
|
+
type TPeerSummary = ReturnType<NonNullable<ICommandHostAdapters['localPeers']>['list']>[number];
|
|
18
|
+
|
|
19
|
+
const OWN = 'session-self';
|
|
20
|
+
|
|
21
|
+
function hostWithPeers(peers: readonly TPeerSummary[]): ICommandHostAdapterAccess {
|
|
22
|
+
return {
|
|
23
|
+
getCommandHostAdapters: () => ({
|
|
24
|
+
localPeers: { list: () => peers, ownSessionId: () => OWN },
|
|
25
|
+
}),
|
|
26
|
+
} as ICommandHostAdapterAccess;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('what the operator is told', () => {
|
|
30
|
+
it('lists another live session, and marks which row is this one', async () => {
|
|
31
|
+
const result = await executePeersCommand(
|
|
32
|
+
hostWithPeers([
|
|
33
|
+
{ sessionId: OWN, liveness: 'alive' },
|
|
34
|
+
{ sessionId: 'session-other', name: 'review', liveness: 'alive' },
|
|
35
|
+
]),
|
|
36
|
+
);
|
|
37
|
+
expect(result.success).toBe(true);
|
|
38
|
+
expect(result.message).toContain('session-other');
|
|
39
|
+
expect(result.message).toContain('review');
|
|
40
|
+
expect(result.message).toMatch(/session-self.*\(this session\)/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('says so plainly when this session is the only one', async () => {
|
|
44
|
+
// Not an empty list rendered as a header with nothing under it. "No other live session" is the
|
|
45
|
+
// answer, and it is different from "discovery is unavailable" below.
|
|
46
|
+
const result = await executePeersCommand(
|
|
47
|
+
hostWithPeers([{ sessionId: OWN, liveness: 'alive' }]),
|
|
48
|
+
);
|
|
49
|
+
expect(result.message).toContain('No other live session');
|
|
50
|
+
expect(result.success).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('prints `unknown` liveness rather than rounding it to alive', async () => {
|
|
54
|
+
// A host that cannot read process start times answers `unknown` for every peer. A reader who is
|
|
55
|
+
// not told that would take a stale entry for a live session — which is the exact guess the
|
|
56
|
+
// registry refuses to make, and it must not be reintroduced by the surface that displays it.
|
|
57
|
+
const result = await executePeersCommand(
|
|
58
|
+
hostWithPeers([
|
|
59
|
+
{ sessionId: OWN, liveness: 'alive' },
|
|
60
|
+
{ sessionId: 'session-other', liveness: 'unknown' },
|
|
61
|
+
]),
|
|
62
|
+
);
|
|
63
|
+
expect(result.message).toContain('liveness unknown');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('shows observed activity and marks missing activity unknown', async () => {
|
|
67
|
+
const result = await executePeersCommand(
|
|
68
|
+
hostWithPeers([
|
|
69
|
+
{ sessionId: OWN, liveness: 'alive', status: 'idle' },
|
|
70
|
+
{ sessionId: 'busy', liveness: 'alive', status: 'working' },
|
|
71
|
+
{ sessionId: 'waiting', liveness: 'alive', status: 'needs-input' },
|
|
72
|
+
{ sessionId: 'unobserved', liveness: 'unknown', status: 'unknown' },
|
|
73
|
+
]),
|
|
74
|
+
);
|
|
75
|
+
expect(result.message).toMatch(/busy.*working/);
|
|
76
|
+
expect(result.message).toMatch(/waiting.*needs input/);
|
|
77
|
+
expect(result.message).toMatch(/unobserved.*status unknown/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('shows how each peer is related by workspace (#3101 B2)', async () => {
|
|
81
|
+
const result = await executePeersCommand(
|
|
82
|
+
hostWithPeers([
|
|
83
|
+
{ sessionId: OWN, liveness: 'alive', workspaceRelation: 'same-worktree' },
|
|
84
|
+
{ sessionId: 'here', liveness: 'alive', workspaceRelation: 'same-worktree' },
|
|
85
|
+
{ sessionId: 'sibling', liveness: 'alive', workspaceRelation: 'same-repo' },
|
|
86
|
+
{ sessionId: 'elsewhere', liveness: 'alive', workspaceRelation: 'different-repo' },
|
|
87
|
+
{ sessionId: 'plain', liveness: 'alive', workspaceRelation: 'unknown' },
|
|
88
|
+
]),
|
|
89
|
+
);
|
|
90
|
+
expect(result.message).toMatch(/here.*same worktree/);
|
|
91
|
+
expect(result.message).toMatch(/sibling.*same repo/);
|
|
92
|
+
expect(result.message).toMatch(/elsewhere.*different repo/);
|
|
93
|
+
expect(result.message).toMatch(/plain.*workspace unknown/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('reads the workspace-judged listing when the host offers one', async () => {
|
|
97
|
+
const result = await executePeersCommand({
|
|
98
|
+
getCommandHostAdapters: () => ({
|
|
99
|
+
localPeers: {
|
|
100
|
+
list: () => [
|
|
101
|
+
{ sessionId: OWN, liveness: 'alive' },
|
|
102
|
+
{ sessionId: 'sibling', liveness: 'alive' },
|
|
103
|
+
],
|
|
104
|
+
listWithWorkspace: async () => [
|
|
105
|
+
{ sessionId: OWN, liveness: 'alive' },
|
|
106
|
+
{ sessionId: 'sibling', liveness: 'alive', workspaceRelation: 'same-repo' },
|
|
107
|
+
],
|
|
108
|
+
ownSessionId: () => OWN,
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
} as ICommandHostAdapterAccess);
|
|
112
|
+
expect(result.message).toMatch(/sibling.*same repo/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('says a mismatched workspace claim was not believed', async () => {
|
|
116
|
+
const result = await executePeersCommand(
|
|
117
|
+
hostWithPeers([
|
|
118
|
+
{ sessionId: OWN, liveness: 'alive' },
|
|
119
|
+
{
|
|
120
|
+
sessionId: 'liar',
|
|
121
|
+
liveness: 'alive',
|
|
122
|
+
workspaceRelation: 'unknown',
|
|
123
|
+
workspaceClaim: 'mismatched',
|
|
124
|
+
},
|
|
125
|
+
]),
|
|
126
|
+
);
|
|
127
|
+
expect(result.message).toMatch(/liar.*workspace claim mismatched, not believed/);
|
|
128
|
+
expect(result.message).not.toMatch(/liar.*same (worktree|repo)/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('does not show a dead entry', async () => {
|
|
132
|
+
// A crashed session leaves its file behind. That is debris, not a peer, and offering it as one
|
|
133
|
+
// would make the operator address something that cannot answer.
|
|
134
|
+
const result = await executePeersCommand(
|
|
135
|
+
hostWithPeers([
|
|
136
|
+
{ sessionId: OWN, liveness: 'alive' },
|
|
137
|
+
{ sessionId: 'session-gone', liveness: 'dead' },
|
|
138
|
+
]),
|
|
139
|
+
);
|
|
140
|
+
expect(result.message).not.toContain('session-gone');
|
|
141
|
+
expect(result.message).toContain('No other live session');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('linked devices of the device mesh', () => {
|
|
146
|
+
const DEVICE = 'D'.repeat(43);
|
|
147
|
+
|
|
148
|
+
function hostWithDevices(
|
|
149
|
+
peers: readonly TPeerSummary[],
|
|
150
|
+
devices: readonly { deviceId: string; name?: string; locality: 'same-host' | 'another-host' }[],
|
|
151
|
+
): ICommandHostAdapterAccess {
|
|
152
|
+
return {
|
|
153
|
+
getCommandHostAdapters: () => ({
|
|
154
|
+
localPeers: { list: () => peers, ownSessionId: () => OWN, listDevices: () => devices },
|
|
155
|
+
}),
|
|
156
|
+
} as ICommandHostAdapterAccess;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
it('lists a linked device beside the sessions on this host, addressable by its id', async () => {
|
|
160
|
+
const result = await executePeersCommand(
|
|
161
|
+
hostWithDevices(
|
|
162
|
+
[
|
|
163
|
+
{ sessionId: OWN, liveness: 'alive' },
|
|
164
|
+
{ sessionId: 'session-other', liveness: 'alive' },
|
|
165
|
+
],
|
|
166
|
+
[{ deviceId: DEVICE, name: 'desktop', locality: 'another-host' }],
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
expect(result.success).toBe(true);
|
|
170
|
+
expect(result.message).toContain('session-other');
|
|
171
|
+
expect(result.message).toMatch(new RegExp(`${DEVICE}\\s+desktop\\s+on another machine`));
|
|
172
|
+
expect(result.message).toContain('/peers send <session-or-device-id>');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('lists a linked device even when this is the only session on this host', async () => {
|
|
176
|
+
const result = await executePeersCommand(
|
|
177
|
+
hostWithDevices(
|
|
178
|
+
[{ sessionId: OWN, liveness: 'alive' }],
|
|
179
|
+
[{ deviceId: DEVICE, locality: 'same-host' }],
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
expect(result.message).not.toContain('No other live session');
|
|
183
|
+
expect(result.message).toMatch(new RegExp(`${DEVICE}\\s+on this machine`));
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('when local discovery is off but the device mesh is not', () => {
|
|
188
|
+
const DEVICE = 'D'.repeat(43);
|
|
189
|
+
|
|
190
|
+
function hostWithoutDiscovery(
|
|
191
|
+
devices: readonly { deviceId: string; name?: string; locality: 'same-host' | 'another-host' }[],
|
|
192
|
+
): ICommandHostAdapterAccess {
|
|
193
|
+
return {
|
|
194
|
+
getCommandHostAdapters: () => ({
|
|
195
|
+
localPeers: {
|
|
196
|
+
list: () => [],
|
|
197
|
+
ownSessionId: () => OWN,
|
|
198
|
+
listDevices: () => devices,
|
|
199
|
+
localDiscoveryOff: 'the rendezvous directory was not admitted',
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
} as ICommandHostAdapterAccess;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
it('lists the linked devices and says why sessions on this host are not listed', async () => {
|
|
206
|
+
const result = await executePeersCommand(
|
|
207
|
+
hostWithoutDiscovery([{ deviceId: DEVICE, name: 'desktop', locality: 'another-host' }]),
|
|
208
|
+
);
|
|
209
|
+
expect(result.success).toBe(true);
|
|
210
|
+
expect(result.message).toMatch(new RegExp(`${DEVICE}\\s+desktop\\s+on another machine`));
|
|
211
|
+
expect(result.message).toContain('not admitted');
|
|
212
|
+
expect(result.message).not.toContain('Live sessions');
|
|
213
|
+
expect(result.message).toContain('/peers send <device-id>');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('with no device linked, says discovery is off rather than that nobody is there', async () => {
|
|
217
|
+
const result = await executePeersCommand(hostWithoutDiscovery([]));
|
|
218
|
+
expect(result.message).toContain('not admitted');
|
|
219
|
+
expect(result.message).not.toContain('No other live session');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('when the host wires no discovery', () => {
|
|
224
|
+
it('says the feature is unavailable rather than reporting no peers', async () => {
|
|
225
|
+
// The two are different facts and the difference matters: "nobody is there" invites the operator
|
|
226
|
+
// to start a second session, and "this build cannot see them" does not.
|
|
227
|
+
const result = await executePeersCommand({
|
|
228
|
+
getCommandHostAdapters: () => ({}),
|
|
229
|
+
} as ICommandHostAdapterAccess);
|
|
230
|
+
expect(result.message).toContain('not available');
|
|
231
|
+
expect(result.message).not.toContain('No other live session');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('PEER-006 — /peers send', () => {
|
|
236
|
+
function hostWithSend(
|
|
237
|
+
send: (target: string, text: string) => Promise<{ state: string; reason?: string }>,
|
|
238
|
+
): ICommandHostAdapterAccess {
|
|
239
|
+
return {
|
|
240
|
+
getCommandHostAdapters: () => ({
|
|
241
|
+
localPeers: {
|
|
242
|
+
list: () => [{ sessionId: 'other', liveness: 'alive' as const }],
|
|
243
|
+
ownSessionId: () => OWN,
|
|
244
|
+
send,
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
} as unknown as ICommandHostAdapterAccess;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
it('passes the whole message, not just its first word', async () => {
|
|
251
|
+
const calls: Array<[string, string]> = [];
|
|
252
|
+
await executePeersCommand(
|
|
253
|
+
hostWithSend(async (target, text) => {
|
|
254
|
+
calls.push([target, text]);
|
|
255
|
+
return { state: 'acknowledged' };
|
|
256
|
+
}),
|
|
257
|
+
'send other look at the failing test in cli.ts',
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
expect(calls).toEqual([['other', 'look at the failing test in cli.ts']]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('reports `pending` as waiting, never as delivered', async () => {
|
|
264
|
+
const result = await executePeersCommand(
|
|
265
|
+
hostWithSend(async () => ({ state: 'pending' })),
|
|
266
|
+
'send other hello',
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// The operator has to be able to see the wait. "Delivered" here would hide a turn already
|
|
270
|
+
// running on the other side, which is the one thing they might want to act on.
|
|
271
|
+
expect(result.message).toContain('waiting');
|
|
272
|
+
expect(result.message).not.toContain('Delivered');
|
|
273
|
+
expect(result.success).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('carries the refusal reason back to the operator', async () => {
|
|
277
|
+
const result = await executePeersCommand(
|
|
278
|
+
hostWithSend(async () => ({ state: 'refused', reason: 'no session ghost is announced' })),
|
|
279
|
+
'send ghost hello',
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
expect(result.message).toContain('no session ghost is announced');
|
|
283
|
+
expect(result.success).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('explains what it needs when the message is missing', async () => {
|
|
287
|
+
const result = await executePeersCommand(
|
|
288
|
+
hostWithSend(async () => ({ state: 'acknowledged' })),
|
|
289
|
+
'send other',
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
expect(result.message).toContain('Usage: /peers send');
|
|
293
|
+
expect(result.success).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('does not read a session id beginning with `send` as the subcommand', async () => {
|
|
297
|
+
// `startsWith('send')` would claim this as `/peers send`, leaving `er-1` as the target.
|
|
298
|
+
const result = await executePeersCommand(
|
|
299
|
+
hostWithSend(async () => ({ state: 'acknowledged' })),
|
|
300
|
+
'sender-1',
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
expect(result.message).toContain('Live sessions:');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('says so when the host can discover peers but not address them', async () => {
|
|
307
|
+
const result = await executePeersCommand(
|
|
308
|
+
{
|
|
309
|
+
getCommandHostAdapters: () => ({
|
|
310
|
+
localPeers: { list: () => [], ownSessionId: () => OWN },
|
|
311
|
+
}),
|
|
312
|
+
} as ICommandHostAdapterAccess,
|
|
313
|
+
'send other hello',
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
expect(result.message).toContain('cannot address them');
|
|
317
|
+
expect(result.success).toBe(false);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('/peers send-file', () => {
|
|
322
|
+
type TPrepareFile = NonNullable<NonNullable<ICommandHostAdapters['localPeers']>['prepareFile']>;
|
|
323
|
+
|
|
324
|
+
function hostWithFiles(prepareFile: TPrepareFile): ICommandHostAdapterAccess {
|
|
325
|
+
return {
|
|
326
|
+
getCwd: () => '/work',
|
|
327
|
+
getCommandHostAdapters: () => ({
|
|
328
|
+
localPeers: {
|
|
329
|
+
list: () => [{ sessionId: 'other', liveness: 'alive' as const }],
|
|
330
|
+
ownSessionId: () => OWN,
|
|
331
|
+
prepareFile,
|
|
332
|
+
},
|
|
333
|
+
}),
|
|
334
|
+
} as unknown as ICommandHostAdapterAccess;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
it('sends the named file as the operator, spaces in the path and all', async () => {
|
|
338
|
+
const calls: unknown[] = [];
|
|
339
|
+
const result = await executePeersCommand(
|
|
340
|
+
hostWithFiles(async (target, path, options) => {
|
|
341
|
+
calls.push([target, path, options]);
|
|
342
|
+
return {
|
|
343
|
+
ok: true,
|
|
344
|
+
file: {
|
|
345
|
+
path: '/work/my notes.txt',
|
|
346
|
+
size: 5,
|
|
347
|
+
sha256: 'e'.repeat(64),
|
|
348
|
+
send: async () => ({ state: 'delivered' }),
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}),
|
|
352
|
+
'send-file other my notes.txt',
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
expect(calls).toEqual([['other', 'my notes.txt', { origin: 'operator', cwd: '/work' }]]);
|
|
356
|
+
expect(result.success).toBe(true);
|
|
357
|
+
expect(result.message).toContain(`5 bytes, sha256 ${'e'.repeat(64)}`);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('carries a refusal back to the operator', async () => {
|
|
361
|
+
const result = await executePeersCommand(
|
|
362
|
+
hostWithFiles(async () => ({
|
|
363
|
+
ok: true,
|
|
364
|
+
file: {
|
|
365
|
+
path: '/work/a.txt',
|
|
366
|
+
size: 1,
|
|
367
|
+
sha256: 'e'.repeat(64),
|
|
368
|
+
send: async () => ({ state: 'refused', reason: 'the receiving side did not accept it' }),
|
|
369
|
+
},
|
|
370
|
+
})),
|
|
371
|
+
'send-file other a.txt',
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
expect(result.success).toBe(false);
|
|
375
|
+
expect(result.message).toContain('did not accept it');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('says what it needs when the path is missing', async () => {
|
|
379
|
+
const result = await executePeersCommand(
|
|
380
|
+
hostWithFiles(async () => ({ ok: false, reason: 'unused' })),
|
|
381
|
+
'send-file other',
|
|
382
|
+
);
|
|
383
|
+
expect(result.message).toContain('Usage: /peers send-file');
|
|
384
|
+
expect(result.success).toBe(false);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('says so when the host cannot send files', async () => {
|
|
388
|
+
const result = await executePeersCommand(
|
|
389
|
+
{
|
|
390
|
+
getCommandHostAdapters: () => ({
|
|
391
|
+
localPeers: { list: () => [], ownSessionId: () => OWN },
|
|
392
|
+
}),
|
|
393
|
+
} as ICommandHostAdapterAccess,
|
|
394
|
+
'send-file other a.txt',
|
|
395
|
+
);
|
|
396
|
+
expect(result.message).toContain('cannot send files');
|
|
397
|
+
expect(result.success).toBe(false);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Only the module factory leaves this directory.
|
|
3
|
+
*
|
|
4
|
+
* `executePeersCommand`, `PeersCommandSource` and `createPeersCommandEntry` are how the module is
|
|
5
|
+
* BUILT, not how it is used: the command is registered by default, so nothing outside constructs one.
|
|
6
|
+
* Exporting them would have added four entries to this package's frozen undocumented-surface count —
|
|
7
|
+
* debt that may shrink and never grow — in exchange for a reach nobody asked for.
|
|
8
|
+
*/
|
|
9
|
+
export { createPeersCommandModule } from './peers-command-module.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { executePeersCommand } from './peers-command.js';
|
|
2
|
+
|
|
3
|
+
import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
|
|
4
|
+
import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
5
|
+
|
|
6
|
+
export function createPeersCommandEntry(): ICommand {
|
|
7
|
+
return {
|
|
8
|
+
name: 'peers',
|
|
9
|
+
displayName: 'Peers',
|
|
10
|
+
description:
|
|
11
|
+
"List the other live sessions for this user on this host and the user's linked devices, send one a message, or send one a copy of a file. Returns the sessions and devices with the ids to address them by (linked devices even when local discovery is off and sessions on this host cannot be listed), or how a send ended. User-only: the model cannot run it; it answers a peer's message with its reply tool, and when the user wants to reach another session or device, suggest they run `/peers`.",
|
|
12
|
+
source: 'peers',
|
|
13
|
+
// The model does not enumerate the operator's other sessions. Discovery is an operator-facing
|
|
14
|
+
// view of who is at the machine, which is a fact about the person and not about the task.
|
|
15
|
+
// User-only: sends messages and files into other sessions; crossing a session boundary is the
|
|
16
|
+
// user's call. The model sends a file only through `peer_send_file`, which asks every time.
|
|
17
|
+
modelInvocable: false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createPeersSystemCommand(): ISystemCommand {
|
|
22
|
+
const entry = createPeersCommandEntry();
|
|
23
|
+
return {
|
|
24
|
+
name: entry.name,
|
|
25
|
+
displayName: entry.displayName,
|
|
26
|
+
description: entry.description,
|
|
27
|
+
requiresPermission: false,
|
|
28
|
+
userInvocable: true,
|
|
29
|
+
modelInvocable: false,
|
|
30
|
+
lifecycle: 'inline',
|
|
31
|
+
execute: (context, args) => executePeersCommand(context, args),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class PeersCommandSource implements ICommandSource {
|
|
36
|
+
readonly name = 'peers';
|
|
37
|
+
|
|
38
|
+
getCommands(): ICommand[] {
|
|
39
|
+
return [createPeersCommandEntry()];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createPeersCommandModule(): ICommandModule {
|
|
44
|
+
return {
|
|
45
|
+
name: 'agent-command-peers',
|
|
46
|
+
commandSources: [new PeersCommandSource()],
|
|
47
|
+
systemCommands: [createPeersSystemCommand()],
|
|
48
|
+
};
|
|
49
|
+
}
|