@pellux/goodvibes-agent 1.0.0 → 1.0.2

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +8 -4
  3. package/dist/package/main.js +14046 -12557
  4. package/docs/README.md +4 -2
  5. package/docs/channels-remote-and-api.md +4 -0
  6. package/docs/connected-host.md +2 -0
  7. package/docs/getting-started.md +6 -2
  8. package/docs/knowledge-artifacts-and-multimodal.md +5 -3
  9. package/docs/project-planning.md +3 -1
  10. package/docs/providers-and-routing.md +3 -0
  11. package/docs/release-and-publishing.md +7 -6
  12. package/docs/tools-and-commands.md +97 -2
  13. package/docs/voice-and-live-tts.md +2 -0
  14. package/package.json +1 -1
  15. package/src/agent/harness-control.ts +266 -0
  16. package/src/cli/local-library-command.ts +25 -5
  17. package/src/cli/routines-command.ts +15 -3
  18. package/src/config/agent-settings-policy.ts +44 -0
  19. package/src/input/agent-workspace-activation.ts +14 -6
  20. package/src/input/agent-workspace-knowledge-url-editor.ts +4 -11
  21. package/src/input/commands/agent-local-library-args.ts +52 -0
  22. package/src/input/commands/agent-skills-runtime.ts +4 -29
  23. package/src/input/commands/mcp-runtime.ts +62 -15
  24. package/src/input/commands/operator-runtime.ts +139 -3
  25. package/src/input/commands/personas-runtime.ts +4 -29
  26. package/src/input/commands/routines-runtime.ts +4 -29
  27. package/src/input/session-picker-modal.ts +3 -2
  28. package/src/input/settings-modal-agent-policy.ts +5 -44
  29. package/src/runtime/bootstrap.ts +3 -0
  30. package/src/tools/agent-harness-local-operations.ts +233 -0
  31. package/src/tools/agent-harness-metadata.ts +300 -0
  32. package/src/tools/agent-harness-tool.ts +774 -0
  33. package/src/tools/agent-local-registry-requirements.ts +18 -0
  34. package/src/tools/agent-local-registry-tool.ts +88 -34
  35. package/src/version.ts +1 -1
@@ -33,6 +33,20 @@ interface ParsedOptions {
33
33
  readonly positionals: readonly string[];
34
34
  }
35
35
 
36
+ const LOCAL_LIBRARY_VALUE_OPTIONS = [
37
+ 'name',
38
+ 'description',
39
+ 'body',
40
+ 'procedure',
41
+ 'tags',
42
+ 'triggers',
43
+ 'requires-env',
44
+ 'requires-command',
45
+ 'requires-commands',
46
+ 'skills',
47
+ 'provenance',
48
+ ] as const;
49
+
36
50
  function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): string {
37
51
  return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
38
52
  }
@@ -50,24 +64,30 @@ function failure(runtime: CliCommandRuntime, kind: string, error: string, exitCo
50
64
  };
51
65
  }
52
66
 
