@robota-sdk/agent-command 3.0.0-beta.64
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/LICENSE +21 -0
- package/dist/node/index.cjs +30 -0
- package/dist/node/index.d.ts +293 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +31 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +48 -0
- package/src/agent/__tests__/agent-command.test.ts +504 -0
- package/src/agent/agent-command-module.ts +82 -0
- package/src/agent/agent-command-parser.ts +180 -0
- package/src/agent/agent-command.ts +235 -0
- package/src/agent/index.ts +7 -0
- package/src/background/__tests__/background-command-module.test.ts +255 -0
- package/src/background/background-command-module.ts +53 -0
- package/src/background/background-command.ts +63 -0
- package/src/background/index.ts +6 -0
- package/src/compact/__tests__/compact-command-module.test.ts +162 -0
- package/src/compact/compact-command-module.ts +51 -0
- package/src/compact/compact-command.ts +21 -0
- package/src/compact/index.ts +6 -0
- package/src/context/__tests__/context-command-module.test.ts +294 -0
- package/src/context/context-command-module.ts +54 -0
- package/src/context/context-command.ts +298 -0
- package/src/context/index.ts +6 -0
- package/src/exit/__tests__/exit-command-module.test.ts +35 -0
- package/src/exit/exit-command-module.ts +48 -0
- package/src/exit/exit-command.ts +10 -0
- package/src/exit/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +106 -0
- package/src/help/help-command-module.ts +48 -0
- package/src/help/help-command.ts +9 -0
- package/src/help/index.ts +6 -0
- package/src/index.ts +20 -0
- package/src/language/__tests__/language-command-module.test.ts +105 -0
- package/src/language/index.ts +6 -0
- package/src/language/language-command-module.ts +56 -0
- package/src/language/language-command.ts +22 -0
- package/src/memory/__tests__/memory-command-module.test.ts +272 -0
- package/src/memory/index.ts +6 -0
- package/src/memory/memory-command-module.ts +57 -0
- package/src/memory/memory-command.ts +234 -0
- package/src/mode/__tests__/mode-command-module.test.ts +143 -0
- package/src/mode/index.ts +6 -0
- package/src/mode/mode-command-module.ts +56 -0
- package/src/mode/mode-command.ts +34 -0
- package/src/model/__tests__/model-command-module.test.ts +273 -0
- package/src/model/index.ts +6 -0
- package/src/model/model-command-module.ts +68 -0
- package/src/model/model-command.ts +40 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +164 -0
- package/src/permissions/index.ts +6 -0
- package/src/permissions/permissions-command-module.ts +56 -0
- package/src/permissions/permissions-command.ts +45 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +214 -0
- package/src/plugin/index.ts +7 -0
- package/src/plugin/plugin-command-module.ts +81 -0
- package/src/plugin/plugin-command.ts +230 -0
- package/src/provider/__tests__/provider-command-module.test.ts +488 -0
- package/src/provider/__tests__/provider-setup-flow.test.ts +43 -0
- package/src/provider/index.ts +30 -0
- package/src/provider/provider-command-execution.ts +150 -0
- package/src/provider/provider-command-module.ts +65 -0
- package/src/provider/provider-command-profile-lifecycle.ts +211 -0
- package/src/provider/provider-command-profile-operations.ts +198 -0
- package/src/provider/provider-command-profile.ts +109 -0
- package/src/provider/provider-command-setup.ts +104 -0
- package/src/provider/provider-setup-flow.ts +309 -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 +49 -0
- package/src/reset/reset-command.ts +10 -0
- package/src/rewind/__tests__/rewind-command-module.test.ts +215 -0
- package/src/rewind/index.ts +2 -0
- package/src/rewind/rewind-command-module.ts +57 -0
- package/src/rewind/rewind-command.ts +184 -0
- package/src/session/__tests__/session-command-module.test.ts +339 -0
- package/src/session/index.ts +17 -0
- package/src/session/session-command-module.ts +168 -0
- package/src/session/session-command.ts +74 -0
- package/src/settings/index.ts +7 -0
- package/src/settings/settings-command-module.ts +50 -0
- package/src/skills/__tests__/skills-command-module.test.ts +157 -0
- package/src/skills/index.ts +6 -0
- package/src/skills/skills-command-module.ts +62 -0
- package/src/skills/skills-command.ts +110 -0
- package/src/statusline/__tests__/statusline-command-module.test.ts +95 -0
- package/src/statusline/index.ts +6 -0
- package/src/statusline/statusline-command-module.ts +56 -0
- package/src/statusline/statusline-command.ts +79 -0
- package/src/user-local/__tests__/user-local-command.test.ts +145 -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 +67 -0
- package/src/user-local/user-local-command.ts +205 -0
- package/src/user-local/user-local-memory-command.ts +147 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ICommandInteraction,
|
|
3
|
+
ICommandResult,
|
|
4
|
+
IProviderCommandModuleOptions,
|
|
5
|
+
IProviderProfileSettings,
|
|
6
|
+
} from '@robota-sdk/agent-framework';
|
|
7
|
+
import { testProviderProfileCommand } from '@robota-sdk/agent-framework';
|
|
8
|
+
import {
|
|
9
|
+
buildProviderEdit,
|
|
10
|
+
buildProviderSwitch,
|
|
11
|
+
formatProviderChoiceLabel,
|
|
12
|
+
} from './provider-command-profile-operations.js';
|
|
13
|
+
import {
|
|
14
|
+
buildProviderDelete,
|
|
15
|
+
buildProviderDuplicate,
|
|
16
|
+
} from './provider-command-profile-lifecycle.js';
|
|
17
|
+
|
|
18
|
+
const ACTION_SWITCH = 'switch';
|
|
19
|
+
const ACTION_EDIT = 'edit';
|
|
20
|
+
const ACTION_TEST = 'test';
|
|
21
|
+
const ACTION_DUPLICATE = 'duplicate';
|
|
22
|
+
const ACTION_DELETE = 'delete';
|
|
23
|
+
const ACTION_CANCEL = 'cancel';
|
|
24
|
+
|
|
25
|
+
export function createProviderProfileSelectionInteraction(
|
|
26
|
+
currentProvider: string | undefined,
|
|
27
|
+
providers: Record<string, IProviderProfileSettings> | undefined,
|
|
28
|
+
options: IProviderCommandModuleOptions,
|
|
29
|
+
): ICommandInteraction {
|
|
30
|
+
return {
|
|
31
|
+
prompt: {
|
|
32
|
+
kind: 'choice',
|
|
33
|
+
title: 'Select provider profile',
|
|
34
|
+
options: Object.entries(providers ?? {}).map(([name, profile]) => ({
|
|
35
|
+
value: name,
|
|
36
|
+
label: formatProviderChoiceLabel(name, profile, currentProvider),
|
|
37
|
+
})),
|
|
38
|
+
maxVisible: 8,
|
|
39
|
+
},
|
|
40
|
+
submit: (value) => buildProviderProfileActionMenu(value, options),
|
|
41
|
+
cancel: () => ({ message: 'Provider profile selection cancelled.', success: true }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildProviderProfileActionMenu(
|
|
46
|
+
profileName: string,
|
|
47
|
+
options: IProviderCommandModuleOptions,
|
|
48
|
+
): ICommandResult {
|
|
49
|
+
const settings = options.settings.readMergedSettings();
|
|
50
|
+
if (!settings.providers?.[profileName]) {
|
|
51
|
+
return { message: `Provider profile "${profileName}" was not found.`, success: false };
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
message: `Provider profile selected: ${profileName}`,
|
|
55
|
+
success: true,
|
|
56
|
+
interaction: createProviderProfileActionInteraction(profileName, options),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createProviderProfileActionInteraction(
|
|
61
|
+
profileName: string,
|
|
62
|
+
options: IProviderCommandModuleOptions,
|
|
63
|
+
): ICommandInteraction {
|
|
64
|
+
return {
|
|
65
|
+
prompt: {
|
|
66
|
+
kind: 'choice',
|
|
67
|
+
title: `Provider profile: ${profileName}`,
|
|
68
|
+
options: [
|
|
69
|
+
{ value: ACTION_SWITCH, label: 'Switch' },
|
|
70
|
+
{ value: ACTION_EDIT, label: 'Edit' },
|
|
71
|
+
{ value: ACTION_TEST, label: 'Test' },
|
|
72
|
+
{ value: ACTION_DUPLICATE, label: 'Duplicate' },
|
|
73
|
+
{ value: ACTION_DELETE, label: 'Delete' },
|
|
74
|
+
{ value: ACTION_CANCEL, label: 'Cancel' },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
submit: (value) => executeProviderProfileAction(profileName, value, options),
|
|
78
|
+
cancel: () => ({ message: 'Provider profile action cancelled.', success: true }),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function executeProviderProfileAction(
|
|
83
|
+
profileName: string,
|
|
84
|
+
action: string,
|
|
85
|
+
options: IProviderCommandModuleOptions,
|
|
86
|
+
): Promise<ICommandResult> {
|
|
87
|
+
const settings = options.settings.readMergedSettings();
|
|
88
|
+
switch (action) {
|
|
89
|
+
case ACTION_SWITCH:
|
|
90
|
+
return buildProviderSwitch(settings.providers, profileName, options);
|
|
91
|
+
case ACTION_EDIT:
|
|
92
|
+
return buildProviderEdit(profileName, options);
|
|
93
|
+
case ACTION_TEST:
|
|
94
|
+
return await testProviderProfileCommand(
|
|
95
|
+
settings.currentProvider,
|
|
96
|
+
settings.providers,
|
|
97
|
+
profileName,
|
|
98
|
+
options,
|
|
99
|
+
);
|
|
100
|
+
case ACTION_DUPLICATE:
|
|
101
|
+
return buildProviderDuplicate(profileName, options);
|
|
102
|
+
case ACTION_DELETE:
|
|
103
|
+
return buildProviderDelete(profileName, options);
|
|
104
|
+
case ACTION_CANCEL:
|
|
105
|
+
return { message: 'Provider profile action cancelled.', success: true };
|
|
106
|
+
default:
|
|
107
|
+
return { message: `Unknown provider profile action "${action}".`, success: false };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ICommandInteraction,
|
|
3
|
+
ICommandResult,
|
|
4
|
+
IProviderCommandModuleOptions,
|
|
5
|
+
IProviderSetupInput,
|
|
6
|
+
TCommandInteractionPrompt,
|
|
7
|
+
} from '@robota-sdk/agent-framework';
|
|
8
|
+
import { buildProviderSetupPatch, mergeProviderPatch } from '@robota-sdk/agent-framework';
|
|
9
|
+
import type { IProviderSetupFlowState } from './provider-setup-flow.js';
|
|
10
|
+
import {
|
|
11
|
+
createProviderSetupFlow,
|
|
12
|
+
formatProviderSetupHelpLinks,
|
|
13
|
+
getProviderSetupStep,
|
|
14
|
+
submitProviderSetupValue,
|
|
15
|
+
validateProviderSetupValue,
|
|
16
|
+
} from './provider-setup-flow.js';
|
|
17
|
+
|
|
18
|
+
const PROVIDER_RESTART_EFFECT = {
|
|
19
|
+
type: 'session-restart-requested',
|
|
20
|
+
reason: 'other',
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export function createSetupFlow(
|
|
24
|
+
type: string,
|
|
25
|
+
options: IProviderCommandModuleOptions,
|
|
26
|
+
): IProviderSetupFlowState {
|
|
27
|
+
return createProviderSetupFlow(type, options.providerDefinitions, {
|
|
28
|
+
existingProfileNames: Object.keys(options.settings.readMergedSettings().providers ?? {}),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createProviderSetupInteraction(
|
|
33
|
+
flow: IProviderSetupFlowState,
|
|
34
|
+
options: IProviderCommandModuleOptions,
|
|
35
|
+
): ICommandInteraction {
|
|
36
|
+
return {
|
|
37
|
+
prompt: toProviderSetupStepPrompt(flow),
|
|
38
|
+
submit: (value) => submitProviderSetupInteractionValue(flow, value, options),
|
|
39
|
+
cancel: () => ({ message: 'Provider setup cancelled.', success: true }),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function toProviderSetupStepPrompt(
|
|
44
|
+
flow: IProviderSetupFlowState,
|
|
45
|
+
): TCommandInteractionPrompt {
|
|
46
|
+
const step = getProviderSetupStep(flow);
|
|
47
|
+
const placeholder =
|
|
48
|
+
step.masked === true && step.defaultValue !== undefined ? '(unchanged)' : step.defaultValue;
|
|
49
|
+
return {
|
|
50
|
+
kind: 'text',
|
|
51
|
+
title: step.title,
|
|
52
|
+
...toProviderSetupPromptDescription(flow),
|
|
53
|
+
...(placeholder !== undefined ? { placeholder } : {}),
|
|
54
|
+
...(step.defaultValue !== undefined ? { allowEmpty: true } : {}),
|
|
55
|
+
...(step.masked !== undefined ? { masked: step.masked } : {}),
|
|
56
|
+
validate: (value) => validateProviderSetupValue(step, value),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function toProviderSetupPromptDescription(
|
|
61
|
+
flow: IProviderSetupFlowState,
|
|
62
|
+
): { description: string } | Record<string, never> {
|
|
63
|
+
const description = formatProviderSetupHelpLinks(flow.setupHelpLinks);
|
|
64
|
+
return description.length > 0 ? { description } : {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function submitProviderSetupInteractionValue(
|
|
68
|
+
flow: IProviderSetupFlowState,
|
|
69
|
+
value: string,
|
|
70
|
+
options: IProviderCommandModuleOptions,
|
|
71
|
+
): ICommandResult {
|
|
72
|
+
const result = submitProviderSetupValue(flow, value);
|
|
73
|
+
if (result.status === 'error') {
|
|
74
|
+
return {
|
|
75
|
+
message: result.message,
|
|
76
|
+
success: false,
|
|
77
|
+
interaction: createProviderSetupInteraction(flow, options),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (result.status === 'complete') {
|
|
81
|
+
return completeProviderSetup(result.input, options);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
message: '',
|
|
85
|
+
success: true,
|
|
86
|
+
interaction: createProviderSetupInteraction(result.state, options),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function completeProviderSetup(
|
|
91
|
+
input: IProviderSetupInput,
|
|
92
|
+
options: IProviderCommandModuleOptions,
|
|
93
|
+
): ICommandResult {
|
|
94
|
+
const target = options.settings.readTargetSettings();
|
|
95
|
+
const patch = buildProviderSetupPatch(input, {
|
|
96
|
+
providerDefinitions: options.providerDefinitions,
|
|
97
|
+
});
|
|
98
|
+
options.settings.writeTargetSettings(mergeProviderPatch(target, patch));
|
|
99
|
+
return {
|
|
100
|
+
message: `Provider ${input.profile} configured. Restarting...`,
|
|
101
|
+
success: true,
|
|
102
|
+
effects: [{ ...PROVIDER_RESTART_EFFECT, message: 'Provider setup restart' }],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IProviderDefinition,
|
|
3
|
+
IProviderSetupHelpLink,
|
|
4
|
+
IProviderSetupStepDefinition,
|
|
5
|
+
TProviderSetupField,
|
|
6
|
+
} from '@robota-sdk/agent-core';
|
|
7
|
+
import { findProviderDefinition, formatSupportedProviderTypes } from '@robota-sdk/agent-core';
|
|
8
|
+
import type { IProviderSetupInput } from '@robota-sdk/agent-framework';
|
|
9
|
+
import { suggestProviderProfileName } from '@robota-sdk/agent-framework';
|
|
10
|
+
|
|
11
|
+
export type TProviderSetupType = string;
|
|
12
|
+
export type TPromptInput = (label: string, masked?: boolean) => Promise<string>;
|
|
13
|
+
|
|
14
|
+
export interface IProviderSetupPromptStep extends IProviderSetupStepDefinition {
|
|
15
|
+
key: TProviderSetupField;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface IProviderSetupFlowState {
|
|
19
|
+
type: TProviderSetupType;
|
|
20
|
+
steps: readonly IProviderSetupPromptStep[];
|
|
21
|
+
setupHelpLinks: readonly IProviderSetupHelpLink[];
|
|
22
|
+
stepIndex: number;
|
|
23
|
+
values: Partial<Record<TProviderSetupField, string>>;
|
|
24
|
+
existingProfileNames: readonly string[];
|
|
25
|
+
profileName?: string;
|
|
26
|
+
setCurrent?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IProviderSetupFlowOptions {
|
|
30
|
+
existingProfileNames?: readonly string[];
|
|
31
|
+
initialValues?: Partial<Record<TProviderSetupField, string>>;
|
|
32
|
+
profileName?: string;
|
|
33
|
+
setCurrent?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type TProviderSetupFlowSubmitResult =
|
|
37
|
+
| {
|
|
38
|
+
status: 'next';
|
|
39
|
+
state: IProviderSetupFlowState;
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
status: 'complete';
|
|
43
|
+
input: IProviderSetupInput;
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
status: 'error';
|
|
47
|
+
state: IProviderSetupFlowState;
|
|
48
|
+
message: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function createProviderSetupFlow(
|
|
52
|
+
type: TProviderSetupType,
|
|
53
|
+
providerDefinitions: readonly IProviderDefinition[],
|
|
54
|
+
options: IProviderSetupFlowOptions = {},
|
|
55
|
+
): IProviderSetupFlowState {
|
|
56
|
+
const definition = getProviderSetupDefinition(type, providerDefinitions);
|
|
57
|
+
return {
|
|
58
|
+
type,
|
|
59
|
+
steps: applyProviderSetupInitialValues(
|
|
60
|
+
getProviderSetupSteps(definition),
|
|
61
|
+
options.initialValues,
|
|
62
|
+
),
|
|
63
|
+
setupHelpLinks: definition.setupHelpLinks ?? [],
|
|
64
|
+
stepIndex: 0,
|
|
65
|
+
values: {},
|
|
66
|
+
existingProfileNames: options.existingProfileNames ?? [],
|
|
67
|
+
...(options.profileName !== undefined ? { profileName: options.profileName } : {}),
|
|
68
|
+
...(options.setCurrent !== undefined ? { setCurrent: options.setCurrent } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function formatProviderSetupSelectionPrompt(
|
|
73
|
+
providerDefinitions: readonly IProviderDefinition[],
|
|
74
|
+
): string {
|
|
75
|
+
if (providerDefinitions.length === 0) {
|
|
76
|
+
return ' No providers are available.';
|
|
77
|
+
}
|
|
78
|
+
const lines = [
|
|
79
|
+
' Select provider:',
|
|
80
|
+
...providerDefinitions.map(
|
|
81
|
+
(definition, index) => ` ${index + 1}. ${formatProviderSetupChoiceLabel(definition)}`,
|
|
82
|
+
),
|
|
83
|
+
` Provider [1-${providerDefinitions.length}] (default: 1): `,
|
|
84
|
+
];
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function resolveProviderSetupSelection(
|
|
89
|
+
rawValue: string,
|
|
90
|
+
providerDefinitions: readonly IProviderDefinition[],
|
|
91
|
+
): TProviderSetupType {
|
|
92
|
+
const value = rawValue.trim();
|
|
93
|
+
const selectedValue = value.length > 0 ? value : '1';
|
|
94
|
+
const index = parseProviderSelectionIndex(selectedValue);
|
|
95
|
+
if (index !== undefined) {
|
|
96
|
+
const definition = providerDefinitions[index];
|
|
97
|
+
if (definition !== undefined) {
|
|
98
|
+
return definition.type;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Provider selection ${selectedValue} is out of range. Currently supported: ${formatSupportedProviderTypes(providerDefinitions)}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const definition = findProviderDefinition(providerDefinitions, selectedValue);
|
|
105
|
+
if (definition === undefined) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Unknown provider: ${selectedValue}. Currently supported: ${formatSupportedProviderTypes(providerDefinitions)}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return definition.type;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getProviderSetupStep(state: IProviderSetupFlowState): IProviderSetupPromptStep {
|
|
114
|
+
const step = state.steps[state.stepIndex];
|
|
115
|
+
if (step === undefined) {
|
|
116
|
+
throw new Error(`Provider setup step ${state.stepIndex} is out of range`);
|
|
117
|
+
}
|
|
118
|
+
return step;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function submitProviderSetupValue(
|
|
122
|
+
state: IProviderSetupFlowState,
|
|
123
|
+
rawValue: string,
|
|
124
|
+
): TProviderSetupFlowSubmitResult {
|
|
125
|
+
const step = getProviderSetupStep(state);
|
|
126
|
+
const value = rawValue.trim() || step.defaultValue || '';
|
|
127
|
+
const validationMessage = validateProviderSetupValue(step, value);
|
|
128
|
+
if (validationMessage !== undefined) {
|
|
129
|
+
return { status: 'error', state, message: validationMessage };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const nextState = {
|
|
133
|
+
...state,
|
|
134
|
+
stepIndex: state.stepIndex + 1,
|
|
135
|
+
values: { ...state.values, [step.key]: value },
|
|
136
|
+
};
|
|
137
|
+
if (nextState.stepIndex < state.steps.length) {
|
|
138
|
+
return { status: 'next', state: nextState };
|
|
139
|
+
}
|
|
140
|
+
return { status: 'complete', input: buildProviderSetupInput(nextState) };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function runProviderSetupPromptFlow(
|
|
144
|
+
type: TProviderSetupType,
|
|
145
|
+
promptInput: TPromptInput,
|
|
146
|
+
providerDefinitions: readonly IProviderDefinition[],
|
|
147
|
+
options: IProviderSetupFlowOptions = {},
|
|
148
|
+
): Promise<IProviderSetupInput> {
|
|
149
|
+
let state = createProviderSetupFlow(type, providerDefinitions, options);
|
|
150
|
+
const stepCount = state.steps.length;
|
|
151
|
+
while (state.stepIndex < stepCount) {
|
|
152
|
+
const step = getProviderSetupStep(state);
|
|
153
|
+
const value = await promptInput(
|
|
154
|
+
formatProviderSetupPromptLabel(step, state.setupHelpLinks),
|
|
155
|
+
step.masked === true,
|
|
156
|
+
);
|
|
157
|
+
const result = submitProviderSetupValue(state, value);
|
|
158
|
+
if (result.status === 'complete') {
|
|
159
|
+
return result.input;
|
|
160
|
+
}
|
|
161
|
+
if (result.status === 'error') {
|
|
162
|
+
throw new Error(result.message);
|
|
163
|
+
}
|
|
164
|
+
state = result.state;
|
|
165
|
+
}
|
|
166
|
+
throw new Error('Provider setup flow ended without completion');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function formatProviderSetupPromptLabel(
|
|
170
|
+
step: IProviderSetupPromptStep,
|
|
171
|
+
setupHelpLinks: readonly IProviderSetupHelpLink[] = [],
|
|
172
|
+
): string {
|
|
173
|
+
const suffix = step.defaultValue !== undefined ? ` (default: ${step.defaultValue})` : '';
|
|
174
|
+
const setupHelp = formatProviderSetupHelpLinks(setupHelpLinks);
|
|
175
|
+
const prefix = setupHelp.length > 0 ? `${setupHelp}\n` : '';
|
|
176
|
+
return `${prefix} ${step.title}${suffix}: `;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function formatProviderSetupChoiceLabel(definition: IProviderDefinition): string {
|
|
180
|
+
const label =
|
|
181
|
+
definition.displayName !== undefined
|
|
182
|
+
? `${definition.displayName} (${definition.type})`
|
|
183
|
+
: definition.type;
|
|
184
|
+
return definition.description !== undefined ? `${label} - ${definition.description}` : label;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function formatProviderSetupHelpLinks(
|
|
188
|
+
setupHelpLinks: readonly IProviderSetupHelpLink[] = [],
|
|
189
|
+
): string {
|
|
190
|
+
if (setupHelpLinks.length === 0) {
|
|
191
|
+
return '';
|
|
192
|
+
}
|
|
193
|
+
return setupHelpLinks
|
|
194
|
+
.map(
|
|
195
|
+
(link) =>
|
|
196
|
+
` Setup help: ${formatProviderSetupHelpLinkKind(link.kind)}: ${link.label} - ${link.url}`,
|
|
197
|
+
)
|
|
198
|
+
.join('\n');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseProviderSelectionIndex(value: string): number | undefined {
|
|
202
|
+
if (!/^\d+$/.test(value)) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
return Number(value) - 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function validateProviderSetupValue(
|
|
209
|
+
step: IProviderSetupPromptStep,
|
|
210
|
+
value: string,
|
|
211
|
+
): string | undefined {
|
|
212
|
+
if (step.required === true && value.length === 0) {
|
|
213
|
+
return 'Required';
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getProviderSetupDefinition(
|
|
219
|
+
type: TProviderSetupType,
|
|
220
|
+
providerDefinitions: readonly IProviderDefinition[],
|
|
221
|
+
): IProviderDefinition {
|
|
222
|
+
const definition = findProviderDefinition(providerDefinitions, type);
|
|
223
|
+
if (definition === undefined) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Unknown provider: ${type}. Currently supported: ${formatSupportedProviderTypes(providerDefinitions)}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return definition;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getProviderSetupSteps(definition: IProviderDefinition): IProviderSetupPromptStep[] {
|
|
232
|
+
if (definition.setupSteps !== undefined) {
|
|
233
|
+
return [...definition.setupSteps];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const steps: IProviderSetupPromptStep[] = [
|
|
237
|
+
{
|
|
238
|
+
key: 'model',
|
|
239
|
+
title: `${definition.type} model`,
|
|
240
|
+
defaultValue: definition.defaults?.model,
|
|
241
|
+
required: definition.defaults?.model === undefined,
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
if (definition.defaults?.baseURL !== undefined) {
|
|
245
|
+
steps.unshift({
|
|
246
|
+
key: 'baseURL',
|
|
247
|
+
title: `${definition.type} base URL`,
|
|
248
|
+
defaultValue: definition.defaults.baseURL,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (definition.requiresApiKey === true) {
|
|
252
|
+
steps.push({
|
|
253
|
+
key: 'apiKey',
|
|
254
|
+
title: `${definition.type} API key`,
|
|
255
|
+
defaultValue: definition.defaults?.apiKey,
|
|
256
|
+
required: definition.defaults?.apiKey === undefined,
|
|
257
|
+
masked: true,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return steps;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function formatProviderSetupHelpLinkKind(kind: IProviderSetupHelpLink['kind']): string {
|
|
264
|
+
if (kind === 'api-key') {
|
|
265
|
+
return 'API key';
|
|
266
|
+
}
|
|
267
|
+
if (kind === 'console') {
|
|
268
|
+
return 'Console';
|
|
269
|
+
}
|
|
270
|
+
return 'Official';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function applyProviderSetupInitialValues(
|
|
274
|
+
steps: readonly IProviderSetupPromptStep[],
|
|
275
|
+
initialValues: Partial<Record<TProviderSetupField, string>> | undefined,
|
|
276
|
+
): IProviderSetupPromptStep[] {
|
|
277
|
+
if (initialValues === undefined) {
|
|
278
|
+
return [...steps];
|
|
279
|
+
}
|
|
280
|
+
return steps.map((step) => {
|
|
281
|
+
const initialValue = initialValues[step.key];
|
|
282
|
+
if (initialValue === undefined) {
|
|
283
|
+
return step;
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
...step,
|
|
287
|
+
defaultValue: initialValue,
|
|
288
|
+
required: false,
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function buildProviderSetupInput(state: IProviderSetupFlowState): IProviderSetupInput {
|
|
294
|
+
const profile =
|
|
295
|
+
state.profileName ??
|
|
296
|
+
suggestProviderProfileName(
|
|
297
|
+
{ type: state.type },
|
|
298
|
+
{ existingProfileNames: state.existingProfileNames },
|
|
299
|
+
);
|
|
300
|
+
const apiKey = state.values.apiKey;
|
|
301
|
+
return {
|
|
302
|
+
profile,
|
|
303
|
+
type: state.type,
|
|
304
|
+
model: state.values.model,
|
|
305
|
+
...(apiKey !== undefined && apiKey.length > 0 && { apiKey }),
|
|
306
|
+
...(state.values.baseURL !== undefined && { baseURL: state.values.baseURL }),
|
|
307
|
+
setCurrent: state.setCurrent ?? true,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { ICommandHostContext } from '@robota-sdk/agent-framework';
|
|
3
|
+
import { SystemCommandExecutor } from '@robota-sdk/agent-framework';
|
|
4
|
+
import {
|
|
5
|
+
createResetCommandEntry,
|
|
6
|
+
createResetCommandModule,
|
|
7
|
+
executeResetCommand,
|
|
8
|
+
} from '../index.js';
|
|
9
|
+
|
|
10
|
+
const COMMAND_CONTEXT = {} as ICommandHostContext;
|
|
11
|
+
|
|
12
|
+
describe('createResetCommandModule', () => {
|
|
13
|
+
it('provides reset metadata and a user-only executable command from one module owner', () => {
|
|
14
|
+
const module = createResetCommandModule();
|
|
15
|
+
const command = module.systemCommands?.[0];
|
|
16
|
+
const entry = module.commandSources?.[0]?.getCommands()[0];
|
|
17
|
+
|
|
18
|
+
expect(module.name).toBe('agent-command-reset');
|
|
19
|
+
expect(createResetCommandEntry()).toEqual({
|
|
20
|
+
name: 'reset',
|
|
21
|
+
displayName: 'Reset Settings',
|
|
22
|
+
description: 'Delete settings',
|
|
23
|
+
source: 'reset',
|
|
24
|
+
modelInvocable: false,
|
|
25
|
+
});
|
|
26
|
+
expect(entry).toEqual(createResetCommandEntry());
|
|
27
|
+
expect(command).toEqual(
|
|
28
|
+
expect.objectContaining({
|
|
29
|
+
name: 'reset',
|
|
30
|
+
description: 'Delete settings',
|
|
31
|
+
lifecycle: 'inline',
|
|
32
|
+
userInvocable: true,
|
|
33
|
+
modelInvocable: false,
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('emits a typed settings reset effect without host file I/O', () => {
|
|
39
|
+
const result = executeResetCommand(COMMAND_CONTEXT, '');
|
|
40
|
+
|
|
41
|
+
expect(result).toEqual({
|
|
42
|
+
success: true,
|
|
43
|
+
message: 'Reset requested.',
|
|
44
|
+
data: { resetRequested: true },
|
|
45
|
+
effects: [{ type: 'settings-reset-requested' }],
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('executes through the SDK system command executor', async () => {
|
|
50
|
+
const executor = new SystemCommandExecutor([
|
|
51
|
+
...(createResetCommandModule().systemCommands ?? []),
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const result = await executor.execute('reset', COMMAND_CONTEXT, '');
|
|
55
|
+
|
|
56
|
+
expect(result).toEqual({
|
|
57
|
+
success: true,
|
|
58
|
+
message: 'Reset requested.',
|
|
59
|
+
data: { resetRequested: true },
|
|
60
|
+
effects: [{ type: 'settings-reset-requested' }],
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ICommand,
|
|
3
|
+
ICommandModule,
|
|
4
|
+
ICommandSource,
|
|
5
|
+
ISystemCommand,
|
|
6
|
+
} from '@robota-sdk/agent-framework';
|
|
7
|
+
import { executeResetCommand } from './reset-command.js';
|
|
8
|
+
|
|
9
|
+
const RESET_COMMAND_DESCRIPTION = 'Delete settings';
|
|
10
|
+
|
|
11
|
+
export function createResetCommandEntry(): ICommand {
|
|
12
|
+
return {
|
|
13
|
+
name: 'reset',
|
|
14
|
+
displayName: 'Reset Settings',
|
|
15
|
+
description: RESET_COMMAND_DESCRIPTION,
|
|
16
|
+
source: 'reset',
|
|
17
|
+
modelInvocable: false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createResetSystemCommand(): ISystemCommand {
|
|
22
|
+
const entry = createResetCommandEntry();
|
|
23
|
+
return {
|
|
24
|
+
name: entry.name,
|
|
25
|
+
displayName: entry.displayName,
|
|
26
|
+
description: entry.description,
|
|
27
|
+
requiresPermission: true,
|
|
28
|
+
userInvocable: true,
|
|
29
|
+
modelInvocable: false,
|
|
30
|
+
lifecycle: 'inline',
|
|
31
|
+
execute: executeResetCommand,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class ResetCommandSource implements ICommandSource {
|
|
36
|
+
readonly name = 'reset';
|
|
37
|
+
|
|
38
|
+
getCommands(): ICommand[] {
|
|
39
|
+
return [createResetCommandEntry()];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createResetCommandModule(): ICommandModule {
|
|
44
|
+
return {
|
|
45
|
+
name: 'agent-command-reset',
|
|
46
|
+
commandSources: [new ResetCommandSource()],
|
|
47
|
+
systemCommands: [createResetSystemCommand()],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ICommandHostContext, ICommandResult } from '@robota-sdk/agent-framework';
|
|
2
|
+
|
|
3
|
+
export function executeResetCommand(_context: ICommandHostContext, _args: string): ICommandResult {
|
|
4
|
+
return {
|
|
5
|
+
success: true,
|
|
6
|
+
message: 'Reset requested.',
|
|
7
|
+
data: { resetRequested: true },
|
|
8
|
+
effects: [{ type: 'settings-reset-requested' }],
|
|
9
|
+
};
|
|
10
|
+
}
|