@robota-sdk/agent-command 3.0.0-beta.86 → 3.0.0-beta.88
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 +64 -12
- package/README.md +6 -3
- package/dist/node/index.cjs +72 -70
- package/dist/node/index.d.cts +16 -8
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +16 -8
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +51 -49
- package/dist/node/index.js.map +1 -1
- package/package.json +15 -13
- package/src/agent/__tests__/agent-command.test.ts +5 -5
- package/src/agent/agent-command-module.ts +1 -1
- package/src/background/__tests__/background-command-module.test.ts +6 -6
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +15 -4
- package/src/default/__tests__/default-command-modules.test.ts +2 -2
- package/src/default/__tests__/model-exposure.test.ts +18 -5
- package/src/default/__tests__/org-policy-forwarding.test.ts +1 -1
- package/src/default/default-command-modules.ts +2 -2
- package/src/devices/devices-command-module.ts +1 -1
- package/src/doctor/__tests__/doctor-command-module.test.ts +10 -10
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +1 -1
- package/src/doctor/__tests__/doctor-fixture.ts +14 -14
- package/src/doctor/__tests__/doctor-runner.test.ts +57 -57
- package/src/doctor/doctor-storage-probe.ts +1 -1
- package/src/doctor/doctor-types.ts +2 -2
- package/src/editor/__tests__/editor-command-functional.test.ts +1 -1
- package/src/fork/__tests__/fork-command.test.ts +1 -1
- package/src/fork/fork-command-module.ts +1 -1
- package/src/git/__tests__/git-command-module.test.ts +1 -1
- package/src/handoff/__tests__/handoff-command-module.test.ts +1 -1
- package/src/handoff/handoff-command-module.ts +1 -1
- package/src/keybindings/keybindings-command-module.ts +1 -1
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +8 -8
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +43 -12
- package/src/mcp-activation/__tests__/mcp-skill-command.test.ts +141 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +27 -2
- package/src/mcp-activation/mcp-activation-command.ts +149 -15
- package/src/mcp-activation/mcp-model-notice.ts +19 -5
- package/src/mcp-activation/mcp-model-status.ts +7 -2
- package/src/memory/__tests__/memory-command-module.test.ts +89 -16
- package/src/memory/memory-command-module.ts +4 -1
- package/src/memory/memory-command.ts +53 -1
- package/src/peers/__tests__/peers-command-module.test.ts +1 -1
- package/src/peers/peers-command-module.ts +1 -1
- package/src/permissions/__tests__/permissions-command-module.test.ts +4 -4
- package/src/plugin/__tests__/plugin-active-turn.test.ts +100 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +7 -7
- package/src/plugin/plugin-command-module.ts +4 -0
- package/src/provider/provider-startup.ts +4 -2
- package/src/remote-control/__tests__/remote-control-command.test.ts +2 -2
- package/src/rewind/__tests__/rewind-command-module.test.ts +4 -3
- package/src/rewind/rewind-command.ts +27 -21
- package/src/schedule/schedule-command-module.ts +1 -1
- package/src/session/__tests__/session-command-module.test.ts +2 -2
- package/src/skills/__tests__/skill-read-command.test.ts +64 -0
- package/src/skills/__tests__/skills-command-module.test.ts +2 -1
- package/src/skills/skill-read-command.ts +60 -0
- package/src/skills/skills-command-module.ts +3 -2
- package/src/terminal-client/__tests__/terminal-client-commands.test.ts +1 -1
- package/src/theme/__tests__/theme-command.test.ts +1 -1
- package/src/theme/theme-command-module.ts +1 -1
- package/src/user-local/__tests__/user-local-command.test.ts +16 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robota-sdk/agent-command",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.88",
|
|
4
4
|
"description": "Slash-command modules for Robota SDK sessions (/help, /permissions, /mcp, /rewind, …)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"CHANGELOG.md"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@robota-sdk/agent-core": "3.0.0-beta.
|
|
44
|
-
"@robota-sdk/agent-framework": "3.0.0-beta.
|
|
45
|
-
"@robota-sdk/agent-interface-command": "3.0.0-beta.
|
|
46
|
-
"@robota-sdk/agent-interface-execution": "3.0.0-beta.
|
|
47
|
-
"@robota-sdk/agent-interface-session": "3.0.0-beta.
|
|
48
|
-
"@robota-sdk/agent-preset": "3.0.0-beta.
|
|
43
|
+
"@robota-sdk/agent-core": "3.0.0-beta.88",
|
|
44
|
+
"@robota-sdk/agent-framework": "3.0.0-beta.88",
|
|
45
|
+
"@robota-sdk/agent-interface-command": "3.0.0-beta.88",
|
|
46
|
+
"@robota-sdk/agent-interface-execution": "3.0.0-beta.88",
|
|
47
|
+
"@robota-sdk/agent-interface-session": "3.0.0-beta.88",
|
|
48
|
+
"@robota-sdk/agent-preset": "3.0.0-beta.88"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^22.19.21",
|
|
@@ -55,18 +55,20 @@
|
|
|
55
55
|
"vitest": "^3.2.6"
|
|
56
56
|
},
|
|
57
57
|
"license": "AGPL-3.0-only OR LicenseRef-Commercial",
|
|
58
|
-
"repository": {
|
|
59
|
-
"type": "git",
|
|
60
|
-
"url": "https://github.com/woojubb/robota.git",
|
|
61
|
-
"directory": "packages/agent-command"
|
|
62
|
-
},
|
|
63
|
-
"homepage": "https://docs.robota.io/en/packages/agent-command/",
|
|
64
58
|
"publishConfig": {
|
|
65
59
|
"access": "public"
|
|
66
60
|
},
|
|
67
61
|
"volta": {
|
|
68
62
|
"extends": "../../package.json"
|
|
69
63
|
},
|
|
64
|
+
"repository": {
|
|
65
|
+
"type": "git",
|
|
66
|
+
"url": "https://github.com/woojubb/robota"
|
|
67
|
+
},
|
|
68
|
+
"bugs": {
|
|
69
|
+
"url": "https://github.com/woojubb/robota/issues"
|
|
70
|
+
},
|
|
71
|
+
"homepage": "https://robota.io/",
|
|
70
72
|
"scripts": {
|
|
71
73
|
"build": "rimraf dist && tsdown",
|
|
72
74
|
"build:js": "pnpm run build",
|
|
@@ -105,7 +105,7 @@ describe('agent command module', () => {
|
|
|
105
105
|
it('requests agent runtime wiring through the command module contract', () => {
|
|
106
106
|
const module = createAgentCommandModule();
|
|
107
107
|
|
|
108
|
-
expect(module.sessionRequirements).toEqual(['
|
|
108
|
+
expect(module.sessionRequirements).toEqual(['runtime']);
|
|
109
109
|
expect(module.commandSources).toHaveLength(1);
|
|
110
110
|
expect(module.systemCommands).toHaveLength(1);
|
|
111
111
|
});
|
|
@@ -179,8 +179,8 @@ describe('agent command module', () => {
|
|
|
179
179
|
cwd: '/workspace',
|
|
180
180
|
promptPreview: 'change files',
|
|
181
181
|
updatedAt: '2026-05-01T00:00:00.000Z',
|
|
182
|
-
worktreePath: '/workspace/.
|
|
183
|
-
branchName: '
|
|
182
|
+
worktreePath: '/workspace/.fixture-state/worktrees/agent_1',
|
|
183
|
+
branchName: 'agent/worker_1',
|
|
184
184
|
},
|
|
185
185
|
]),
|
|
186
186
|
});
|
|
@@ -188,8 +188,8 @@ describe('agent command module', () => {
|
|
|
188
188
|
const result = await executor.execute('agent', session, 'list');
|
|
189
189
|
|
|
190
190
|
expect(result?.success).toBe(true);
|
|
191
|
-
expect(result?.message).toContain('worktree=/workspace/.
|
|
192
|
-
expect(result?.message).toContain('branch=
|
|
191
|
+
expect(result?.message).toContain('worktree=/workspace/.fixture-state/worktrees/agent_1');
|
|
192
|
+
expect(result?.message).toContain('branch=agent/worker_1');
|
|
193
193
|
});
|
|
194
194
|
|
|
195
195
|
it('spawns a background agent from direct natural-language /agent input', async () => {
|
|
@@ -84,6 +84,6 @@ export function createAgentCommandModule(): ICommandModule {
|
|
|
84
84
|
name: 'agent-command-agent',
|
|
85
85
|
commandSources: [new AgentCommandSource()],
|
|
86
86
|
systemCommands: [createAgentSystemCommand()],
|
|
87
|
-
sessionRequirements: ['
|
|
87
|
+
sessionRequirements: ['runtime'],
|
|
88
88
|
};
|
|
89
89
|
}
|
|
@@ -145,10 +145,10 @@ describe('background command module', () => {
|
|
|
145
145
|
listBackgroundTasks: vi.fn().mockReturnValue([
|
|
146
146
|
createTask({
|
|
147
147
|
status: 'completed',
|
|
148
|
-
worktreePath: '/workspace/.
|
|
149
|
-
branchName: '
|
|
148
|
+
worktreePath: '/workspace/.fixture-state/worktrees/agent_1',
|
|
149
|
+
branchName: 'agent/worker_1',
|
|
150
150
|
worktreeStatus: ' M changed.ts\n?? new.ts',
|
|
151
|
-
worktreeNextAction: 'Review /workspace/.
|
|
151
|
+
worktreeNextAction: 'Review /workspace/.fixture-state/worktrees/agent_1.',
|
|
152
152
|
}),
|
|
153
153
|
]),
|
|
154
154
|
});
|
|
@@ -156,10 +156,10 @@ describe('background command module', () => {
|
|
|
156
156
|
const result = await executeBackgroundCommand(context, 'list');
|
|
157
157
|
|
|
158
158
|
expect(result.success).toBe(true);
|
|
159
|
-
expect(result.message).toContain('worktree=/workspace/.
|
|
160
|
-
expect(result.message).toContain('branch=
|
|
159
|
+
expect(result.message).toContain('worktree=/workspace/.fixture-state/worktrees/agent_1');
|
|
160
|
+
expect(result.message).toContain('branch=agent/worker_1');
|
|
161
161
|
expect(result.message).toContain('worktreeStatus="M changed.ts ?? new.ts"');
|
|
162
|
-
expect(result.message).toContain('next="Review /workspace/.
|
|
162
|
+
expect(result.message).toContain('next="Review /workspace/.fixture-state/worktrees/agent_1."');
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
it('defaults to list when no action is provided', async () => {
|
|
@@ -52,10 +52,11 @@ Run it with no arguments.",
|
|
|
52
52
|
"userInvocable": true,
|
|
53
53
|
},
|
|
54
54
|
"mcp": {
|
|
55
|
-
"argumentHint": "[status]",
|
|
56
|
-
"description": "Show which MCP servers are configured and why any of them is unavailable. Use it when an MCP server or its tools are missing, a tool call reports that a server needs sign-in or approval, or the user asks about MCP servers. Returns one line per server: its name, activation state, sign-in state, and the command to suggest to the user when the server needs their approval, sign-in or workspace trust. You cannot approve, sign in or sign out yourself.
|
|
55
|
+
"argumentHint": "[status | skill-list <server>]",
|
|
56
|
+
"description": "Show which MCP servers are configured and why any of them is unavailable. Use it when an MCP server or its tools are missing, a tool call reports that a server needs sign-in or approval, or the user asks about MCP servers. Returns one line per server: its name, activation state, sign-in state, and the command to suggest to the user when the server needs their approval, sign-in or workspace trust. Use skill-list <server> to discover advertised skill names, URIs and descriptions without loading instructions. Returns an invocation name for each supported skill; use the session skill activation command with that exact name when its workflow fits the task. If content consent is needed, ask the user to run /mcp skill-inspect <server> <uri> and approve the reviewed fingerprint. You cannot approve, inspect instructions, sign in or sign out yourself.
|
|
57
57
|
Subcommands you may run (bare \`mcp\` is also allowed):
|
|
58
|
-
- status: Show every MCP server’s name, activation state and sign-in state
|
|
58
|
+
- status: Show every MCP server’s name, activation state and sign-in state
|
|
59
|
+
- skill-list: Discover MCP skill metadata and exact invocation names, or reasons they are unavailable, without activating instructions",
|
|
59
60
|
"kind": "builtin-command",
|
|
60
61
|
"modelInvocable": true,
|
|
61
62
|
"name": "mcp",
|
|
@@ -64,7 +65,7 @@ Subcommands you may run (bare \`mcp\` is also allowed):
|
|
|
64
65
|
},
|
|
65
66
|
"memory": {
|
|
66
67
|
"argumentHint": "[list | show [topic] | add <user|feedback|project|reference> <topic> <text> | pending | used]",
|
|
67
|
-
"description": "Read and write this project’s durable memory. Use \`list\` or \`show [topic]\` to look up stored conventions, preferences or references before asking the user again; \`add <type> <topic> <text>\` to save a durable preference, project convention, feedback item or reference worth reusing across sessions (never secrets, credentials or transient facts); \`pending\` to see candidates awaiting the user’s review; \`used\` to report which memory items informed this turn. Bare lists topics. In plan mode \`add\` is refused: save after plan mode ends. Returns the requested topics or entries, or a confirmation of what was saved. Approving or rejecting a pending candidate is the user’s review: suggest \`/memory approve <id>\` or \`/memory reject <id>\`.
|
|
68
|
+
"description": "Read and write this project’s durable memory. Use \`list\` or \`show [topic]\` to look up stored conventions, preferences or references before asking the user again; \`add <type> <topic> <text>\` to save a durable preference, project convention, feedback item or reference worth reusing across sessions (never secrets, credentials or transient facts); \`pending\` to see candidates awaiting the user’s review; \`used\` to report which memory items informed this turn. Bare lists topics. In plan mode \`add\` is refused: save after plan mode ends. Returns the requested topics or entries, or a confirmation of what was saved. Approving or rejecting a pending candidate is the user’s review: suggest \`/memory approve <id>\` or \`/memory reject <id>\`. Correcting or forgetting saved knowledge is also user-only: suggest \`/memory correct <type> <topic> <text>\` to replace every entry in a topic or \`/memory forget <topic>\` to remove it from active recall. These operations block automatic restoration of that topic; historical transcripts and backups remain.
|
|
68
69
|
Subcommands you may run (bare \`memory\` is also allowed):
|
|
69
70
|
- list: List project memory topics
|
|
70
71
|
- show: Show project memory index or a topic
|
|
@@ -96,6 +97,16 @@ Subcommands you may run (bare \`memory\` is also allowed):
|
|
|
96
97
|
"requiresPermission": false,
|
|
97
98
|
"userInvocable": true,
|
|
98
99
|
},
|
|
100
|
+
"skill-read": {
|
|
101
|
+
"argumentHint": "["<skill-name>", "<resource-uri>"]",
|
|
102
|
+
"description": "Read verified supporting bytes from a skill already activated in this execution. Use when its instructions refer to a resource in the returned manifest. Args are a JSON array ["<skill-name>", "<resource-uri>"]. Returns the verified URI, digest, size and text or base64 blob with optional MIME type. This does not activate nested skills or grant consent; unactivated skills, undeclared resources and ended executions are refused. Binary blobs are returned as encoded data, not a rendered image.",
|
|
103
|
+
"kind": "builtin-command",
|
|
104
|
+
"modelInvocable": true,
|
|
105
|
+
"name": "skill-read",
|
|
106
|
+
"requiresPermission": false,
|
|
107
|
+
"safety": "read-only",
|
|
108
|
+
"userInvocable": true,
|
|
109
|
+
},
|
|
99
110
|
"skills": {
|
|
100
111
|
"argumentHint": "[list | <skill-name> [args]]",
|
|
101
112
|
"description": "Skill command. Before following a matching registered skill from the system prompt Skills section, invoke the projected skills command tool with args "<skill-name> [args]". Without arguments, list registered skills. With a skill name, activate that skill; a skill that declares it is not model-invocable is refused. Returns the skill list with each skill's description, or the activated skill's instructions to follow. Slash syntax is a UI input/display concern; the SDK command identity is "skills".",
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
import { createDefaultCommandModules } from '../default-command-modules.js';
|
|
14
14
|
|
|
15
15
|
/** User-local storage lives in a private per-run directory, never a fixed name under /tmp. */
|
|
16
|
-
const USER_LOCAL_STORAGE_ROOT = mkdtempSync(join(tmpdir(), '
|
|
16
|
+
const USER_LOCAL_STORAGE_ROOT = mkdtempSync(join(tmpdir(), 'agent-test-test-'));
|
|
17
17
|
afterAll(() => rmSync(USER_LOCAL_STORAGE_ROOT, { recursive: true, force: true }));
|
|
18
18
|
|
|
19
19
|
const providerDefinitions: readonly IProviderDefinition[] = [
|
|
@@ -112,7 +112,7 @@ describe('createDefaultCommandModules — PRESET-004 module-selection delta', ()
|
|
|
112
112
|
expect(command, name).toBeDefined();
|
|
113
113
|
const result = await command!.execute({} as never, '');
|
|
114
114
|
expect(result).toMatchObject({ success: false });
|
|
115
|
-
expect(result.message).toMatch(/
|
|
115
|
+
expect(result.message).toMatch(/interactive terminal/);
|
|
116
116
|
}
|
|
117
117
|
});
|
|
118
118
|
|
|
@@ -46,7 +46,7 @@ const themeCataloguePort: IThemeCataloguePort = {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
/** User-local storage lives in a private per-run directory, never a fixed name under /tmp. */
|
|
49
|
-
const USER_LOCAL_STORAGE_ROOT = mkdtempSync(join(tmpdir(), '
|
|
49
|
+
const USER_LOCAL_STORAGE_ROOT = mkdtempSync(join(tmpdir(), 'agent-test-test-'));
|
|
50
50
|
afterAll(() => rmSync(USER_LOCAL_STORAGE_ROOT, { recursive: true, force: true }));
|
|
51
51
|
|
|
52
52
|
const fixtures: IDoctorFixture[] = [];
|
|
@@ -66,7 +66,8 @@ function allBuiltInCommands(): ISystemCommand[] {
|
|
|
66
66
|
});
|
|
67
67
|
// /keybindings and /theme are in the default set; built here with their capabilities instead.
|
|
68
68
|
const withoutPortless = modules.filter(
|
|
69
|
-
(module) =>
|
|
69
|
+
(module) =>
|
|
70
|
+
module.name !== 'agent-command-keybindings' && module.name !== 'agent-command-theme',
|
|
70
71
|
);
|
|
71
72
|
return [
|
|
72
73
|
...withoutPortless,
|
|
@@ -97,6 +98,7 @@ const MODEL_INVOCABLE = [
|
|
|
97
98
|
'memory',
|
|
98
99
|
'monitor',
|
|
99
100
|
'schedule',
|
|
101
|
+
'skill-read',
|
|
100
102
|
'skills',
|
|
101
103
|
];
|
|
102
104
|
|
|
@@ -151,7 +153,17 @@ const USER_ONLY_COMMANDS = [
|
|
|
151
153
|
|
|
152
154
|
/** User-only subcommands of commands the model IS offered; `list` is an alias `/mcp` accepts for the user. */
|
|
153
155
|
const USER_ONLY_SUBCOMMANDS: Record<string, readonly string[]> = {
|
|
154
|
-
mcp: [
|
|
156
|
+
mcp: [
|
|
157
|
+
'approve',
|
|
158
|
+
'reject',
|
|
159
|
+
'revoke',
|
|
160
|
+
'logout',
|
|
161
|
+
'login',
|
|
162
|
+
'list',
|
|
163
|
+
'skill-inspect',
|
|
164
|
+
'skill-approve',
|
|
165
|
+
'skill-withdraw',
|
|
166
|
+
],
|
|
155
167
|
memory: ['approve', 'reject'],
|
|
156
168
|
context: ['add', 'remove', 'clear', 'auto'],
|
|
157
169
|
cost: ['budget'],
|
|
@@ -212,7 +224,7 @@ describe('built-in commands offered to the model', () => {
|
|
|
212
224
|
const offeredBlock = descriptor?.description.split('Subcommands you may run')[1] ?? '';
|
|
213
225
|
for (const sub of subcommands) {
|
|
214
226
|
expect(descriptor?.argumentHint ?? '', `${name} ${sub}`).not.toMatch(
|
|
215
|
-
new RegExp(
|
|
227
|
+
new RegExp(`(?:^|\\s|\\[|\\|)${sub}(?=\\s|\\]|\\||$)`),
|
|
216
228
|
);
|
|
217
229
|
expect(offeredBlock, `${name} ${sub}`).not.toContain(`- ${sub}:`);
|
|
218
230
|
const result = await executor.executeModelInvocable(name, host, `${sub} some-arg`);
|
|
@@ -228,6 +240,7 @@ describe('built-in commands offered to the model', () => {
|
|
|
228
240
|
for (const [name, args] of [
|
|
229
241
|
['mcp', ''],
|
|
230
242
|
['mcp', 'status'],
|
|
243
|
+
['mcp', 'skill-list docs'],
|
|
231
244
|
['memory', 'list'],
|
|
232
245
|
['context', ''],
|
|
233
246
|
['context', 'list'],
|
|
@@ -235,7 +248,7 @@ describe('built-in commands offered to the model', () => {
|
|
|
235
248
|
] as const) {
|
|
236
249
|
expect((await executor.executeModelInvocable(name, host, args))?.message).toBe('ran');
|
|
237
250
|
}
|
|
238
|
-
expect(executed).toHaveBeenCalledTimes(
|
|
251
|
+
expect(executed).toHaveBeenCalledTimes(7);
|
|
239
252
|
});
|
|
240
253
|
|
|
241
254
|
it('skips the by-name prompt only where the model reaches nothing that needs it', () => {
|
|
@@ -60,7 +60,7 @@ const adapter: IProviderCommandSettingsAdapter = {
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
/** User-local storage lives in a private per-run directory, never a fixed name under /tmp. */
|
|
63
|
-
const USER_LOCAL_STORAGE_ROOT = mkdtempSync(join(tmpdir(), '
|
|
63
|
+
const USER_LOCAL_STORAGE_ROOT = mkdtempSync(join(tmpdir(), 'agent-test-test-'));
|
|
64
64
|
afterAll(() => rmSync(USER_LOCAL_STORAGE_ROOT, { recursive: true, force: true }));
|
|
65
65
|
|
|
66
66
|
const POLICY: IOrgPolicy = { allowedProviders: ['anthropic'], adminContact: 'ops@example.com' };
|
|
@@ -74,11 +74,11 @@ export interface IDefaultCommandModulesOptions {
|
|
|
74
74
|
* red when the producer dropped it.
|
|
75
75
|
*/
|
|
76
76
|
orgPolicy?: IOrgPolicy;
|
|
77
|
-
/** Optional TUI-owned file capability; absent, `/keybindings` says it belongs to
|
|
77
|
+
/** Optional TUI-owned file capability; absent, `/keybindings` says it belongs to an interactive terminal. */
|
|
78
78
|
keybindingsFilePort?: IKeybindingsFilePort;
|
|
79
79
|
/**
|
|
80
80
|
* SCREEN-2002: the surface's theme catalogue. Absent (print mode, `--serve`), `/theme` is still
|
|
81
|
-
* listed and says it belongs to
|
|
81
|
+
* listed and says it belongs to an interactive terminal, which runs it when attached.
|
|
82
82
|
*/
|
|
83
83
|
themeCataloguePort?: IThemeCataloguePort;
|
|
84
84
|
/**
|
|
@@ -81,7 +81,7 @@ const REFUSALS: Readonly<Record<TDevicesRefusal, string>> = {
|
|
|
81
81
|
'no-terminal':
|
|
82
82
|
'This needs an interactive terminal; the recovery phrase is never shown or read anywhere else.',
|
|
83
83
|
'not-initialized':
|
|
84
|
-
'This device has no identity yet. If
|
|
84
|
+
'This device has no identity yet. If your other devices are already linked, run `/devices add` there and `/devices join` here, typing the code it shows. Run `/devices init` only when setting up your first device: it creates a separate identity that cannot link to an existing device group.',
|
|
85
85
|
'already-initialized':
|
|
86
86
|
'This device already has an identity. Use `/devices recover` to rotate its signing key.',
|
|
87
87
|
cancelled: 'Cancelled. Nothing was changed.',
|
|
@@ -77,11 +77,11 @@ describe('/doctor command module (OBSERVABILITY-1991 TC-06)', () => {
|
|
|
77
77
|
expect(command?.name).toBe('doctor');
|
|
78
78
|
const result = await command!.execute(contextWith(undefined), '');
|
|
79
79
|
expect(result.success).toBe(false);
|
|
80
|
-
expect(result.message).toContain('settings.user.
|
|
80
|
+
expect(result.message).toContain('settings.user.fixture-product');
|
|
81
81
|
expect(result.message).toContain('plugin.broken-plugin@fixture-market');
|
|
82
82
|
expect(result.data).toMatchObject({
|
|
83
83
|
failCount: expect.any(Number),
|
|
84
|
-
repairable: ['settings.user.
|
|
84
|
+
repairable: ['settings.user.fixture-product'],
|
|
85
85
|
});
|
|
86
86
|
expect(createProvider).not.toHaveBeenCalled();
|
|
87
87
|
});
|
|
@@ -100,7 +100,7 @@ describe('/doctor command module (OBSERVABILITY-1991 TC-06)', () => {
|
|
|
100
100
|
const result = await command.execute(contextWith(undefined), '');
|
|
101
101
|
expect(result.message).toContain('Atlas doctor');
|
|
102
102
|
expect(result.message).toContain('Atlas');
|
|
103
|
-
expect(result.message).toContain('atlas doctor --repair settings.user.
|
|
103
|
+
expect(result.message).toContain('atlas doctor --repair settings.user.fixture-product');
|
|
104
104
|
expect(result.message).toContain('run atlas doctor --repair <check-id>');
|
|
105
105
|
});
|
|
106
106
|
|
|
@@ -108,15 +108,15 @@ describe('/doctor command module (OBSERVABILITY-1991 TC-06)', () => {
|
|
|
108
108
|
const fixture = createDoctorFixture({ env: {} });
|
|
109
109
|
fixtures.push(fixture);
|
|
110
110
|
installBrokenHome(fixture);
|
|
111
|
-
const path = join(fixture.home, '.
|
|
111
|
+
const path = join(fixture.home, '.fixture-product', 'settings.json');
|
|
112
112
|
const command = createDoctorCommandModule(fixture.inputs, fixture.deps).systemCommands![0]!;
|
|
113
113
|
|
|
114
|
-
const noPort = await command.execute(contextWith(undefined), 'repair settings.user.
|
|
114
|
+
const noPort = await command.execute(contextWith(undefined), 'repair settings.user.fixture-product');
|
|
115
115
|
expect(noPort.success).toBe(false);
|
|
116
116
|
expect(readFileSync(path, 'utf8')).toBe('');
|
|
117
117
|
|
|
118
118
|
const cancelled = answering({ type: 'cancelled' });
|
|
119
|
-
const declined = await command.execute(contextWith(cancelled), 'repair settings.user.
|
|
119
|
+
const declined = await command.execute(contextWith(cancelled), 'repair settings.user.fixture-product');
|
|
120
120
|
expect(declined.success).toBe(false);
|
|
121
121
|
expect(cancelled.ask).toHaveBeenCalledTimes(1);
|
|
122
122
|
expect(readFileSync(path, 'utf8')).toBe('');
|
|
@@ -129,17 +129,17 @@ describe('/doctor command module (OBSERVABILITY-1991 TC-06)', () => {
|
|
|
129
129
|
const fixture = createDoctorFixture({ env: {} });
|
|
130
130
|
fixtures.push(fixture);
|
|
131
131
|
installBrokenHome(fixture);
|
|
132
|
-
const path = join(fixture.home, '.
|
|
132
|
+
const path = join(fixture.home, '.fixture-product', 'settings.json');
|
|
133
133
|
const command = createDoctorCommandModule(fixture.inputs, fixture.deps).systemCommands![0]!;
|
|
134
134
|
const yes = answering({ type: 'answer', values: ['yes'] });
|
|
135
|
-
const result = await command.execute(contextWith(yes), 'repair settings.user.
|
|
135
|
+
const result = await command.execute(contextWith(yes), 'repair settings.user.fixture-product');
|
|
136
136
|
expect(result.success).toBe(true);
|
|
137
137
|
expect(JSON.parse(readFileSync(path, 'utf8'))).toEqual({});
|
|
138
138
|
expect(result.data).toMatchObject({
|
|
139
|
-
repair: 'settings.user.
|
|
139
|
+
repair: 'settings.user.fixture-product',
|
|
140
140
|
applied: true,
|
|
141
141
|
status: 'ok',
|
|
142
142
|
});
|
|
143
|
-
expect(yes.ask.mock.calls[0]?.[0]).toMatchObject({ id: 'doctor-repair:settings.user.
|
|
143
|
+
expect(yes.ask.mock.calls[0]?.[0]).toMatchObject({ id: 'doctor-repair:settings.user.fixture-product' });
|
|
144
144
|
});
|
|
145
145
|
});
|
|
@@ -15,7 +15,7 @@ const REPORT: IDoctorReport = {
|
|
|
15
15
|
describe('doctor display vocabulary', () => {
|
|
16
16
|
it('uses neutral report and repair text without a host vocabulary', () => {
|
|
17
17
|
const text = renderDoctorReport(REPORT).join('\n');
|
|
18
|
-
expect(text).not.
|
|
18
|
+
expect(text).not.toContain('product name');
|
|
19
19
|
expect(text).not.toContain('--repair');
|
|
20
20
|
expect(text).toContain('storage.user');
|
|
21
21
|
});
|
|
@@ -37,7 +37,7 @@ export function fixtureProviderDefinition(
|
|
|
37
37
|
return {
|
|
38
38
|
type: 'fixture',
|
|
39
39
|
displayName: 'Fixture',
|
|
40
|
-
defaults: { model: 'fixture-model', apiKey: '$ENV:
|
|
40
|
+
defaults: { model: 'fixture-model', apiKey: '$ENV:FIXTURE_DOCTOR_API_KEY' },
|
|
41
41
|
endpoint: { host: 'api.fixture.example', port: 443 },
|
|
42
42
|
createProvider: () => {
|
|
43
43
|
throw new Error('the doctor must never construct a provider');
|
|
@@ -74,7 +74,7 @@ function fixtureDeps(
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export function createDoctorFixture(options: IDoctorFixtureOptions = {}): IDoctorFixture {
|
|
77
|
-
const home = mkdtempSync(join(tmpdir(), '
|
|
77
|
+
const home = mkdtempSync(join(tmpdir(), 'agent-doctor-fixture-'));
|
|
78
78
|
const write = (relative: string, content: string): string => {
|
|
79
79
|
const path = join(home, relative);
|
|
80
80
|
mkdirSync(join(path, '..'), { recursive: true });
|
|
@@ -92,18 +92,18 @@ export function createDoctorFixture(options: IDoctorFixtureOptions = {}): IDocto
|
|
|
92
92
|
const inputs: IDoctorInputs = {
|
|
93
93
|
cwd: home,
|
|
94
94
|
userHome: home,
|
|
95
|
-
userSettingsPath: join(home, '.
|
|
96
|
-
userStorage: { root: join(home, '.
|
|
95
|
+
userSettingsPath: join(home, '.fixture-product', 'settings.json'),
|
|
96
|
+
userStorage: { root: join(home, '.fixture-product'), sessions: join(home, '.fixture-product', 'sessions') },
|
|
97
97
|
settingsSources: [
|
|
98
|
-
createNodeHostSettingsSource('user', join(home, '.
|
|
98
|
+
createNodeHostSettingsSource('user', join(home, '.fixture-product', 'settings.json')),
|
|
99
99
|
createNodeHostSettingsSource('user', join(home, '.claude', 'settings.json')),
|
|
100
100
|
],
|
|
101
101
|
projectAccess: createRestrictedWorkspaceProjectAccess('untrusted', home),
|
|
102
102
|
providerDefinitions: options.providerDefinitions ?? [fixtureProviderDefinition()],
|
|
103
103
|
env,
|
|
104
104
|
contributionSources: [createNodeHostContributionSource(home)],
|
|
105
|
-
skillRoots: [{ root: join('.
|
|
106
|
-
pluginsDirs: [join(home, '.
|
|
105
|
+
skillRoots: [{ root: join('.fixture-product', 'skills'), kind: 'skills' }],
|
|
106
|
+
pluginsDirs: [join(home, '.fixture-product', 'plugins')],
|
|
107
107
|
hostChecks: [
|
|
108
108
|
{ id: 'host.fixture', label: 'Fixture host check', status: 'ok', cause: 'present' },
|
|
109
109
|
],
|
|
@@ -122,23 +122,23 @@ export function createDoctorFixture(options: IDoctorFixtureOptions = {}): IDocto
|
|
|
122
122
|
|
|
123
123
|
/** The "broken HOME" of the built-CLI scenario, as a unit fixture. */
|
|
124
124
|
export function installBrokenHome(fixture: IDoctorFixture): void {
|
|
125
|
-
fixture.mkdir('.
|
|
126
|
-
fixture.mkdir('.
|
|
127
|
-
fixture.write('.
|
|
125
|
+
fixture.mkdir('.fixture-product');
|
|
126
|
+
fixture.mkdir('.fixture-product/sessions');
|
|
127
|
+
fixture.write('.fixture-product/settings.json', '');
|
|
128
128
|
fixture.write('.claude/settings.json', '{"defaultTrustLevel":42}');
|
|
129
129
|
fixture.write(
|
|
130
|
-
'.
|
|
130
|
+
'.fixture-product/plugins/cache/fixture-market/broken-plugin/1.0.0/.claude-plugin/plugin.json',
|
|
131
131
|
'{',
|
|
132
132
|
);
|
|
133
133
|
fixture.write(
|
|
134
|
-
'.
|
|
134
|
+
'.fixture-product/plugins/cache/fixture-market/mcp-plugin/1.0.0/.claude-plugin/plugin.json',
|
|
135
135
|
JSON.stringify({ name: 'mcp-plugin', version: '1.0.0', description: 'doctor fixture' }),
|
|
136
136
|
);
|
|
137
137
|
fixture.write(
|
|
138
|
-
'.
|
|
138
|
+
'.fixture-product/plugins/cache/fixture-market/mcp-plugin/1.0.0/.mcp.json',
|
|
139
139
|
JSON.stringify({
|
|
140
140
|
mcpServers: {
|
|
141
|
-
ghost: { command: '
|
|
141
|
+
ghost: { command: 'agent-test-doctor-missing-binary', env: { GHOST_TOKEN: MARKERS.mcp } },
|
|
142
142
|
ready: { type: 'http', url: 'https://mcp.example.test' },
|
|
143
143
|
},
|
|
144
144
|
}),
|