@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,220 @@
|
|
|
1
|
+
import type { ICommandHostAdapterAccess, ICommandHostAdapters } from '@robota-sdk/agent-framework';
|
|
2
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
3
|
+
|
|
4
|
+
/** One row as the port hands it over. Derived, so it cannot drift from the adapter it reads. */
|
|
5
|
+
type TPeerSummary = ReturnType<NonNullable<ICommandHostAdapters['localPeers']>['list']>[number];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `/peers` (PEER-004, #1863) — which other live sessions this one can address.
|
|
9
|
+
*
|
|
10
|
+
* Discovery landed as a leaf (a guarded rendezvous directory, an entry per live session, liveness
|
|
11
|
+
* settled by pid AND process start time) and nothing called it: measured on this tree, no file
|
|
12
|
+
* outside the module and its tests referenced `announcePeer` or `openLocalPeerRendezvous`. This
|
|
13
|
+
* command is the surface that makes it observable, which is also what makes it falsifiable — a
|
|
14
|
+
* discovery layer nobody can look at is one nobody can catch being wrong.
|
|
15
|
+
*
|
|
16
|
+
* The command reads an injected adapter and touches no filesystem, for the same reason it never
|
|
17
|
+
* constructs a transport: the guarded directory, its permissions and the liveness rule are
|
|
18
|
+
* composition-root concerns and must have one owner.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** `dead` is not shown. An entry whose process is gone is debris, not a peer. */
|
|
22
|
+
function addressable(peer: TPeerSummary): boolean {
|
|
23
|
+
return peer.liveness !== 'dead';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function describe(peer: TPeerSummary, ownSessionId: string): string {
|
|
27
|
+
const self = peer.sessionId === ownSessionId ? ' (this session)' : '';
|
|
28
|
+
// `unknown` is printed, never rounded to `alive`. A host that cannot read process start times
|
|
29
|
+
// answers `unknown` for every peer, and a reader who is not told that would take a stale entry for
|
|
30
|
+
// a live session.
|
|
31
|
+
const liveness = peer.liveness === 'unknown' ? ' liveness unknown' : '';
|
|
32
|
+
const status = peer.status === 'needs-input' ? 'needs input' : (peer.status ?? 'unknown');
|
|
33
|
+
return ` ${peer.sessionId}${peer.name ? ` ${peer.name}` : ''} status ${status}${self ? '' : workspace(peer)}${liveness}${self}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type TDeviceSummary = ReturnType<
|
|
37
|
+
NonNullable<NonNullable<ICommandHostAdapters['localPeers']>['listDevices']>
|
|
38
|
+
>[number];
|
|
39
|
+
|
|
40
|
+
/** A linked device: its full id, since that is what `send` takes, and where it runs. */
|
|
41
|
+
function describeDevice(device: TDeviceSummary): string {
|
|
42
|
+
const where = device.locality === 'same-host' ? 'on this machine' : 'on another machine';
|
|
43
|
+
return ` ${device.deviceId}${device.name ? ` ${device.name}` : ''} ${where}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The relation this session verified. A claim it could not confirm is named as such, not shown. */
|
|
47
|
+
function workspace(peer: TPeerSummary): string {
|
|
48
|
+
if (peer.workspaceClaim === 'mismatched') return ' workspace claim mismatched, not believed';
|
|
49
|
+
switch (peer.workspaceRelation) {
|
|
50
|
+
case undefined:
|
|
51
|
+
return '';
|
|
52
|
+
case 'unknown':
|
|
53
|
+
return ' workspace unknown';
|
|
54
|
+
default:
|
|
55
|
+
return ` ${peer.workspaceRelation.replace('-', ' ')}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* PEER-006 — `/peers send <session-id> <text>`.
|
|
61
|
+
*
|
|
62
|
+
* Split on the FIRST run of whitespace only: everything after the session id is the message, spaces
|
|
63
|
+
* and all. Splitting on every space would silently truncate any message with more than one word,
|
|
64
|
+
* which is nearly all of them.
|
|
65
|
+
*/
|
|
66
|
+
function parseSend(args: string): { target: string; text: string } | undefined {
|
|
67
|
+
const trimmed = args.trim();
|
|
68
|
+
const at = trimmed.search(/\s/);
|
|
69
|
+
if (at === -1) return undefined;
|
|
70
|
+
const text = trimmed.slice(at).trim();
|
|
71
|
+
if (text === '') return undefined;
|
|
72
|
+
return { target: trimmed.slice(0, at), text };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A state the sender can act on, rendered as a sentence rather than a status word. */
|
|
76
|
+
function describeSend(result: { state: string; reason?: string }, target: string): ICommandResult {
|
|
77
|
+
const reason = result.reason !== undefined ? ` ${result.reason}` : '';
|
|
78
|
+
switch (result.state) {
|
|
79
|
+
case 'acknowledged':
|
|
80
|
+
case 'delivered':
|
|
81
|
+
return { message: `Delivered to ${target}.`, success: true };
|
|
82
|
+
case 'pending':
|
|
83
|
+
// NOT reported as delivered. The peer has it and has not run it — usually because a turn is
|
|
84
|
+
// already running there — and telling the operator it landed would hide a wait they can see.
|
|
85
|
+
return {
|
|
86
|
+
message: `${target} has the message; it is waiting behind work already running there.`,
|
|
87
|
+
success: true,
|
|
88
|
+
};
|
|
89
|
+
case 'duplicate':
|
|
90
|
+
return { message: `${target} had already seen that message.`, success: true };
|
|
91
|
+
default:
|
|
92
|
+
return { message: `Not delivered to ${target}.${reason}`, success: false };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function executeSend(
|
|
97
|
+
adapter: NonNullable<ICommandHostAdapters['localPeers']>,
|
|
98
|
+
args: string,
|
|
99
|
+
): Promise<ICommandResult> {
|
|
100
|
+
if (adapter.send === undefined) {
|
|
101
|
+
return {
|
|
102
|
+
message: 'This environment can discover other sessions but cannot address them.',
|
|
103
|
+
success: false,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const parsed = parseSend(args);
|
|
107
|
+
if (parsed === undefined) {
|
|
108
|
+
return {
|
|
109
|
+
message: 'Usage: /peers send <session-id> <message>. Run /peers for the session ids.',
|
|
110
|
+
success: false,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return describeSend(await adapter.send(parsed.target, parsed.text), parsed.target);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* `/peers send-file <session-id> <path>` — the operator sends a copy of a file.
|
|
118
|
+
*
|
|
119
|
+
* The operator typed the path, so any regular file they can read may go, including one outside the
|
|
120
|
+
* workspace or one that looks like it holds secrets: this is the one way to send those. Everything
|
|
121
|
+
* after the session id is the path, spaces and all.
|
|
122
|
+
*/
|
|
123
|
+
async function executeSendFile(
|
|
124
|
+
adapter: NonNullable<ICommandHostAdapters['localPeers']>,
|
|
125
|
+
args: string,
|
|
126
|
+
cwd: string,
|
|
127
|
+
): Promise<ICommandResult> {
|
|
128
|
+
if (adapter.prepareFile === undefined) {
|
|
129
|
+
return { message: 'This environment cannot send files to other sessions.', success: false };
|
|
130
|
+
}
|
|
131
|
+
const parsed = parseSend(args);
|
|
132
|
+
if (parsed === undefined) {
|
|
133
|
+
return {
|
|
134
|
+
message: 'Usage: /peers send-file <session-id> <path>. Run /peers for the session ids.',
|
|
135
|
+
success: false,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const prepared = await adapter.prepareFile(parsed.target, parsed.text, {
|
|
139
|
+
origin: 'operator',
|
|
140
|
+
cwd,
|
|
141
|
+
});
|
|
142
|
+
if (!prepared.ok) return { message: `Not sent: ${prepared.reason}`, success: false };
|
|
143
|
+
const { file } = prepared;
|
|
144
|
+
const result = await file.send();
|
|
145
|
+
if (result.state === 'delivered' || result.state === 'acknowledged') {
|
|
146
|
+
return {
|
|
147
|
+
message: `Sent ${file.path} to ${parsed.target}: ${file.size} bytes, sha256 ${file.sha256}.`,
|
|
148
|
+
success: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const reason = result.reason !== undefined ? ` ${result.reason}` : '';
|
|
152
|
+
return { message: `${file.path} was not sent to ${parsed.target}.${reason}`, success: false };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function executePeersCommand(
|
|
156
|
+
context: ICommandHostAdapterAccess & { getCwd?(): string },
|
|
157
|
+
args = '',
|
|
158
|
+
): Promise<ICommandResult> {
|
|
159
|
+
const adapter = context.getCommandHostAdapters?.().localPeers;
|
|
160
|
+
if (!adapter) {
|
|
161
|
+
return { message: 'Local peer discovery is not available in this environment.', success: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// `send` must be a WHOLE word: `startsWith('send')` would also claim a session id beginning with
|
|
165
|
+
// those four letters, and a uuid that happens to start `send…` is not a subcommand.
|
|
166
|
+
const trimmed = args.trim();
|
|
167
|
+
const sendFileVerb = /^send-file(?=\s|$)/.exec(trimmed);
|
|
168
|
+
if (sendFileVerb !== null) {
|
|
169
|
+
return executeSendFile(
|
|
170
|
+
adapter,
|
|
171
|
+
trimmed.slice(sendFileVerb[0].length),
|
|
172
|
+
context.getCwd?.() ?? '',
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const sendVerb = /^send(?=\s|$)/.exec(trimmed);
|
|
176
|
+
if (sendVerb !== null) return executeSend(adapter, trimmed.slice(sendVerb[0].length));
|
|
177
|
+
|
|
178
|
+
const devices = adapter.listDevices?.() ?? [];
|
|
179
|
+
const discoveryOff = adapter.localDiscoveryOff;
|
|
180
|
+
if (discoveryOff !== undefined) {
|
|
181
|
+
const off = `Sessions on this host are not listed: local peer discovery is off for this session (${discoveryOff}).`;
|
|
182
|
+
return {
|
|
183
|
+
message:
|
|
184
|
+
devices.length === 0
|
|
185
|
+
? `${off}\nNo device is linked right now.`
|
|
186
|
+
: `Linked devices:\n${devices.map(describeDevice).join('\n')}\n\n${off}` +
|
|
187
|
+
`\n\nSend to one: /peers send <device-id> <message>` +
|
|
188
|
+
`\nSend a file: /peers send-file <device-id> <path>`,
|
|
189
|
+
success: true,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const own = adapter.ownSessionId();
|
|
194
|
+
// The workspace-judged listing when the host has one; it reads git, so only this view asks for it.
|
|
195
|
+
const peers = (
|
|
196
|
+
adapter.listWithWorkspace !== undefined ? await adapter.listWithWorkspace() : adapter.list()
|
|
197
|
+
).filter(addressable);
|
|
198
|
+
const others = peers.filter((peer) => peer.sessionId !== own);
|
|
199
|
+
|
|
200
|
+
if (others.length === 0 && devices.length === 0) {
|
|
201
|
+
return {
|
|
202
|
+
message:
|
|
203
|
+
'No other live session is announced. Start a second session on this host, as this user, ' +
|
|
204
|
+
'and it appears here.',
|
|
205
|
+
success: true,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const sections = [`Live sessions:\n${peers.map((peer) => describe(peer, own)).join('\n')}`];
|
|
210
|
+
if (devices.length > 0) {
|
|
211
|
+
sections.push(`Linked devices:\n${devices.map(describeDevice).join('\n')}`);
|
|
212
|
+
}
|
|
213
|
+
const target = devices.length > 0 ? '<session-or-device-id>' : '<session-id>';
|
|
214
|
+
return {
|
|
215
|
+
message:
|
|
216
|
+
`${sections.join('\n\n')}\n\nSend to one: /peers send ${target} <message>` +
|
|
217
|
+
`\nSend a file: /peers send-file ${target} <path>`,
|
|
218
|
+
success: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
ICommandHostContext,
|
|
4
|
+
IEditCheckpointRestoreResult,
|
|
5
|
+
IPermissionRuleLayer,
|
|
6
|
+
IPermissionsCommandState,
|
|
7
|
+
} from '@robota-sdk/agent-framework';
|
|
8
|
+
import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
|
|
9
|
+
import { createPermissionsCommandModule } from '../permissions-command-module.js';
|
|
10
|
+
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
11
|
+
|
|
12
|
+
type TPermissionModeName = 'plan' | 'default' | 'acceptEdits' | 'bypassPermissions';
|
|
13
|
+
type TSetPermissionModeSpy = ReturnType<typeof vi.fn<(nextMode: TPermissionModeName) => void>>;
|
|
14
|
+
|
|
15
|
+
function createCheckpointResult(): IEditCheckpointRestoreResult {
|
|
16
|
+
return {
|
|
17
|
+
target: {
|
|
18
|
+
id: 'checkpoint_1',
|
|
19
|
+
sessionId: 'session_1',
|
|
20
|
+
sequence: 1,
|
|
21
|
+
prompt: 'edit files',
|
|
22
|
+
createdAt: '2026-05-03T00:00:00.000Z',
|
|
23
|
+
fileCount: 1,
|
|
24
|
+
},
|
|
25
|
+
restoredCheckpointCount: 0,
|
|
26
|
+
restoredFileCount: 0,
|
|
27
|
+
removedCheckpointCount: 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createCommandHostContext(options?: {
|
|
32
|
+
mode?: TPermissionModeName;
|
|
33
|
+
sessionAllowed?: readonly string[];
|
|
34
|
+
rules?: { allow: string[]; deny: string[]; ask: string[] };
|
|
35
|
+
denials?: IPermissionsCommandState['recentDenials'];
|
|
36
|
+
layers?: readonly IPermissionRuleLayer[];
|
|
37
|
+
retryDenial?: (index: number) => IPermissionsCommandState['recentDenials'][number] | undefined;
|
|
38
|
+
}): ReturnType<typeof createTestCommandHost> & { setPermissionMode: TSetPermissionModeSpy } {
|
|
39
|
+
let mode = options?.mode ?? 'default';
|
|
40
|
+
const setPermissionMode = vi.fn((nextMode: TPermissionModeName) => {
|
|
41
|
+
mode = nextMode;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...createTestCommandHost({
|
|
46
|
+
overrides: {
|
|
47
|
+
getSession: () => {
|
|
48
|
+
throw new Error('permissions command should use the permission mode adapter');
|
|
49
|
+
},
|
|
50
|
+
getCommandHostAdapters: () => ({
|
|
51
|
+
permissionMode: {
|
|
52
|
+
getPermissionMode: () => mode,
|
|
53
|
+
setPermissionMode,
|
|
54
|
+
listSessionAllowedTools: () => options?.sessionAllowed ?? [],
|
|
55
|
+
getPermissionRules: () => options?.rules ?? { allow: [], deny: [], ask: [] },
|
|
56
|
+
listRecentDenials: () => options?.denials ?? [],
|
|
57
|
+
retryDenial: (index: number) => options?.retryDenial?.(index),
|
|
58
|
+
},
|
|
59
|
+
...(options?.layers !== undefined
|
|
60
|
+
? { permissionRules: { readLayers: () => options.layers! } }
|
|
61
|
+
: {}),
|
|
62
|
+
}),
|
|
63
|
+
getContextState: () => ({
|
|
64
|
+
usedTokens: 0,
|
|
65
|
+
maxTokens: 1,
|
|
66
|
+
usedPercentage: 0,
|
|
67
|
+
remainingPercentage: 100,
|
|
68
|
+
}),
|
|
69
|
+
getAutoCompactThreshold: () => 0.835,
|
|
70
|
+
compactContext: async () => undefined,
|
|
71
|
+
getCwd: () => '/workspace',
|
|
72
|
+
listEditCheckpoints: () => [],
|
|
73
|
+
restoreEditCheckpoint: async () => createCheckpointResult(),
|
|
74
|
+
rollbackEditCheckpoint: async () => createCheckpointResult(),
|
|
75
|
+
getUsedMemoryReferences: () => [],
|
|
76
|
+
recordMemoryEvent: () => undefined,
|
|
77
|
+
listBackgroundTasks: () => [],
|
|
78
|
+
readBackgroundTaskLog: async () => ({ taskId: 'task_1', lines: [] }),
|
|
79
|
+
cancelBackgroundTask: async () => undefined,
|
|
80
|
+
closeBackgroundTask: async () => undefined,
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
setPermissionMode,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe('createPermissionsCommandModule', () => {
|
|
88
|
+
it('provides permissions metadata and user-only executable command from one module owner', () => {
|
|
89
|
+
const module = createPermissionsCommandModule();
|
|
90
|
+
const command = module.systemCommands?.[0];
|
|
91
|
+
const entry = module.commandSources?.[0]?.getCommands()[0];
|
|
92
|
+
|
|
93
|
+
expect(module.name).toBe('agent-command-permissions');
|
|
94
|
+
expect(entry).toEqual(
|
|
95
|
+
expect.objectContaining({
|
|
96
|
+
name: 'permissions',
|
|
97
|
+
description: 'Show/change permission mode and permission rules',
|
|
98
|
+
argumentHint: 'plan | default | acceptEdits | bypassPermissions | auto',
|
|
99
|
+
source: 'permissions',
|
|
100
|
+
modelInvocable: false,
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
expect(entry?.subcommands?.map((subcommand) => subcommand.name)).toEqual([
|
|
104
|
+
'plan',
|
|
105
|
+
'default',
|
|
106
|
+
'acceptEdits',
|
|
107
|
+
'bypassPermissions',
|
|
108
|
+
'auto',
|
|
109
|
+
]);
|
|
110
|
+
expect(command).toEqual(
|
|
111
|
+
expect.objectContaining({
|
|
112
|
+
name: 'permissions',
|
|
113
|
+
description: 'Show/change permission mode and permission rules',
|
|
114
|
+
argumentHint: 'plan | default | acceptEdits | bypassPermissions | auto',
|
|
115
|
+
lifecycle: 'inline',
|
|
116
|
+
modelInvocable: false,
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
expect(command?.subcommands).toEqual(entry?.subcommands);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('reports no session-approved tools when the command allowlist is empty', async () => {
|
|
123
|
+
const executor = new SystemCommandExecutor([
|
|
124
|
+
...(createPermissionsCommandModule().systemCommands ?? []),
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const result = await executor.execute('permissions', createCommandHostContext(), '');
|
|
128
|
+
|
|
129
|
+
expect(result?.success).toBe(true);
|
|
130
|
+
expect(result?.message).toBe(
|
|
131
|
+
'Permission mode: default\n\nRules: none configured.\n\nNo session-approved tools.\n\nRecent denials: none.',
|
|
132
|
+
);
|
|
133
|
+
expect(result?.data).toEqual({ mode: 'default', sessionAllowed: [] });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('reports session-approved tools when present', async () => {
|
|
137
|
+
const executor = new SystemCommandExecutor([
|
|
138
|
+
...(createPermissionsCommandModule().systemCommands ?? []),
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
const result = await executor.execute(
|
|
142
|
+
'permissions',
|
|
143
|
+
createCommandHostContext({ mode: 'acceptEdits', sessionAllowed: ['Bash', 'Read'] }),
|
|
144
|
+
'',
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(result?.success).toBe(true);
|
|
148
|
+
expect(result?.message).toContain('Approved this session ("allow always"): Bash, Read');
|
|
149
|
+
expect(result?.data).toEqual({ mode: 'acceptEdits', sessionAllowed: ['Bash', 'Read'] });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('lists the effective rules under the settings file each comes from, and recent denials (issue #3082)', async () => {
|
|
153
|
+
const executor = new SystemCommandExecutor([
|
|
154
|
+
...(createPermissionsCommandModule().systemCommands ?? []),
|
|
155
|
+
]);
|
|
156
|
+
const at = new Date(2026, 8, 25, 9, 5, 7).getTime();
|
|
157
|
+
const result = await executor.execute(
|
|
158
|
+
'permissions',
|
|
159
|
+
createCommandHostContext({
|
|
160
|
+
rules: { allow: ['Read', 'mcp__docs__*'], deny: ['Bash(rm *)'], ask: ['Bash(git push*)'] },
|
|
161
|
+
layers: [
|
|
162
|
+
{
|
|
163
|
+
source: '~/.fixture-state/settings.json',
|
|
164
|
+
scope: 'user',
|
|
165
|
+
allow: ['Read'],
|
|
166
|
+
deny: ['Bash(rm *)'],
|
|
167
|
+
ask: [],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
source: '.fixture-state/settings.json',
|
|
171
|
+
scope: 'project',
|
|
172
|
+
allow: ['Read', 'Stale'],
|
|
173
|
+
deny: [],
|
|
174
|
+
ask: ['Bash(git push*)'],
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
denials: [{ toolName: 'Bash', argument: 'rm -rf ~', reason: 'policy', at }],
|
|
178
|
+
}),
|
|
179
|
+
'',
|
|
180
|
+
);
|
|
181
|
+
expect(result?.message).toContain(
|
|
182
|
+
[
|
|
183
|
+
'Rules (checked deny, then ask, then allow):',
|
|
184
|
+
' ~/.fixture-state/settings.json [user]',
|
|
185
|
+
' deny: Bash(rm *)',
|
|
186
|
+
' allow: Read',
|
|
187
|
+
' .fixture-state/settings.json [project]',
|
|
188
|
+
' ask: Bash(git push*)',
|
|
189
|
+
' allow: Read',
|
|
190
|
+
' this session (CLI flags, preset, commands)',
|
|
191
|
+
' allow: mcp__docs__*',
|
|
192
|
+
].join('\n'),
|
|
193
|
+
);
|
|
194
|
+
// A layer's rule the session does not enforce is not listed as if it were.
|
|
195
|
+
expect(result?.message).not.toContain('Stale');
|
|
196
|
+
expect(result?.message).toContain(
|
|
197
|
+
'Recent denials (most recent first):\n 1. 09:05:07 Bash(rm -rf ~) — denied by a rule or the mode',
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('updates valid permission modes through the SDK command adapter', async () => {
|
|
202
|
+
const executor = new SystemCommandExecutor([
|
|
203
|
+
...(createPermissionsCommandModule().systemCommands ?? []),
|
|
204
|
+
]);
|
|
205
|
+
const context = createCommandHostContext();
|
|
206
|
+
|
|
207
|
+
const result = await executor.execute('permissions', context, 'plan');
|
|
208
|
+
|
|
209
|
+
expect(result?.success).toBe(true);
|
|
210
|
+
expect(result?.message).toMatch(/^Permission mode set to: plan\nPermission mode: plan\n/);
|
|
211
|
+
expect(result?.data).toEqual({ mode: 'plan', sessionAllowed: [] });
|
|
212
|
+
expect(context.setPermissionMode).toHaveBeenCalledWith('plan');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('rejects invalid permission modes without writing state', async () => {
|
|
216
|
+
const executor = new SystemCommandExecutor([
|
|
217
|
+
...(createPermissionsCommandModule().systemCommands ?? []),
|
|
218
|
+
]);
|
|
219
|
+
const context = createCommandHostContext();
|
|
220
|
+
|
|
221
|
+
const result = await executor.execute('permissions', context, 'invalid');
|
|
222
|
+
|
|
223
|
+
expect(result?.success).toBe(false);
|
|
224
|
+
expect(result?.message).toBe(
|
|
225
|
+
'Invalid mode. Valid: plan | default | acceptEdits | bypassPermissions | auto',
|
|
226
|
+
);
|
|
227
|
+
expect(context.setPermissionMode).not.toHaveBeenCalled();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('reports a mode the session refuses instead of throwing', async () => {
|
|
231
|
+
const executor = new SystemCommandExecutor([
|
|
232
|
+
...(createPermissionsCommandModule().systemCommands ?? []),
|
|
233
|
+
]);
|
|
234
|
+
const context = createCommandHostContext();
|
|
235
|
+
context.setPermissionMode.mockImplementation(() => {
|
|
236
|
+
throw new Error('Auto mode is unavailable: this session has no permission classifier.');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const result = await executor.execute('permissions', context, 'auto');
|
|
240
|
+
|
|
241
|
+
expect(result?.success).toBe(false);
|
|
242
|
+
expect(result?.message).toBe(
|
|
243
|
+
'Auto mode is unavailable: this session has no permission classifier.',
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('retries a classifier denial by the number /permissions shows (issue #3082)', async () => {
|
|
248
|
+
const executor = new SystemCommandExecutor([
|
|
249
|
+
...(createPermissionsCommandModule().systemCommands ?? []),
|
|
250
|
+
]);
|
|
251
|
+
const denial = {
|
|
252
|
+
toolName: 'Bash',
|
|
253
|
+
argument: 'npm publish',
|
|
254
|
+
reason: 'classifier' as const,
|
|
255
|
+
detail: 'publishes a package',
|
|
256
|
+
at: new Date(2026, 8, 25, 13, 4, 5).getTime(),
|
|
257
|
+
};
|
|
258
|
+
const retryDenial = vi.fn((index: number) => (index === 0 ? denial : undefined));
|
|
259
|
+
const context = createCommandHostContext({ denials: [denial], retryDenial });
|
|
260
|
+
|
|
261
|
+
const listed = await executor.execute('permissions', context, '');
|
|
262
|
+
expect(listed?.message).toContain(
|
|
263
|
+
'1. 13:04:05 Bash(npm publish) — blocked by the auto-mode classifier: publishes a package',
|
|
264
|
+
);
|
|
265
|
+
expect(listed?.message).toContain('/permissions retry <n>');
|
|
266
|
+
|
|
267
|
+
const retried = await executor.execute('permissions', context, 'retry 1');
|
|
268
|
+
expect(retried?.success).toBe(true);
|
|
269
|
+
expect(retried?.message).toContain('Bash(npm publish) will run once');
|
|
270
|
+
expect(retryDenial).toHaveBeenCalledWith(0);
|
|
271
|
+
|
|
272
|
+
const missing = await executor.execute('permissions', context, 'retry 2');
|
|
273
|
+
expect(missing?.success).toBe(false);
|
|
274
|
+
expect(missing?.message).toMatch(/^Usage: \/permissions retry <n>/);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildPermissionModeSubcommands,
|
|
3
|
+
PERMISSION_MODE_ARGUMENT_HINT,
|
|
4
|
+
PERMISSIONS_COMMAND_DESCRIPTION,
|
|
5
|
+
} from '@robota-sdk/agent-framework';
|
|
6
|
+
|
|
7
|
+
import { executePermissionsCommand } from './permissions-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
|
+
export function createPermissionsCommandEntry(): ICommand {
|
|
13
|
+
return {
|
|
14
|
+
name: 'permissions',
|
|
15
|
+
displayName: 'Permissions',
|
|
16
|
+
description: PERMISSIONS_COMMAND_DESCRIPTION,
|
|
17
|
+
source: 'permissions',
|
|
18
|
+
argumentHint: PERMISSION_MODE_ARGUMENT_HINT,
|
|
19
|
+
subcommands: buildPermissionModeSubcommands('permissions'),
|
|
20
|
+
// User-only: edits permission rules; permission widening is the user's.
|
|
21
|
+
modelInvocable: false,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createPermissionsSystemCommand(): ISystemCommand {
|
|
26
|
+
const entry = createPermissionsCommandEntry();
|
|
27
|
+
return {
|
|
28
|
+
name: entry.name,
|
|
29
|
+
displayName: entry.displayName,
|
|
30
|
+
description: entry.description,
|
|
31
|
+
requiresPermission: true,
|
|
32
|
+
userInvocable: true,
|
|
33
|
+
modelInvocable: false,
|
|
34
|
+
argumentHint: entry.argumentHint,
|
|
35
|
+
subcommands: entry.subcommands,
|
|
36
|
+
lifecycle: 'inline',
|
|
37
|
+
execute: executePermissionsCommand,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class PermissionsCommandSource implements ICommandSource {
|
|
42
|
+
readonly name = 'permissions';
|
|
43
|
+
|
|
44
|
+
getCommands(): ICommand[] {
|
|
45
|
+
return [createPermissionsCommandEntry()];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createPermissionsCommandModule(): ICommandModule {
|
|
50
|
+
return {
|
|
51
|
+
name: 'agent-command-permissions',
|
|
52
|
+
commandSources: [new PermissionsCommandSource()],
|
|
53
|
+
systemCommands: [createPermissionsSystemCommand()],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatCommandPermissionsMessage,
|
|
3
|
+
formatInvalidPermissionModeMessage,
|
|
4
|
+
isPermissionMode,
|
|
5
|
+
parsePermissionModeArgument,
|
|
6
|
+
readCommandPermissionsState,
|
|
7
|
+
retryCommandPermissionDenial,
|
|
8
|
+
writeCommandPermissionMode,
|
|
9
|
+
} from '@robota-sdk/agent-framework';
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
ICommandHostAdapterAccess,
|
|
13
|
+
ICommandHostSessionAccess,
|
|
14
|
+
} from '@robota-sdk/agent-framework';
|
|
15
|
+
import type { TPermissionMode } from '@robota-sdk/agent-core';
|
|
16
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
17
|
+
|
|
18
|
+
/** Switch the mode, or the refusal to show when the session will not enter it. */
|
|
19
|
+
export function tryWritePermissionMode(
|
|
20
|
+
context: ICommandHostAdapterAccess & ICommandHostSessionAccess,
|
|
21
|
+
mode: TPermissionMode,
|
|
22
|
+
): ICommandResult | undefined {
|
|
23
|
+
try {
|
|
24
|
+
writeCommandPermissionMode(context, mode);
|
|
25
|
+
return undefined;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return { message: error instanceof Error ? error.message : String(error), success: false };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function executeRetry(
|
|
32
|
+
context: ICommandHostAdapterAccess & ICommandHostSessionAccess,
|
|
33
|
+
args: string,
|
|
34
|
+
): ICommandResult {
|
|
35
|
+
const position = Number(args.trim().split(/\s+/)[1]);
|
|
36
|
+
const denial = retryCommandPermissionDenial(context, position);
|
|
37
|
+
if (denial === undefined) {
|
|
38
|
+
return {
|
|
39
|
+
message:
|
|
40
|
+
'Usage: /permissions retry <n>, where <n> numbers a call blocked by the auto-mode ' +
|
|
41
|
+
'classifier in /permissions.',
|
|
42
|
+
success: false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const call =
|
|
46
|
+
denial.argument !== undefined ? `${denial.toolName}(${denial.argument})` : denial.toolName;
|
|
47
|
+
return {
|
|
48
|
+
message: `${call} will run once, without the classifier, when the model tries it again.`,
|
|
49
|
+
success: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function executePermissionsCommand(
|
|
54
|
+
context: ICommandHostAdapterAccess & ICommandHostSessionAccess,
|
|
55
|
+
args: string,
|
|
56
|
+
): ICommandResult {
|
|
57
|
+
const arg = parsePermissionModeArgument(args);
|
|
58
|
+
if (arg === 'retry') return executeRetry(context, args);
|
|
59
|
+
if (arg !== undefined) {
|
|
60
|
+
if (!isPermissionMode(arg)) {
|
|
61
|
+
return {
|
|
62
|
+
message: formatInvalidPermissionModeMessage(),
|
|
63
|
+
success: false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const refused = tryWritePermissionMode(context, arg);
|
|
68
|
+
if (refused !== undefined) return refused;
|
|
69
|
+
const state = readCommandPermissionsState(context);
|
|
70
|
+
return {
|
|
71
|
+
message: `Permission mode set to: ${arg}\n${formatCommandPermissionsMessage(state)}`,
|
|
72
|
+
success: true,
|
|
73
|
+
data: {
|
|
74
|
+
mode: state.mode,
|
|
75
|
+
sessionAllowed: state.sessionAllowed,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const state = readCommandPermissionsState(context);
|
|
81
|
+
return {
|
|
82
|
+
message: formatCommandPermissionsMessage(state),
|
|
83
|
+
success: true,
|
|
84
|
+
data: {
|
|
85
|
+
mode: state.mode,
|
|
86
|
+
sessionAllowed: state.sessionAllowed,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|