53
- function parseOptions(args: readonly string[]): ParsedOptions {
67
+ function parseOptions(args: readonly string[], valueOptions: readonly string[] = LOCAL_LIBRARY_VALUE_OPTIONS): ParsedOptions {
68
+ const valued = new Set(valueOptions);
54
69
  const values = new Map<string, string>();
55
70
  const flags = new Set<string>();
56
71
  const positionals: string[] = [];
57
72
  for (let index = 0; index < args.length; index += 1) {
58
73
  const arg = args[index] ?? '';
74
+ if (arg === '--') {
75
+ positionals.push(...args.slice(index + 1));
76
+ break;
77
+ }
59
78
  if (!arg.startsWith('--')) {
60
79
  positionals.push(arg);
61
80
  continue;
62
81
  }
63
- const equalIndex = arg.indexOf('=');
82
+ const raw = arg.slice(2);
83
+ const equalIndex = raw.indexOf('=');
64
84
  if (equalIndex >= 0) {
65
- values.set(arg.slice(2, equalIndex), arg.slice(equalIndex + 1));
85
+ values.set(raw.slice(0, equalIndex), raw.slice(equalIndex + 1));
66
86
  continue;
67
87
  }
68
- const name = arg.slice(2);
88
+ const name = raw;
69
89
  const next = args[index + 1];
70
- if (next !== undefined && !next.startsWith('--')) {
90
+ if (next !== undefined && (valued.has(name) || !next.startsWith('--'))) {
71
91
  values.set(name, next);
72
92
  index += 1;
73
93
  continue;
@@ -43,6 +43,7 @@ interface ParsedRoutineOptions {
43
43
  }
44
44
 
45
45
  const ROUTINE_CREATE_USAGE = 'Usage: goodvibes-agent routines create --name <name> --description <summary> --steps <steps> [--tags a,b] [--triggers a,b] [--requires-env A,B] [--requires-command gh,jq] [--enabled]';
46
+ const ROUTINE_VALUE_OPTIONS = ['name', 'description', 'steps', 'tags', 'triggers', 'requires-env', 'requires-command', 'requires-commands'] as const;
46
47
 
47
48
  function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): string {
48
49
  return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
@@ -83,19 +84,30 @@ function splitList(value: string | undefined): readonly string[] {
83
84
  }
84
85
 
85
86
  function parseRoutineOptions(args: readonly string[]): ParsedRoutineOptions {
87
+ const valued: ReadonlySet<string> = new Set(ROUTINE_VALUE_OPTIONS);
86
88
  const flags = new Map<string, string>();
87
89
  const positionals: string[] = [];
88
90
  let yes = false;
89
91
  for (let index = 0; index < args.length; index += 1) {
90
92
  const token = args[index] ?? '';
93
+ if (token === '--') {
94
+ positionals.push(...args.slice(index + 1));
95
+ break;
96
+ }
91
97
  if (token === '--yes') {
92
98
  yes = true;
93
99
  continue;
94
100
  }
95
- if (token.startsWith('--')) {
96
- const key = token.slice(2);
101
+ if (token.startsWith('--') && token.length > 2) {
102
+ const raw = token.slice(2);
103
+ const equalIndex = raw.indexOf('=');
104
+ if (equalIndex >= 0) {
105
+ flags.set(raw.slice(0, equalIndex), raw.slice(equalIndex + 1));
106
+ continue;
107
+ }
108
+ const key = raw;
97
109
  const next = args[index + 1];
98
- if (next !== undefined && !next.startsWith('--')) {
110
+ if (next !== undefined && (valued.has(key) || !next.startsWith('--'))) {
99
111
  flags.set(key, next);
100
112
  index += 1;
101
113
  } else {
@@ -0,0 +1,44 @@
1
+ export const AGENT_EXTERNAL_HOST_SETTING_LOCK_REASON = 'GoodVibes Agent uses a connected GoodVibes host. Change host lifecycle and bind posture from the owning host; Agent settings are read-only for those controls.';
2
+
3
+ const AGENT_HIDDEN_SETTING_PREFIXES = [
4
+ ['cloud', 'flare.'].join(''),
5
+ ['surfaces.', 'home', 'assistant.'].join(''),
6
+ 'batch.',
7
+ 'controlPlane.',
8
+ 'danger.',
9
+ 'httpListener.',
10
+ 'network.',
11
+ 'orchestration.',
12
+ 'runtime.',
13
+ 'service.',
14
+ 'sandbox.',
15
+ 'web.',
16
+ 'watchers.',
17
+ 'wrfc.',
18
+ ] as const;
19
+
20
+ const AGENT_HIDDEN_SETTING_KEYS = new Set<string>([
21
+ 'ui.wrfcMessages',
22
+ ]);
23
+
24
+ const EXTERNAL_HOST_SETTING_PREFIXES = [
25
+ 'service.',
26
+ 'controlPlane.',
27
+ 'httpListener.',
28
+ 'web.',
29
+ ] as const;
30
+
31
+ const EXTERNAL_HOST_SETTING_KEYS = new Set<string>([
32
+ 'danger.daemon',
33
+ 'danger.httpListener',
34
+ ]);
35
+
36
+ export function isExternalHostOwnedSettingKey(key: string): boolean {
37
+ return EXTERNAL_HOST_SETTING_KEYS.has(key)
38
+ || EXTERNAL_HOST_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
39
+ }
40
+
41
+ export function isAgentHiddenSettingKey(key: string): boolean {
42
+ return AGENT_HIDDEN_SETTING_KEYS.has(key)
43
+ || AGENT_HIDDEN_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
44
+ }
@@ -9,12 +9,14 @@ import type {
9
9
  AgentWorkspaceActionResult,
10
10
  AgentWorkspaceCategory,
11
11
  AgentWorkspaceCommandDispatcher,
12
+ AgentWorkspaceEditorKind,
12
13
  AgentWorkspaceFocusPane,
13
14
  AgentWorkspaceLocalEditor,
14
15
  AgentWorkspaceLocalEditorKind,
15
16
  AgentWorkspaceLocalLibraryItem,
16
17
  AgentWorkspaceLocalOperation,
17
18
  AgentWorkspaceRuntimeSnapshot,
19
+ AgentWorkspaceRuntimeStarterTemplateItem,
18
20
  } from './agent-workspace-types.ts';
19
21
 
20
22
  interface AgentWorkspaceActivationHost {
@@ -55,7 +57,10 @@ export function activateAgentWorkspaceSelection(
55
57
  const action = workspace.selectedAction;
56
58
  if (!action) return;
57
59
  if (action.kind === 'editor' && action.editorKind) {
58
- const editor = createWorkspaceEditor(workspace, action.editorKind);
60
+ const editor = createAgentWorkspaceEditor(action.editorKind, {
61
+ runtimeStarterTemplates: workspace.runtimeSnapshot?.runtimeStarterTemplates ?? [],
62
+ selectedRoutine: workspace.selectedLocalLibraryItem('routine'),
63
+ });
59
64
  if (!editor) {
60
65
  workspace.status = `Editor unavailable: ${action.editorKind}.`;
61
66
  workspace.lastActionResult = {
@@ -143,11 +148,14 @@ export function activateAgentWorkspaceSelection(
143
148
  workspace.dispatchWorkspaceCommand(action.command);
144
149
  }
145
150
 
146
- function createWorkspaceEditor(
147
- workspace: AgentWorkspaceActivationHost,
148
- editorKind: NonNullable<AgentWorkspaceCategory['actions'][number]['editorKind']>,
151
+ export function createAgentWorkspaceEditor(
152
+ editorKind: AgentWorkspaceEditorKind,
153
+ options: {
154
+ readonly runtimeStarterTemplates?: readonly AgentWorkspaceRuntimeStarterTemplateItem[];
155
+ readonly selectedRoutine?: AgentWorkspaceLocalLibraryItem | null;
156
+ } = {},
149
157
  ): AgentWorkspaceLocalEditor | null {
150
- if (editorKind === 'profile') return createProfileEditor(workspace.runtimeSnapshot?.runtimeStarterTemplates ?? []);
158
+ if (editorKind === 'profile') return createProfileEditor(options.runtimeStarterTemplates ?? []);
151
159
  if (editorKind === 'learned-behavior') return createLearnedBehaviorEditor();
152
160
  if (editorKind === 'web-research') return createAgentWorkspaceWebResearchEditor('research');
153
161
  if (editorKind === 'web-fetch') return createAgentWorkspaceWebResearchEditor('fetch');
@@ -155,7 +163,7 @@ function createWorkspaceEditor(
155
163
  if (editorKind === 'knowledge-ask') return createAgentKnowledgeQueryEditor('ask');
156
164
  if (editorKind === 'knowledge-search') return createAgentKnowledgeQueryEditor('search');
157
165
  if (editorKind === 'reminder-schedule') return createReminderScheduleEditor();
158
- if (editorKind === 'routine-schedule') return createRoutineScheduleEditor(workspace.selectedLocalLibraryItem('routine'));
166
+ if (editorKind === 'routine-schedule') return createRoutineScheduleEditor(options.selectedRoutine ?? null);
159
167
  if (
160
168
  editorKind === 'memory'
161
169
  || editorKind === 'note'
@@ -1,5 +1,6 @@
1
1
  import type { AgentWorkspaceActionResult, AgentWorkspaceLocalEditor } from './agent-workspace-types.ts';
2
2
  import { isAffirmative, splitList } from './agent-workspace-editors.ts';
3
+ import { quoteSlashCommandArg } from './slash-command-parser.ts';
3
4
 
4
5
  type AgentWorkspaceFieldReader = (fieldId: string) => string;
5
6
 
@@ -65,18 +66,10 @@ export function buildAgentKnowledgeUrlEditorSubmission(
65
66
  }
66
67
 
67
68
  const folder = readField('folder');
68
- if (/\s/.test(folder)) {
69
- return {
70
- kind: 'editor',
71
- editor: { ...editor, message: 'Folder paths with spaces are not supported from this compact workspace form.' },
72
- status: 'Folder path contains spaces.',
73
- };
74
- }
75
-
76
69
  const tags = splitList(readField('tags'));
77
- const parts = ['/knowledge', 'ingest-url', url];
78
- if (tags.length > 0) parts.push('--tags', tags.join(','));
79
- if (folder.length > 0) parts.push('--folder', folder);
70
+ const parts = ['/knowledge', 'ingest-url', quoteSlashCommandArg(url)];
71
+ if (tags.length > 0) parts.push('--tags', quoteSlashCommandArg(tags.join(',')));
72
+ if (folder.length > 0) parts.push('--folder', quoteSlashCommandArg(folder));
80
73
  parts.push('--yes');
81
74
  const command = parts.join(' ');
82
75
 
@@ -0,0 +1,52 @@
1
+ export interface ParsedAgentLocalLibraryArgs {
2
+ readonly rest: readonly string[];
3
+ readonly flags: ReadonlyMap<string, string>;
4
+ readonly yes: boolean;
5
+ }
6
+
7
+ export interface AgentLocalLibraryArgOptions {
8
+ readonly valueFlags: readonly string[];
9
+ }
10
+
11
+ export function parseAgentLocalLibraryArgs(
12
+ args: readonly string[],
13
+ options: AgentLocalLibraryArgOptions,
14
+ ): ParsedAgentLocalLibraryArgs {
15
+ const valueFlags = new Set(options.valueFlags);
16
+ const flags = new Map<string, string>();
17
+ const rest: string[] = [];
18
+ let yes = false;
19
+ for (let index = 0; index < args.length; index += 1) {
20
+ const token = args[index] ?? '';
21
+ if (token === '--') {
22
+ rest.push(...args.slice(index + 1));
23
+ break;
24
+ }
25
+ if (token === '--yes') {
26
+ yes = true;
27
+ continue;
28
+ }
29
+ if (token.startsWith('--') && token.length > 2) {
30
+ const raw = token.slice(2);
31
+ const equalIndex = raw.indexOf('=');
32
+ if (equalIndex >= 0) {
33
+ flags.set(raw.slice(0, equalIndex), raw.slice(equalIndex + 1));
34
+ continue;
35
+ }
36
+ if (valueFlags.has(raw)) {
37
+ const next = args[index + 1];
38
+ if (next !== undefined) {
39
+ flags.set(raw, next);
40
+ index += 1;
41
+ } else {
42
+ flags.set(raw, '');
43
+ }
44
+ continue;
45
+ }
46
+ flags.set(raw, 'true');
47
+ continue;
48
+ }
49
+ rest.push(token);
50
+ }
51
+ return { rest, flags, yes };
52
+ }
@@ -10,38 +10,13 @@ import {
10
10
  import { discoverSkills, type SkillRecord } from '../../agent/skill-discovery.ts';
11
11
  import { formatAgentRecordOrigin, formatAgentRecordReviewState } from '../../agent/record-labels.ts';
12
12
  import type { CommandContext, CommandRegistry } from '../command-registry.ts';
13
+ import { parseAgentLocalLibraryArgs, type ParsedAgentLocalLibraryArgs } from './agent-local-library-args.ts';
13
14
  import { requireShellPaths } from './runtime-services.ts';
14
15
 
15
- interface ParsedSkillArgs {
16
- readonly rest: readonly string[];
17
- readonly flags: ReadonlyMap<string, string>;
18
- readonly yes: boolean;
19
- }
16
+ const SKILL_VALUE_FLAGS = ['name', 'description', 'procedure', 'tags', 'triggers', 'requires-env', 'requires-command', 'requires-commands', 'skills'] as const;
20
17
 
21
- function parseSkillArgs(args: readonly string[]): ParsedSkillArgs {
22
- const flags = new Map<string, string>();
23
- const rest: string[] = [];
24
- let yes = false;
25
- for (let index = 0; index < args.length; index += 1) {
26
- const token = args[index] ?? '';
27
- if (token === '--yes') {
28
- yes = true;
29
- continue;
30
- }
31
- if (token.startsWith('--')) {
32
- const key = token.slice(2);
33
- const next = args[index + 1];
34
- if (next !== undefined && !next.startsWith('--')) {
35
- flags.set(key, next);
36
- index += 1;
37
- } else {
38
- flags.set(key, 'true');
39
- }
40
- continue;
41
- }
42
- rest.push(token);
43
- }
44
- return { rest, flags, yes };
18
+ function parseSkillArgs(args: readonly string[]): ParsedAgentLocalLibraryArgs {
19
+ return parseAgentLocalLibraryArgs(args, { valueFlags: SKILL_VALUE_FLAGS });
45
20
  }
46
21
 
47
22
  function splitList(value: string | undefined): readonly string[] {
@@ -3,9 +3,14 @@ import type { McpConfigScope, McpReloadResult, McpServerConfig } from '@pellux/g
3
3
  import { requireMcpApi, requireShellPaths } from './runtime-services.ts';
4
4
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
5
5
  import { requireYesFlag, stripYesFlag } from './confirmation.ts';
6
+ import { quoteSlashCommandArg } from '../slash-command-parser.ts';
6
7
 
7
8
  const MCP_ROLES = ['general', 'docs', 'filesystem', 'git', 'database', 'browser', 'automation', 'ops', 'remote'] as const;
8
9
  const MCP_TRUST_MODES = ['constrained', 'ask-on-risk', 'allow-all', 'blocked'] as const;
10
+ const MCP_COMMAND_TRUST_MODES = ['constrained', 'ask-on-risk', 'blocked'] as const;
11
+ const MCP_ADD_COMMAND_USAGE = '/mcp add <name> <command> ... --yes; options: --scope project|global, --role <role>, --trust constrained|ask-on-risk|blocked, --env KEY=VALUE, --path <path>, --host <host>';
12
+ const MCP_TRUST_COMMAND_USAGE = '/mcp trust <server> <constrained|ask-on-risk|blocked> --yes';
13
+ const MCP_ROLE_COMMAND_USAGE = '/mcp role <server> <general|docs|filesystem|git|database|browser|automation|ops|remote> --yes';
9
14
 
10
15
  interface ParsedMcpAddArgs {
11
16
  readonly scope: McpConfigScope;
@@ -25,10 +30,37 @@ function isMcpTrustMode(value: string): value is NonNullable<McpServerConfig['tr
25
30
  return MCP_TRUST_MODES.includes(value as NonNullable<McpServerConfig['trustMode']>);
26
31
  }
27
32
 
33
+ function isMcpCommandTrustMode(value: string): value is Exclude<NonNullable<McpServerConfig['trustMode']>, 'allow-all'> {
34
+ return MCP_COMMAND_TRUST_MODES.includes(value as Exclude<NonNullable<McpServerConfig['trustMode']>, 'allow-all'>);
35
+ }
36
+
28
37
  function isMcpScope(value: string): value is McpConfigScope {
29
38
  return value === 'project' || value === 'global';
30
39
  }
31
40
 
41
+ function stripMcpAddYesFlag(args: readonly string[]): { readonly rest: readonly string[]; readonly yes: boolean } {
42
+ const rest: string[] = [];
43
+ let yes = false;
44
+ let passthrough = false;
45
+ for (const token of args) {
46
+ if (passthrough) {
47
+ rest.push(token);
48
+ continue;
49
+ }
50
+ if (token === '--') {
51
+ passthrough = true;
52
+ rest.push(token);
53
+ continue;
54
+ }
55
+ if (token === '--yes') {
56
+ yes = true;
57
+ continue;
58
+ }
59
+ rest.push(token);
60
+ }
61
+ return { rest, yes };
62
+ }
63
+
32
64
  function formatMcpTrustMode(mode: McpDisplayTrustMode): string {
33
65
  if (mode === 'ask-on-risk') return 'ask on risky actions';
34
66
  if (mode === 'allow-all') return 'allow all actions';
@@ -64,7 +96,7 @@ function parseAddServerArgs(args: string[]): ParsedMcpAddArgs {
64
96
  const name = args[1]?.trim();
65
97
  const command = args[2]?.trim();
66
98
  if (!name || !command) {
67
- throw new Error('Usage: /mcp add <name> <command> [args...] [--scope project|global] [--role <role>] [--trust <mode>] [--env KEY=VALUE] [--path <path>] [--host <host>]');
99
+ throw new Error(`Usage: ${MCP_ADD_COMMAND_USAGE}`);
68
100
  }
69
101
  const nameError = validateServerName(name);
70
102
  if (nameError) throw new Error(nameError);
@@ -106,6 +138,9 @@ function parseAddServerArgs(args: string[]): ParsedMcpAddArgs {
106
138
  if (token === '--trust') {
107
139
  const value = readFlagValue(tokens, index, token);
108
140
  if (!isMcpTrustMode(value)) throw new Error(`Invalid MCP trust mode "${value}". Expected one of ${MCP_TRUST_MODES.join(', ')}.`);
141
+ if (!isMcpCommandTrustMode(value)) {
142
+ throw new Error(`Use /settings -> MCP to explicitly enable allow-all.\nUsage: ${MCP_ADD_COMMAND_USAGE}`);
143
+ }
109
144
  trustMode = value;
110
145
  index += 1;
111
146
  continue;
@@ -162,7 +197,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
162
197
  async handler(args, ctx) {
163
198
  const mcpApi = requireMcpApi(ctx);
164
199
  const listServerSecurity = () => mcpApi.listServerSecurity();
165
- const confirmation = stripYesFlag(args);
200
+ const confirmation = args[0] === 'add' ? stripMcpAddYesFlag(args) : stripYesFlag(args);
166
201
  const commandArgs = [...confirmation.rest];
167
202
  const subcommand = commandArgs[0];
168
203
  if (!subcommand && ctx.openMcpWorkspace) {
@@ -259,7 +294,7 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
259
294
  }
260
295
  const nextSteps = [
261
296
  selected.schemaFreshness === 'quarantined'
262
- ? `/mcp quarantine ${selected.name} approve operator --yes`
297
+ ? `/mcp quarantine ${quoteSlashCommandArg(selected.name)} approve operator --yes`
263
298
  : null,
264
299
  !selected.connected ? '/auth review' : null,
265
300
  '/mcp review',
@@ -281,48 +316,58 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
281
316
 
282
317
  if (subcommand === 'trust') {
283
318
  const serverName = commandArgs[1];
284
- const mode = commandArgs[2] as 'constrained' | 'ask-on-risk' | 'allow-all' | 'blocked' | undefined;
285
- if (serverName && mode) {
319
+ const requestedMode = commandArgs[2];
320
+ if (serverName && requestedMode) {
321
+ if (!isMcpTrustMode(requestedMode)) {
322
+ ctx.print(`Invalid MCP trust mode "${requestedMode}". Expected constrained, ask-on-risk, blocked, or allow-all.\nUsage: ${MCP_TRUST_COMMAND_USAGE}\nUse /settings -> MCP to explicitly enable allow-all.`);
323
+ return;
324
+ }
325
+ const mode = requestedMode;
286
326
  if (mode === 'allow-all') {
287
327
  ctx.print(`Use /settings → MCP to explicitly enable allow-all for ${serverName}. Direct command escalation is blocked.`);
288
328
  ctx.openSettingsModal?.();
289
329
  return;
290
330
  }
291
331
  if (!confirmation.yes) {
292
- requireYesFlag(ctx, `change MCP trust mode for ${serverName}`, '/mcp trust <server> <constrained|ask-on-risk|blocked> --yes');
332
+ requireYesFlag(ctx, `change MCP trust mode for ${serverName}`, MCP_TRUST_COMMAND_USAGE);
293
333
  return;
294
334
  }
295
335
  mcpApi.setServerTrustMode(serverName, mode);
296
336
  ctx.print(`Updated MCP trust mode for ${serverName} to ${formatMcpTrustMode(mode)}.`);
297
337
  return;
298
338
  }
299
- if (serverName || mode) {
300
- ctx.print('Usage: /mcp trust <server> <constrained|ask-on-risk|blocked>\nUse /settings MCP to explicitly enable allow-all.');
339
+ if (serverName || requestedMode) {
340
+ ctx.print(`Usage: ${MCP_TRUST_COMMAND_USAGE}\nUse /settings -> MCP to explicitly enable allow-all.`);
301
341
  return;
302
342
  }
303
343
  }
304
344
 
305
345
  if (subcommand === 'role') {
306
346
  const serverName = commandArgs[1];
307
- const role = commandArgs[2] as 'general' | 'docs' | 'filesystem' | 'git' | 'database' | 'browser' | 'automation' | 'ops' | 'remote' | undefined;
308
- if (serverName && role) {
347
+ const requestedRole = commandArgs[2];
348
+ if (serverName && requestedRole) {
349
+ if (!isMcpRole(requestedRole)) {
350
+ ctx.print(`Invalid MCP role "${requestedRole}". Expected one of ${MCP_ROLES.join(', ')}.\nUsage: ${MCP_ROLE_COMMAND_USAGE}`);
351
+ return;
352
+ }
353
+ const role = requestedRole;
309
354
  if (!confirmation.yes) {
310
- requireYesFlag(ctx, `change MCP role for ${serverName}`, '/mcp role <server> <general|docs|filesystem|git|database|browser|automation|ops|remote> --yes');
355
+ requireYesFlag(ctx, `change MCP role for ${serverName}`, MCP_ROLE_COMMAND_USAGE);
311
356
  return;
312
357
  }
313
358
  mcpApi.setServerRole(serverName, role);
314
359
  ctx.print(`Updated MCP role for ${serverName} to ${formatMcpRole(role)}.`);
315
360
  return;
316
361
  }
317
- if (serverName || role) {
318
- ctx.print('Usage: /mcp role <server> <general|docs|filesystem|git|database|browser|automation|ops|remote>');
362
+ if (serverName || requestedRole) {
363
+ ctx.print(`Usage: ${MCP_ROLE_COMMAND_USAGE}`);
319
364
  return;
320
365
  }
321
366
  }
322
367
 
323
368
  if (subcommand === 'add') {
324
369
  if (!confirmation.yes) {
325
- requireYesFlag(ctx, 'add or update an MCP server config', '/mcp add <name> <command> [args...] [--scope project|global] [--role <role>] [--trust <mode>] --yes');
370
+ requireYesFlag(ctx, 'add or update an MCP server config', MCP_ADD_COMMAND_USAGE);
326
371
  return;
327
372
  }
328
373
  let parsedAdd: ParsedMcpAddArgs;
@@ -422,8 +467,10 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
422
467
  '',
423
468
  'Add or update from inside Agent with explicit confirmation',
424
469
  ' Open /mcp and choose Add or update server, or use Agent Workspace -> Tools & MCP -> Add MCP server.',
470
+ ' Use Settings -> MCP for allow-all decisions.',
471
+ ' Use -- before server args that look like Agent flags.',
425
472
  'Automation equivalent',
426
- ' /mcp add <name> <command> [args...] [--scope project|global] [--role <role>] [--trust <mode>] --yes',
473
+ ` ${MCP_ADD_COMMAND_USAGE}`,
427
474
  ].join('\n'));
428
475
  } catch (error) {
429
476
  ctx.print(`MCP config read failed ${summarizeError(error)}`);
@@ -2,14 +2,150 @@ import type { CommandRegistry } from '../command-registry.ts';
2
2
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
3
3
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
4
4
  import { requireYesFlag, stripYesFlag } from './confirmation.ts';
5
+ import {
6
+ formatHarnessError,
7
+ formatHarnessMutation,
8
+ formatHarnessSetting,
9
+ formatHarnessSettingList,
10
+ getHarnessSetting,
11
+ listHarnessSettings,
12
+ resetHarnessSetting,
13
+ setHarnessSetting,
14
+ } from '../../agent/harness-control.ts';
15
+
16
+ function readValuedFlag(args: readonly string[], flag: string): string | undefined {
17
+ const assignmentPrefix = `${flag}=`;
18
+ for (let index = 0; index < args.length; index += 1) {
19
+ const arg = args[index];
20
+ if (arg === flag) {
21
+ const value = args[index + 1];
22
+ return value && !value.startsWith('--') ? value : undefined;
23
+ }
24
+ if (arg?.startsWith(assignmentPrefix)) return arg.slice(assignmentPrefix.length);
25
+ }
26
+ return undefined;
27
+ }
28
+
29
+ function stripValuedFlag(args: readonly string[], flag: string): readonly string[] {
30
+ const next: string[] = [];
31
+ const assignmentPrefix = `${flag}=`;
32
+ for (let index = 0; index < args.length; index += 1) {
33
+ const arg = args[index];
34
+ if (arg === flag) {
35
+ const value = args[index + 1];
36
+ if (value && !value.startsWith('--')) index += 1;
37
+ continue;
38
+ }
39
+ if (arg?.startsWith(assignmentPrefix)) {
40
+ continue;
41
+ }
42
+ next.push(arg!);
43
+ }
44
+ return next;
45
+ }
46
+
47
+ function parseSettingListArgs(args: readonly string[]): {
48
+ readonly category?: string;
49
+ readonly prefix?: string;
50
+ readonly query?: string;
51
+ readonly includeHidden: boolean;
52
+ readonly limit?: number;
53
+ } {
54
+ const category = readValuedFlag(args, '--category');
55
+ const prefix = readValuedFlag(args, '--prefix');
56
+ const limitText = readValuedFlag(args, '--limit');
57
+ const remaining = stripValuedFlag(stripValuedFlag(stripValuedFlag(args, '--category'), '--prefix'), '--limit')
58
+ .filter((arg) => arg !== '--include-hidden');
59
+ const query = remaining.join(' ').trim();
60
+ return {
61
+ ...(category ? { category } : {}),
62
+ ...(prefix ? { prefix } : {}),
63
+ ...(query ? { query } : {}),
64
+ includeHidden: args.includes('--include-hidden'),
65
+ ...(limitText ? { limit: Number(limitText) } : {}),
66
+ };
67
+ }
5
68
 
6
69
  export function registerOperatorRuntimeCommands(registry: CommandRegistry): void {
7
70
  registry.register({
8
71
  name: 'settings',
9
72
  aliases: ['cfg-ui'],
10
- description: 'Open the fullscreen configuration workspace',
11
- handler(_args, ctx) {
12
- if (ctx.openSettingsModal) ctx.openSettingsModal();
73
+ description: 'Open, inspect, or update Agent settings',
74
+ usage: '[category|key|list|get <key>|set <key> <value> --yes|reset <key> --yes]',
75
+ argsHint: '[list|get|set|reset]',
76
+ async handler(args, ctx) {
77
+ const parsed = stripYesFlag(args);
78
+ const commandArgs = [...parsed.rest];
79
+ const sub = commandArgs[0];
80
+
81
+ if (!sub) {
82
+ if (ctx.openSettingsModal) ctx.openSettingsModal();
83
+ else ctx.print('Configuration workspace is not available in this runtime.');
84
+ return;
85
+ }
86
+
87
+ if (sub === 'list' || sub === 'schema') {
88
+ ctx.print(formatHarnessSettingList(listHarnessSettings(ctx.platform.configManager, parseSettingListArgs(commandArgs.slice(1)))));
89
+ return;
90
+ }
91
+
92
+ if (sub === 'get' || sub === 'show') {
93
+ const key = commandArgs[1];
94
+ if (!key) {
95
+ ctx.print('Usage: /settings get <key>');
96
+ return;
97
+ }
98
+ ctx.print(formatHarnessSetting(getHarnessSetting(ctx.platform.configManager, key)));
99
+ return;
100
+ }
101
+
102
+ if (sub === 'set') {
103
+ const key = commandArgs[1];
104
+ const rawValue = commandArgs.slice(2).join(' ');
105
+ if (!key || rawValue.length === 0) {
106
+ ctx.print('Usage: /settings set <key> <value> --yes');
107
+ return;
108
+ }
109
+ if (!parsed.yes) {
110
+ requireYesFlag(ctx, `set setting ${key}`, '/settings set <key> <value> --yes');
111
+ return;
112
+ }
113
+ try {
114
+ const result = await setHarnessSetting(ctx.platform.configManager, ctx.platform.secretsManager, key, rawValue);
115
+ ctx.print(formatHarnessMutation(result));
116
+ ctx.renderRequest();
117
+ } catch (error) {
118
+ ctx.print(formatHarnessError(error));
119
+ }
120
+ return;
121
+ }
122
+
123
+ if (sub === 'reset') {
124
+ const key = commandArgs[1];
125
+ if (!key) {
126
+ ctx.print('Usage: /settings reset <key> --yes');
127
+ return;
128
+ }
129
+ if (!parsed.yes) {
130
+ requireYesFlag(ctx, `reset setting ${key}`, '/settings reset <key> --yes');
131
+ return;
132
+ }
133
+ try {
134
+ const result = await resetHarnessSetting(ctx.platform.configManager, ctx.platform.secretsManager, key);
135
+ ctx.print(formatHarnessMutation(result));
136
+ ctx.renderRequest();
137
+ } catch (error) {
138
+ ctx.print(formatHarnessError(error));
139
+ }
140
+ return;
141
+ }
142
+
143
+ if (sub.includes('.')) {
144
+ ctx.print(formatHarnessSetting(getHarnessSetting(ctx.platform.configManager, sub)));
145
+ return;
146
+ }
147
+
148
+ if (ctx.openSettingsModal) ctx.openSettingsModal(sub);
13
149
  else ctx.print('Configuration workspace is not available in this runtime.');
14
150
  },
15
151
  });