@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
|
+
/**
|
|
2
|
+
* What `/devices` needs from the host that keeps this device's identity.
|
|
3
|
+
*
|
|
4
|
+
* The host owns every secret: the recovery phrase is shown and read on the host's own terminal, and
|
|
5
|
+
* private keys live in its credential store. What crosses this port back to the command is ids,
|
|
6
|
+
* names, times and closed refusal reasons — nothing the command formats can carry a secret, because
|
|
7
|
+
* the command never receives one. A thrown error's message is the host's promise to the same effect.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface IDeviceListEntry {
|
|
11
|
+
readonly deviceId: string;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly thisDevice: boolean;
|
|
14
|
+
/** Whether this entry is known to hold the device-signing key. Only known for this device. */
|
|
15
|
+
readonly holdsSigningKey: boolean;
|
|
16
|
+
readonly certificateExpiresAt: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IDevicesView {
|
|
20
|
+
readonly userId: string;
|
|
21
|
+
readonly devices: readonly IDeviceListEntry[];
|
|
22
|
+
readonly revokedDeviceCount: number;
|
|
23
|
+
/** When the roster and revocation list this device holds stop being accepted. */
|
|
24
|
+
readonly listsExpireAt: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Why an operation did not happen. Closed, so no free text from the secret path reaches a result. */
|
|
28
|
+
export type TDevicesRefusal =
|
|
29
|
+
/** No interactive terminal: the phrase is never shown on or read from anything else. */
|
|
30
|
+
| 'no-terminal'
|
|
31
|
+
| 'not-initialized'
|
|
32
|
+
| 'already-initialized'
|
|
33
|
+
/** The operator cancelled at the terminal. */
|
|
34
|
+
| 'cancelled'
|
|
35
|
+
/** The re-typed words or passphrase did not match. */
|
|
36
|
+
| 'confirmation-failed'
|
|
37
|
+
| 'phrase-invalid'
|
|
38
|
+
/** The phrase is valid but derives another identity than this device's. */
|
|
39
|
+
| 'phrase-mismatch'
|
|
40
|
+
| 'no-signing-key'
|
|
41
|
+
| 'signing-key-expired'
|
|
42
|
+
| 'unknown-device'
|
|
43
|
+
| 'ambiguous-device'
|
|
44
|
+
| 'self-revocation'
|
|
45
|
+
/** Another process changed the identity while this operation waited on the operator. */
|
|
46
|
+
| 'changed-concurrently'
|
|
47
|
+
/** No signaling relay is configured to meet the other device through. */
|
|
48
|
+
| 'no-relay'
|
|
49
|
+
/** What was typed is not an enrollment code. */
|
|
50
|
+
| 'code-invalid'
|
|
51
|
+
/** The code was typed on the command line, where history keeps it. */
|
|
52
|
+
| 'code-on-command-line'
|
|
53
|
+
/** No device waits for that code or proved it: wrong, expired, already used, or something in between. */
|
|
54
|
+
| 'code-not-accepted'
|
|
55
|
+
/** Nobody joined before the code expired. */
|
|
56
|
+
| 'enrollment-expired'
|
|
57
|
+
/** Too many attempts failed to prove the code; it no longer works. */
|
|
58
|
+
| 'too-many-attempts'
|
|
59
|
+
/** The operator of either device declined. */
|
|
60
|
+
| 'enrollment-declined'
|
|
61
|
+
/** An operator did not answer in time. */
|
|
62
|
+
| 'enrollment-timed-out'
|
|
63
|
+
/** The connection or the exchange with the other device failed partway. */
|
|
64
|
+
| 'enrollment-failed';
|
|
65
|
+
|
|
66
|
+
export type TDevicesOutcome<T> =
|
|
67
|
+
| { readonly ok: true; readonly value: T }
|
|
68
|
+
| { readonly ok: false; readonly reason: TDevicesRefusal };
|
|
69
|
+
|
|
70
|
+
export interface IDevicesInitResult {
|
|
71
|
+
readonly userId: string;
|
|
72
|
+
readonly deviceId: string;
|
|
73
|
+
readonly signingKeyId: string;
|
|
74
|
+
/** Where the private keys were kept, for the operator. */
|
|
75
|
+
readonly keyStorage?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface IDevicesRecoverResult {
|
|
79
|
+
readonly deviceId: string;
|
|
80
|
+
readonly signingKeyId: string;
|
|
81
|
+
readonly revokedSigningKeyCount: number;
|
|
82
|
+
/** Other devices that were certified by a retired signing key and must enrol again. */
|
|
83
|
+
readonly droppedDeviceCount: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface IDevicesRevokeResult {
|
|
87
|
+
readonly deviceId: string;
|
|
88
|
+
readonly name: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** This session's endpoint in the device mesh. */
|
|
92
|
+
export interface IDevicesMeshStatus {
|
|
93
|
+
/** `off`: the setting is off, or this run does not open the mesh. `failed`: it could not open. */
|
|
94
|
+
readonly state: 'off' | 'starting' | 'on' | 'failed';
|
|
95
|
+
/** Why it could not open, for `failed`. */
|
|
96
|
+
readonly reason?: string;
|
|
97
|
+
/** How this device looks for the others, in the operator's words. */
|
|
98
|
+
readonly sources: readonly string[];
|
|
99
|
+
/** The devices with an admitted link now. */
|
|
100
|
+
readonly linked: readonly {
|
|
101
|
+
readonly deviceId: string;
|
|
102
|
+
readonly name?: string;
|
|
103
|
+
readonly locality: 'same-host' | 'another-host';
|
|
104
|
+
}[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface IDevicesAddResult {
|
|
108
|
+
readonly deviceId: string;
|
|
109
|
+
readonly name: string;
|
|
110
|
+
/** Whether the new device said it kept what it was given. */
|
|
111
|
+
readonly confirmed: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface IDevicesJoinResult {
|
|
115
|
+
readonly userId: string;
|
|
116
|
+
readonly deviceId: string;
|
|
117
|
+
readonly name: string;
|
|
118
|
+
/** Where the private keys were kept, for the operator. */
|
|
119
|
+
readonly keyStorage?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface IDevicesCommandPort {
|
|
123
|
+
/** This device's view of the roster, or `undefined` when it has no identity yet. */
|
|
124
|
+
list(): Promise<IDevicesView | undefined>;
|
|
125
|
+
/** The device mesh as this session runs it. Absent on a host with no device mesh. */
|
|
126
|
+
meshStatus?(): IDevicesMeshStatus;
|
|
127
|
+
/** Create the identity. Runs on the operator terminal (it shows the phrase once). */
|
|
128
|
+
init(options: { readonly name?: string }): Promise<TDevicesOutcome<IDevicesInitResult>>;
|
|
129
|
+
/** Rotate the signing key from the phrase. Runs on the operator terminal. */
|
|
130
|
+
recover(): Promise<TDevicesOutcome<IDevicesRecoverResult>>;
|
|
131
|
+
/** Revoke a device by id prefix with the signing key. Confirms on the operator terminal. */
|
|
132
|
+
revoke(deviceIdPrefix: string): Promise<TDevicesOutcome<IDevicesRevokeResult>>;
|
|
133
|
+
/**
|
|
134
|
+
* Enrol another device with the signing key: show a one-time code, and certify the device that
|
|
135
|
+
* proves it once both operators confirm the string both devices show. Runs on the operator terminal.
|
|
136
|
+
*/
|
|
137
|
+
add(): Promise<TDevicesOutcome<IDevicesAddResult>>;
|
|
138
|
+
/**
|
|
139
|
+
* Join the user's devices with a code another device shows, once both operators confirm the string
|
|
140
|
+
* both devices show. Runs on the operator terminal.
|
|
141
|
+
*/
|
|
142
|
+
join(options: { readonly name?: string }): Promise<TDevicesOutcome<IDevicesJoinResult>>;
|
|
143
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { createDevicesCommandModule } from './devices-command-module.js';
|
|
2
|
+
export type {
|
|
3
|
+
IDeviceListEntry,
|
|
4
|
+
IDevicesCommandPort,
|
|
5
|
+
IDevicesInitResult,
|
|
6
|
+
IDevicesMeshStatus,
|
|
7
|
+
IDevicesRecoverResult,
|
|
8
|
+
IDevicesAddResult,
|
|
9
|
+
IDevicesJoinResult,
|
|
10
|
+
IDevicesRevokeResult,
|
|
11
|
+
IDevicesView,
|
|
12
|
+
TDevicesOutcome,
|
|
13
|
+
TDevicesRefusal,
|
|
14
|
+
} from './devices-command-port.js';
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { createDoctorCommandModule } from '../doctor-command-module.js';
|
|
7
|
+
import {
|
|
8
|
+
createDoctorFixture,
|
|
9
|
+
fixtureProviderDefinition,
|
|
10
|
+
installBrokenHome,
|
|
11
|
+
} from './doctor-fixture.js';
|
|
12
|
+
|
|
13
|
+
import type { IDoctorFixture } from './doctor-fixture.js';
|
|
14
|
+
import { CommandRegistry, SystemCommandExecutor } from '@robota-sdk/agent-framework';
|
|
15
|
+
import { createTestCommandHost } from '@robota-sdk/agent-framework/testing';
|
|
16
|
+
|
|
17
|
+
import type { IUserInteraction, TActionResponse } from '@robota-sdk/agent-core';
|
|
18
|
+
|
|
19
|
+
const fixtures: IDoctorFixture[] = [];
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
for (const fixture of fixtures.splice(0)) fixture.cleanup();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/** The published conformant double, with only the interaction port overridden. */
|
|
25
|
+
function contextWith(interaction: IUserInteraction | undefined) {
|
|
26
|
+
return createTestCommandHost({ overrides: { getUserInteraction: () => interaction } });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function answering(
|
|
30
|
+
response: TActionResponse,
|
|
31
|
+
): IUserInteraction & { ask: ReturnType<typeof vi.fn> } {
|
|
32
|
+
return { ask: vi.fn(async () => response) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('/doctor command module (OBSERVABILITY-1991 TC-06)', () => {
|
|
36
|
+
it('advertises repair arguments consistently in the palette and executable metadata', () => {
|
|
37
|
+
const fixture = createDoctorFixture({ env: {} });
|
|
38
|
+
fixtures.push(fixture);
|
|
39
|
+
const module = createDoctorCommandModule(fixture.inputs, fixture.deps);
|
|
40
|
+
const registry = new CommandRegistry();
|
|
41
|
+
registry.addModule(module);
|
|
42
|
+
const executor = new SystemCommandExecutor([...(module.systemCommands ?? [])]);
|
|
43
|
+
const entry = registry.getCommands().find((command) => command.name === 'doctor');
|
|
44
|
+
const command = executor.getCommand('doctor');
|
|
45
|
+
expect(entry).toMatchObject({ argumentHint: '[repair <check-id>]', modelInvocable: false });
|
|
46
|
+
expect(command).toMatchObject({
|
|
47
|
+
argumentHint: '[repair <check-id>]',
|
|
48
|
+
modelInvocable: false,
|
|
49
|
+
userInvocable: true,
|
|
50
|
+
requiresPermission: false,
|
|
51
|
+
lifecycle: 'inline',
|
|
52
|
+
});
|
|
53
|
+
expect(registry.getCapabilityDescriptors()).toContainEqual(
|
|
54
|
+
expect.objectContaining({
|
|
55
|
+
name: 'doctor',
|
|
56
|
+
argumentHint: '[repair <check-id>]',
|
|
57
|
+
userInvocable: true,
|
|
58
|
+
modelInvocable: false,
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
expect(command?.description).toBe(entry?.description);
|
|
62
|
+
expect(executor.isModelInvocable('doctor')).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('renders the same check set without constructing a provider or submitting input', async () => {
|
|
66
|
+
const createProvider = vi.fn(() => {
|
|
67
|
+
throw new Error('must not be called');
|
|
68
|
+
});
|
|
69
|
+
const fixture = createDoctorFixture({
|
|
70
|
+
env: {},
|
|
71
|
+
providerDefinitions: [fixtureProviderDefinition({ createProvider })],
|
|
72
|
+
});
|
|
73
|
+
fixtures.push(fixture);
|
|
74
|
+
installBrokenHome(fixture);
|
|
75
|
+
const module = createDoctorCommandModule(fixture.inputs, fixture.deps);
|
|
76
|
+
const command = module.systemCommands?.[0];
|
|
77
|
+
expect(command?.name).toBe('doctor');
|
|
78
|
+
const result = await command!.execute(contextWith(undefined), '');
|
|
79
|
+
expect(result.success).toBe(false);
|
|
80
|
+
expect(result.message).toContain('settings.user.fixture-product');
|
|
81
|
+
expect(result.message).toContain('plugin.broken-plugin@fixture-market');
|
|
82
|
+
expect(result.data).toMatchObject({
|
|
83
|
+
failCount: expect.any(Number),
|
|
84
|
+
repairable: ['settings.user.fixture-product'],
|
|
85
|
+
});
|
|
86
|
+
expect(createProvider).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('renders host-selected product wording in the interactive command', async () => {
|
|
90
|
+
const fixture = createDoctorFixture({ env: {} });
|
|
91
|
+
fixtures.push(fixture);
|
|
92
|
+
installBrokenHome(fixture);
|
|
93
|
+
const command = createDoctorCommandModule(fixture.inputs, fixture.deps, {
|
|
94
|
+
title: 'Atlas doctor',
|
|
95
|
+
productName: 'Atlas',
|
|
96
|
+
formatRepairCommand: (id) => `atlas doctor --repair ${id}`,
|
|
97
|
+
repairOffer: 'run atlas doctor --repair <check-id>',
|
|
98
|
+
}).systemCommands![0]!;
|
|
99
|
+
|
|
100
|
+
const result = await command.execute(contextWith(undefined), '');
|
|
101
|
+
expect(result.message).toContain('Atlas doctor');
|
|
102
|
+
expect(result.message).toContain('Atlas');
|
|
103
|
+
expect(result.message).toContain('atlas doctor --repair settings.user.fixture-product');
|
|
104
|
+
expect(result.message).toContain('run atlas doctor --repair <check-id>');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('repair without an interaction port or with a cancelled answer writes nothing', async () => {
|
|
108
|
+
const fixture = createDoctorFixture({ env: {} });
|
|
109
|
+
fixtures.push(fixture);
|
|
110
|
+
installBrokenHome(fixture);
|
|
111
|
+
const path = join(fixture.home, '.fixture-product', 'settings.json');
|
|
112
|
+
const command = createDoctorCommandModule(fixture.inputs, fixture.deps).systemCommands![0]!;
|
|
113
|
+
|
|
114
|
+
const noPort = await command.execute(contextWith(undefined), 'repair settings.user.fixture-product');
|
|
115
|
+
expect(noPort.success).toBe(false);
|
|
116
|
+
expect(readFileSync(path, 'utf8')).toBe('');
|
|
117
|
+
|
|
118
|
+
const cancelled = answering({ type: 'cancelled' });
|
|
119
|
+
const declined = await command.execute(contextWith(cancelled), 'repair settings.user.fixture-product');
|
|
120
|
+
expect(declined.success).toBe(false);
|
|
121
|
+
expect(cancelled.ask).toHaveBeenCalledTimes(1);
|
|
122
|
+
expect(readFileSync(path, 'utf8')).toBe('');
|
|
123
|
+
|
|
124
|
+
const usage = await command.execute(contextWith(cancelled), 'repair');
|
|
125
|
+
expect(usage.message).toContain('Usage');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('repair with a confirmed answer applies the allowlisted writer and re-renders', async () => {
|
|
129
|
+
const fixture = createDoctorFixture({ env: {} });
|
|
130
|
+
fixtures.push(fixture);
|
|
131
|
+
installBrokenHome(fixture);
|
|
132
|
+
const path = join(fixture.home, '.fixture-product', 'settings.json');
|
|
133
|
+
const command = createDoctorCommandModule(fixture.inputs, fixture.deps).systemCommands![0]!;
|
|
134
|
+
const yes = answering({ type: 'answer', values: ['yes'] });
|
|
135
|
+
const result = await command.execute(contextWith(yes), 'repair settings.user.fixture-product');
|
|
136
|
+
expect(result.success).toBe(true);
|
|
137
|
+
expect(JSON.parse(readFileSync(path, 'utf8'))).toEqual({});
|
|
138
|
+
expect(result.data).toMatchObject({
|
|
139
|
+
repair: 'settings.user.fixture-product',
|
|
140
|
+
applied: true,
|
|
141
|
+
status: 'ok',
|
|
142
|
+
});
|
|
143
|
+
expect(yes.ask.mock.calls[0]?.[0]).toMatchObject({ id: 'doctor-repair:settings.user.fixture-product' });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { renderDoctorReport } from '../doctor-render.js';
|
|
4
|
+
|
|
5
|
+
import type { IDoctorReport } from '../doctor-types.js';
|
|
6
|
+
|
|
7
|
+
const REPORT: IDoctorReport = {
|
|
8
|
+
checks: [{ id: 'storage.user', label: 'Storage', status: 'warn', repair: 'storage.user' }],
|
|
9
|
+
failCount: 0,
|
|
10
|
+
warnCount: 1,
|
|
11
|
+
repairable: ['storage.user'],
|
|
12
|
+
exitCode: 0,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('doctor display vocabulary', () => {
|
|
16
|
+
it('uses neutral report and repair text without a host vocabulary', () => {
|
|
17
|
+
const text = renderDoctorReport(REPORT).join('\n');
|
|
18
|
+
expect(text).not.toContain('product name');
|
|
19
|
+
expect(text).not.toContain('--repair');
|
|
20
|
+
expect(text).toContain('storage.user');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('uses the host-selected title, product name, and repair command', () => {
|
|
24
|
+
const text = renderDoctorReport(REPORT, 'Atlas doctor', {
|
|
25
|
+
productName: 'Atlas',
|
|
26
|
+
formatRepairCommand: (id) => `atlas doctor --repair ${id}`,
|
|
27
|
+
repairOffer: 'run atlas doctor --repair <check-id>',
|
|
28
|
+
}).join('\n');
|
|
29
|
+
expect(text).toContain('Atlas doctor');
|
|
30
|
+
expect(text).toContain('atlas doctor --repair storage.user');
|
|
31
|
+
expect(text).toContain('Atlas may work');
|
|
32
|
+
expect(text).toContain('run atlas doctor --repair <check-id>');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('preserves the public string title argument', () => {
|
|
36
|
+
expect(renderDoctorReport(REPORT, 'Atlas doctor')).toContain('Atlas doctor');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createNodeHostContributionSource,
|
|
7
|
+
createNodeHostSettingsSource,
|
|
8
|
+
createRestrictedWorkspaceProjectAccess,
|
|
9
|
+
} from '@robota-sdk/agent-framework';
|
|
10
|
+
|
|
11
|
+
import { inspectPathFacts } from '../doctor-node-deps.js';
|
|
12
|
+
|
|
13
|
+
import type { IDoctorDeps, IDoctorInputs } from '../doctor-types.js';
|
|
14
|
+
import type { IProviderDefinition } from '@robota-sdk/agent-core';
|
|
15
|
+
|
|
16
|
+
export const MARKERS = {
|
|
17
|
+
env: 'sk-doctor-marker-env-1a2b3c',
|
|
18
|
+
profile: 'sk-doctor-marker-profile-4d5e6f',
|
|
19
|
+
mcp: 'sk-doctor-marker-mcp-9f8e7d',
|
|
20
|
+
adjacent: 'sk-doctor-marker-adjacent-7c8d9e',
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export interface IDoctorFixture {
|
|
24
|
+
readonly home: string;
|
|
25
|
+
readonly inputs: IDoctorInputs;
|
|
26
|
+
readonly deps: IDoctorDeps;
|
|
27
|
+
readonly probes: { host: string; port: number }[];
|
|
28
|
+
write(relative: string, content: string): string;
|
|
29
|
+
mkdir(relative: string, mode?: number): string;
|
|
30
|
+
cleanup(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A provider definition whose SDK-embedded host is declared through `endpoint`, like anthropic. */
|
|
34
|
+
export function fixtureProviderDefinition(
|
|
35
|
+
overrides: Partial<IProviderDefinition> = {},
|
|
36
|
+
): IProviderDefinition {
|
|
37
|
+
return {
|
|
38
|
+
type: 'fixture',
|
|
39
|
+
displayName: 'Fixture',
|
|
40
|
+
defaults: { model: 'fixture-model', apiKey: '$ENV:FIXTURE_DOCTOR_API_KEY' },
|
|
41
|
+
endpoint: { host: 'api.fixture.example', port: 443 },
|
|
42
|
+
createProvider: () => {
|
|
43
|
+
throw new Error('the doctor must never construct a provider');
|
|
44
|
+
},
|
|
45
|
+
...overrides,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface IDoctorFixtureOptions {
|
|
50
|
+
env?: Record<string, string | undefined>;
|
|
51
|
+
providerDefinitions?: readonly IProviderDefinition[];
|
|
52
|
+
reachable?: boolean;
|
|
53
|
+
resolvable?: readonly string[];
|
|
54
|
+
guarantee?: 'posix-mode' | 'windows-acl';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fixtureDeps(
|
|
58
|
+
options: IDoctorFixtureOptions,
|
|
59
|
+
probes: { host: string; port: number }[],
|
|
60
|
+
): IDoctorDeps {
|
|
61
|
+
const resolvable = new Set(options.resolvable ?? []);
|
|
62
|
+
return {
|
|
63
|
+
probeEndpoint: async (host, port) => {
|
|
64
|
+
probes.push({ host, port });
|
|
65
|
+
return options.reachable === false
|
|
66
|
+
? { reachable: false, error: 'connect ECONNREFUSED' }
|
|
67
|
+
: { reachable: true, elapsedMs: 1 };
|
|
68
|
+
},
|
|
69
|
+
// Real filesystem facts, so the storage probe is exercised against the fixture HOME.
|
|
70
|
+
inspectPath: inspectPathFacts,
|
|
71
|
+
resolveCommand: (command) => resolvable.has(command.split(/\s+/)[0] ?? command),
|
|
72
|
+
ownerOnlyGuarantee: () => options.guarantee ?? 'posix-mode',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createDoctorFixture(options: IDoctorFixtureOptions = {}): IDoctorFixture {
|
|
77
|
+
const home = mkdtempSync(join(tmpdir(), 'agent-doctor-fixture-'));
|
|
78
|
+
const write = (relative: string, content: string): string => {
|
|
79
|
+
const path = join(home, relative);
|
|
80
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
81
|
+
writeFileSync(path, content, 'utf8');
|
|
82
|
+
return path;
|
|
83
|
+
};
|
|
84
|
+
const mkdir = (relative: string, mode = 0o700): string => {
|
|
85
|
+
const path = join(home, relative);
|
|
86
|
+
mkdirSync(path, { recursive: true });
|
|
87
|
+
chmodSync(path, mode);
|
|
88
|
+
return path;
|
|
89
|
+
};
|
|
90
|
+
const probes: { host: string; port: number }[] = [];
|
|
91
|
+
const env = options.env ?? {};
|
|
92
|
+
const inputs: IDoctorInputs = {
|
|
93
|
+
cwd: home,
|
|
94
|
+
userHome: home,
|
|
95
|
+
userSettingsPath: join(home, '.fixture-product', 'settings.json'),
|
|
96
|
+
userStorage: { root: join(home, '.fixture-product'), sessions: join(home, '.fixture-product', 'sessions') },
|
|
97
|
+
settingsSources: [
|
|
98
|
+
createNodeHostSettingsSource('user', join(home, '.fixture-product', 'settings.json')),
|
|
99
|
+
createNodeHostSettingsSource('user', join(home, '.claude', 'settings.json')),
|
|
100
|
+
],
|
|
101
|
+
projectAccess: createRestrictedWorkspaceProjectAccess('untrusted', home),
|
|
102
|
+
providerDefinitions: options.providerDefinitions ?? [fixtureProviderDefinition()],
|
|
103
|
+
env,
|
|
104
|
+
contributionSources: [createNodeHostContributionSource(home)],
|
|
105
|
+
skillRoots: [{ root: join('.fixture-product', 'skills'), kind: 'skills' }],
|
|
106
|
+
pluginsDirs: [join(home, '.fixture-product', 'plugins')],
|
|
107
|
+
hostChecks: [
|
|
108
|
+
{ id: 'host.fixture', label: 'Fixture host check', status: 'ok', cause: 'present' },
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
const deps = fixtureDeps(options, probes);
|
|
112
|
+
return {
|
|
113
|
+
home,
|
|
114
|
+
inputs,
|
|
115
|
+
deps,
|
|
116
|
+
probes,
|
|
117
|
+
write,
|
|
118
|
+
mkdir,
|
|
119
|
+
cleanup: () => rmSync(home, { recursive: true, force: true }),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The "broken HOME" of the built-CLI scenario, as a unit fixture. */
|
|
124
|
+
export function installBrokenHome(fixture: IDoctorFixture): void {
|
|
125
|
+
fixture.mkdir('.fixture-product');
|
|
126
|
+
fixture.mkdir('.fixture-product/sessions');
|
|
127
|
+
fixture.write('.fixture-product/settings.json', '');
|
|
128
|
+
fixture.write('.claude/settings.json', '{"defaultTrustLevel":42}');
|
|
129
|
+
fixture.write(
|
|
130
|
+
'.fixture-product/plugins/cache/fixture-market/broken-plugin/1.0.0/.claude-plugin/plugin.json',
|
|
131
|
+
'{',
|
|
132
|
+
);
|
|
133
|
+
fixture.write(
|
|
134
|
+
'.fixture-product/plugins/cache/fixture-market/mcp-plugin/1.0.0/.claude-plugin/plugin.json',
|
|
135
|
+
JSON.stringify({ name: 'mcp-plugin', version: '1.0.0', description: 'doctor fixture' }),
|
|
136
|
+
);
|
|
137
|
+
fixture.write(
|
|
138
|
+
'.fixture-product/plugins/cache/fixture-market/mcp-plugin/1.0.0/.mcp.json',
|
|
139
|
+
JSON.stringify({
|
|
140
|
+
mcpServers: {
|
|
141
|
+
ghost: { command: 'agent-test-doctor-missing-binary', env: { GHOST_TOKEN: MARKERS.mcp } },
|
|
142
|
+
ready: { type: 'http', url: 'https://mcp.example.test' },
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { redactDiagnosticText } from '../doctor-redaction.js';
|
|
4
|
+
|
|
5
|
+
describe('doctor redaction', () => {
|
|
6
|
+
it('still masks URL userinfo', () => {
|
|
7
|
+
expect(redactDiagnosticText('see https://user:hunter22@example.com/x')).toBe(
|
|
8
|
+
'see https://[redacted]@example.com/x',
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('stays linear on long word runs that never reach a URL scheme', () => {
|
|
13
|
+
const timeFor = (n: number): number => {
|
|
14
|
+
const text = `x-${'a-'.repeat(n / 2)}`;
|
|
15
|
+
let best = Number.POSITIVE_INFINITY;
|
|
16
|
+
for (let i = 0; i < 3; i += 1) {
|
|
17
|
+
const started = performance.now();
|
|
18
|
+
redactDiagnosticText(text);
|
|
19
|
+
best = Math.min(best, performance.now() - started);
|
|
20
|
+
}
|
|
21
|
+
return best;
|
|
22
|
+
};
|
|
23
|
+
const small = timeFor(16 * 1024);
|
|
24
|
+
const large = timeFor(64 * 1024);
|
|
25
|
+
// Linear growth is about 4x for 4x input; the old unbounded scheme run grew about 16x.
|
|
26
|
+
if (large >= 50) expect(large / Math.max(small, 1)).toBeLessThan(10);
|
|
27
|
+
expect(large).toBeLessThan(500);
|
|
28
|
+
});
|
|
29
|
+
});
|