@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.
- package/CHANGELOG.md +14 -0
- package/README.md +8 -4
- package/dist/package/main.js +14046 -12557
- package/docs/README.md +4 -2
- package/docs/channels-remote-and-api.md +4 -0
- package/docs/connected-host.md +2 -0
- package/docs/getting-started.md +6 -2
- package/docs/knowledge-artifacts-and-multimodal.md +5 -3
- package/docs/project-planning.md +3 -1
- package/docs/providers-and-routing.md +3 -0
- package/docs/release-and-publishing.md +7 -6
- package/docs/tools-and-commands.md +97 -2
- package/docs/voice-and-live-tts.md +2 -0
- package/package.json +1 -1
- package/src/agent/harness-control.ts +266 -0
- package/src/cli/local-library-command.ts +25 -5
- package/src/cli/routines-command.ts +15 -3
- package/src/config/agent-settings-policy.ts +44 -0
- package/src/input/agent-workspace-activation.ts +14 -6
- package/src/input/agent-workspace-knowledge-url-editor.ts +4 -11
- package/src/input/commands/agent-local-library-args.ts +52 -0
- package/src/input/commands/agent-skills-runtime.ts +4 -29
- package/src/input/commands/mcp-runtime.ts +62 -15
- package/src/input/commands/operator-runtime.ts +139 -3
- package/src/input/commands/personas-runtime.ts +4 -29
- package/src/input/commands/routines-runtime.ts +4 -29
- package/src/input/session-picker-modal.ts +3 -2
- package/src/input/settings-modal-agent-policy.ts +5 -44
- package/src/runtime/bootstrap.ts +3 -0
- package/src/tools/agent-harness-local-operations.ts +233 -0
- package/src/tools/agent-harness-metadata.ts +300 -0
- package/src/tools/agent-harness-tool.ts +774 -0
- package/src/tools/agent-local-registry-requirements.ts +18 -0
- package/src/tools/agent-local-registry-tool.ts +88 -34
- 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
|
|
82
|
+
const raw = arg.slice(2);
|
|
83
|
+
const equalIndex = raw.indexOf('=');
|
|
64
84
|
if (equalIndex >= 0) {
|
|
65
|
-
values.set(
|
|
85
|
+
values.set(raw.slice(0, equalIndex), raw.slice(equalIndex + 1));
|
|
66
86
|
continue;
|
|
67
87
|
}
|
|
68
|
-
const name =
|
|
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
|
|
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 =
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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[]):
|
|
22
|
-
|
|
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(
|
|
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
|
|
285
|
-
if (serverName &&
|
|
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}`,
|
|
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 ||
|
|
300
|
-
ctx.print(
|
|
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
|
|
308
|
-
if (serverName &&
|
|
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}`,
|
|
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 ||
|
|
318
|
-
ctx.print(
|
|
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',
|
|
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
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
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
|
});
|