@dev-mainsequence/codex-plugin 0.3.0 → 0.7.9

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 (39) hide show
  1. package/.codex-plugin/plugin.json +6 -5
  2. package/.mcp.json +17 -0
  3. package/README.md +49 -11
  4. package/assets/mainsequence-mark.svg +453 -0
  5. package/build-info.json +3 -3
  6. package/dist/hosts/codexPresentation.js +1 -0
  7. package/dist/hosts/discovery.js +252 -0
  8. package/dist/hosts/messaging.js +63 -0
  9. package/dist/hosts/panel/panel.html +265 -0
  10. package/dist/hosts/workspace.js +107 -0
  11. package/dist/panel-server.js +223 -0
  12. package/dist/vendor/devtools/index.js +45 -1
  13. package/dist/vendor/devtools/package.json +1 -1
  14. package/dist/vendor/devtools/shared/accountScope.js +19 -0
  15. package/dist/vendor/devtools/shared/actions.js +44 -7
  16. package/dist/vendor/devtools/shared/agentAssets.js +38 -0
  17. package/dist/vendor/devtools/shared/agentDelivery.js +477 -0
  18. package/dist/vendor/devtools/shared/agentIcon.js +43 -0
  19. package/dist/vendor/devtools/shared/agents.js +18 -7
  20. package/dist/vendor/devtools/shared/arguments.js +16 -9
  21. package/dist/vendor/devtools/shared/deployments.js +214 -0
  22. package/dist/vendor/devtools/shared/environments.js +38 -0
  23. package/dist/vendor/devtools/shared/mcp.js +237 -46
  24. package/dist/vendor/devtools/shared/mcpTransport.js +79 -23
  25. package/dist/vendor/devtools/shared/organizationProjectWorkspace.js +8 -3
  26. package/dist/vendor/devtools/shared/organizationProjects.js +2 -13
  27. package/dist/vendor/devtools/shared/projectTools.js +437 -0
  28. package/dist/vendor/devtools/shared/repositories.js +2 -2
  29. package/dist/vendor/devtools/shared/session.js +133 -14
  30. package/dist/vendor/devtools/shared/sharedTransport.js +506 -0
  31. package/dist/vendor/devtools/shared/stateDirectory.js +44 -0
  32. package/dist/vendor/devtools/shared/workingDirectory.js +13 -0
  33. package/dist/vendor/devtools/shared/workspaceSettings.js +96 -0
  34. package/docs/agents-tools-panel.md +522 -0
  35. package/docs/codex-verification.md +37 -0
  36. package/docs/conversational-mcp.md +16 -3
  37. package/package.json +9 -5
  38. package/skills/mainsequence-mcp/SKILL.md +19 -4
  39. package/skills/ms/assets/mainsequence-mark.svg +453 -0
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DiscoveryError = void 0;
4
+ exports.createDiscovery = createDiscovery;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const row = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
7
+ const text = (value, max = 4000) => typeof value === 'string' ? value.slice(0, max) : '';
8
+ const uuid = (value) => typeof value === 'string' && /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i.test(value);
9
+ class DiscoveryError extends Error {
10
+ code;
11
+ constructor(code, message) {
12
+ super(message);
13
+ this.code = code;
14
+ }
15
+ }
16
+ exports.DiscoveryError = DiscoveryError;
17
+ // Every refusal names the argument and what would be accepted: the model reads these, and
18
+ // one generic sentence for five different mistakes was indistinguishable from a broken host.
19
+ const invalid = (message) => new DiscoveryError('INVALID_REQUEST', message);
20
+ const shown = (value) => JSON.stringify(typeof value === 'string' ? value.slice(0, 80) : value === undefined ? 'missing' : value).slice(0, 100);
21
+ function success(result) {
22
+ if (result.status !== 'success')
23
+ throw new DiscoveryError(result.error?.code || 'API_ERROR', result.summary);
24
+ if (result.data === undefined)
25
+ throw new DiscoveryError('API_ERROR', 'MainSequence returned no data.');
26
+ return result.data;
27
+ }
28
+ function account(value) {
29
+ return { backend: text(value.backend), authenticated: value.authenticated === true, username: text(value.username, 300),
30
+ source: text(value.source, 40), checked: value.checked === true, expiresAt: text(value.expiresAt, 100) };
31
+ }
32
+ const environmentLabel = (context, uid) => `${context.environments.get(uid) || 'environment'} (${uid})`;
33
+ /** A release the context has seen, in this environment. Says where it was seen instead when it belongs elsewhere. */
34
+ function knownRelease(context, environmentUid, value, kind) {
35
+ if (!uuid(value))
36
+ throw invalid(`releaseUid must be a release UUID from mainsequence_view connections or mainsequence_discover tools, not ${shown(value)}.`);
37
+ const uid = value.toLowerCase();
38
+ if (context.releases.get(environmentUid)?.has(uid))
39
+ return uid;
40
+ const elsewhere = [...context.releases].find(([, set]) => set.has(uid))?.[0];
41
+ if (elsewhere)
42
+ throw invalid(`Release ${uid} belongs to ${environmentLabel(context, elsewhere)}; pass environmentUid ${shown(elsewhere)} instead of ${shown(environmentUid)}.`);
43
+ throw invalid(`Release ${uid} is not a ${kind} MCP of ${environmentLabel(context, environmentUid)} in this chat. Read mainsequence_view with view "connections" (saved) or mainsequence_discover with catalog "tools" first, then use its releaseUid.`);
44
+ }
45
+ /** Host presentation only. Every domain operation goes through the public devtools dispatcher. */
46
+ function createDiscovery(dependencies) {
47
+ const contexts = new Map();
48
+ // A view preference only; authorization still uses the current context and devtools.
49
+ const selectedEnvironments = new Map();
50
+ const accountKey = (backend, username) => JSON.stringify([backend, username]);
51
+ function prune() {
52
+ for (const [id, context] of contexts)
53
+ if (context.expires < Date.now())
54
+ contexts.delete(id);
55
+ }
56
+ async function status(dispatch, conversationId) {
57
+ prune();
58
+ const local = account(success(await dispatch({ action: 'auth-status' })));
59
+ const base = { account: local, environments: [], contextId: null, issue: null };
60
+ if (!local.authenticated)
61
+ return base;
62
+ try {
63
+ const verified = account(success(await dispatch({ action: 'auth-status', backend: local.backend, check: true })));
64
+ base.account = verified;
65
+ if (!verified.authenticated || !verified.username)
66
+ throw new DiscoveryError('NOT_AUTHENTICATED', 'Sign in to MainSequence to browse agents and tools.');
67
+ const available = success(await dispatch({ action: 'mcp-environments', backend: verified.backend, expectedAccount: verified.username }))
68
+ .map(item => ({ uid: item.uid, name: text(item.name, 300), isProduction: item.isProduction }));
69
+ const final = account(success(await dispatch({ action: 'auth-status' })));
70
+ if (final.backend !== verified.backend || !final.authenticated)
71
+ throw new DiscoveryError('CONTEXT_CHANGED', `The backend changed from ${verified.backend} to ${final.backend || 'none'} while opening. Refresh MainSequence.`);
72
+ while (contexts.size >= 32)
73
+ contexts.delete(contexts.keys().next().value);
74
+ const id = (0, node_crypto_1.randomUUID)();
75
+ contexts.set(id, { backend: verified.backend, account: verified.username, conversationId, environments: new Map(available.map(e => [e.uid.toLowerCase(), e.name])), releases: new Map(), agents: new Map(), projects: new Map(), expires: Date.now() + 30 * 60_000 });
76
+ const selected = selectedEnvironments.get(accountKey(verified.backend, verified.username));
77
+ return { ...base, contextId: id, environments: available,
78
+ selectedEnvironmentUid: available.some(environment => environment.uid === selected) ? selected : null };
79
+ }
80
+ catch (error) {
81
+ if (!(error instanceof DiscoveryError))
82
+ throw error;
83
+ return { ...base, issue: { code: error.code, message: error.message } };
84
+ }
85
+ }
86
+ async function scoped(dispatch, args, run) {
87
+ prune();
88
+ const context = contexts.get(String(args.contextId));
89
+ if (!context)
90
+ throw new DiscoveryError('CONTEXT_CHANGED', `contextId ${shown(args.contextId)} is unknown to this server (contexts live 30 minutes in the chat's server process). Call mainsequence_open again and use its contextId.`);
91
+ if (!uuid(args.environmentUid) || !context.environments.has(args.environmentUid.toLowerCase())) {
92
+ throw invalid(`environmentUid ${shown(args.environmentUid)} is not one of this account's environments: ${[...context.environments].map(([uid, name]) => `${name} = ${uid}`).join('; ')}.`);
93
+ }
94
+ const environmentUid = args.environmentUid.toLowerCase();
95
+ async function verify() {
96
+ // Host settings may change while the panel is open. Devtools binds domain
97
+ // requests to the verified account; this local check invalidates the old view.
98
+ const current = account(success(await dispatch({ action: 'auth-status' })));
99
+ if (!current.authenticated || current.backend !== context.backend) {
100
+ contexts.delete(String(args.contextId));
101
+ throw new DiscoveryError('CONTEXT_CHANGED', current.authenticated ? `The backend changed from ${context.backend} to ${current.backend}. Refresh MainSequence.` : `No MainSequence session is available for ${context.backend} any more. Sign in again, then refresh MainSequence.`);
102
+ }
103
+ }
104
+ await verify();
105
+ let answer;
106
+ try {
107
+ answer = await run(context, environmentUid);
108
+ }
109
+ catch (error) {
110
+ if (error instanceof DiscoveryError && ['AUTH_REJECTED', 'NOT_AUTHENTICATED'].includes(error.code)) {
111
+ contexts.delete(String(args.contextId));
112
+ throw new DiscoveryError('CONTEXT_CHANGED', `The account or session changed (${error.code}: ${error.message}). Refresh MainSequence and retry once.`);
113
+ }
114
+ throw error;
115
+ }
116
+ await verify();
117
+ const key = accountKey(context.backend, context.account);
118
+ while (selectedEnvironments.size >= 32 && !selectedEnvironments.has(key))
119
+ selectedEnvironments.delete(selectedEnvironments.keys().next().value);
120
+ selectedEnvironments.set(key, environmentUid);
121
+ return answer;
122
+ }
123
+ function page(args) {
124
+ if (args.search !== undefined && (typeof args.search !== 'string' || args.search.length > 300))
125
+ throw invalid(`search must be a string of at most 300 characters, not ${shown(args.search)}.`);
126
+ if (args.offset !== undefined && (!Number.isSafeInteger(args.offset) || Number(args.offset) < 0))
127
+ throw invalid(`offset must be a non-negative integer, not ${shown(args.offset)}.`);
128
+ }
129
+ async function discover(dispatch, args) {
130
+ if (!['agents', 'tools'].includes(String(args.catalog)))
131
+ throw invalid(`catalog must be "agents" or "tools", not ${shown(args.catalog)}.`);
132
+ page(args);
133
+ return scoped(dispatch, args, async (context, environmentUid) => {
134
+ const search = text(args.search, 300).trim(), offset = Number(args.offset || 0), limit = 20;
135
+ // Agent semantic search is a ranked result set, not an offset-paginated directory.
136
+ if (args.catalog === 'agents' && search && offset)
137
+ throw invalid('Agent search returns one ranked result set; omit offset when search is set.');
138
+ const selection = { backend: context.backend, environmentUid, expectedAccount: context.account };
139
+ if (args.catalog === 'agents') {
140
+ const output = success(await dispatch({ action: 'agents-list', ...selection, search, limit, offset, includeIcons: true }));
141
+ const results = output.results.map(item => {
142
+ return { uid: item.uid, name: text(item.name, 300), description: text(item.description), runtimeState: text(item.runtimeState, 100),
143
+ repositoryBranch: text(item.repositoryBranch, 300), responseKinds: item.responseKinds.slice(0, 10), icon: item.icon ?? null,
144
+ defaults: { provider: text(item.defaults.provider, 100), model: text(item.defaults.model, 100), thinking: text(item.defaults.thinking, 100) } };
145
+ });
146
+ const known = context.agents.get(environmentUid) || new Set();
147
+ results.forEach(item => known.add(item.uid));
148
+ context.agents.set(environmentUid, known);
149
+ return { catalog: 'agents', environmentUid, count: output.count, offset: output.offset, limit, semantic: output.semantic, results };
150
+ }
151
+ const output = success(await dispatch({ action: 'mcp-discover', ...selection, search, limit, offset, enrichNames: true }));
152
+ const saved = new Set(success(await dispatch({ action: 'mcp-list', ...selection, readOnly: true })).map(item => item.resource_release_uid));
153
+ const results = output.results.map(item => {
154
+ return { uid: item.resource_release_uid, name: text(item.name, 300) || text(item.display_name, 300) || item.resource_release_uid,
155
+ repository: text(item.display_name, 300), environmentName: text(item.mcp_connection.organization_environment_name, 300), saved: saved.has(item.resource_release_uid) };
156
+ });
157
+ const known = context.releases.get(environmentUid) || new Set();
158
+ results.forEach(item => known.add(item.uid));
159
+ context.releases.set(environmentUid, known);
160
+ return { catalog: 'tools', environmentUid, count: output.count, offset, limit, semantic: false, results };
161
+ });
162
+ }
163
+ async function tools(dispatch, args) {
164
+ return scoped(dispatch, args, async (context, environmentUid) => {
165
+ const releaseUid = knownRelease(context, environmentUid, args.releaseUid, 'saved or discovered');
166
+ // mcp-tools checks existing saved metadata and current access. It never adds a connection or invokes a project tool.
167
+ const output = success(await dispatch({ action: 'mcp-tools', backend: context.backend, environmentUid, releaseUid, expectedAccount: context.account }));
168
+ return { releaseUid, tools: output.tools.map(item => {
169
+ return { name: text(item.name, 300), description: text(item.description), inputSchema: item.inputSchema,
170
+ annotations: Object.fromEntries(['readOnlyHint', 'destructiveHint', 'idempotentHint', 'openWorldHint'].filter(k => typeof row(item.annotations)[k] === 'boolean').map(k => [k, row(item.annotations)[k]])) };
171
+ }) };
172
+ });
173
+ }
174
+ async function messageSelection(dispatch, args, conversationId) {
175
+ return scoped(dispatch, args, async (context, environmentUid) => {
176
+ if (context.conversationId !== conversationId)
177
+ throw invalid('This contextId was opened from another chat; agent messages are bound to the chat that opened MainSequence. Call mainsequence_open in this chat and use its contextId.');
178
+ if (!uuid(args.agentUid) || !context.agents.get(environmentUid)?.has(args.agentUid.toLowerCase())) {
179
+ throw invalid(`agentUid ${shown(args.agentUid)} is not an agent of ${environmentLabel(context, environmentUid)} seen in this chat. Read mainsequence_discover with catalog "agents" first and use a uid from its results.`);
180
+ }
181
+ return { backend: context.backend, account: context.account, environmentUid, agentUid: args.agentUid.toLowerCase() };
182
+ });
183
+ }
184
+ async function view(dispatch, args, signal) {
185
+ return scoped(dispatch, args, async (context, environmentUid) => {
186
+ const selection = { backend: context.backend, expectedAccount: context.account, environmentUid };
187
+ if (args.view === 'agent' || args.view === 'assets') {
188
+ const ids = args.view === 'assets' ? args.agentUids : [args.agentUid];
189
+ if (!Array.isArray(ids) || ids.length > 20 || ids.some(id => !uuid(id) || !context.agents.get(environmentUid)?.has(id.toLowerCase()))) {
190
+ throw invalid(`${args.view === 'assets' ? 'agentUids (at most 20)' : 'agentUid'} must name agents of ${environmentLabel(context, environmentUid)} seen in this chat; read mainsequence_discover with catalog "agents" first.`);
191
+ }
192
+ // Old open panels must not trigger another directory or individual icon reads.
193
+ if (args.view === 'assets')
194
+ return { assets: {} };
195
+ const { agent } = success(await dispatch({ action: 'agent-get', ...selection, agentUid: String(args.agentUid), includeIcons: true }));
196
+ return { agent };
197
+ }
198
+ if (args.view === 'connections') {
199
+ const connections = success(await dispatch({ action: 'mcp-list', ...selection, readOnly: true }));
200
+ const known = context.releases.get(environmentUid) || new Set();
201
+ const results = connections.map(item => { known.add(item.resource_release_uid); return { uid: item.resource_release_uid, name: text(item.name, 300) || text(item.display_name, 300) || item.resource_release_uid, saved: true }; });
202
+ context.releases.set(environmentUid, known);
203
+ return { results, count: results.length };
204
+ }
205
+ if (args.view === 'projects') {
206
+ page(args);
207
+ const result = success(await dispatch({ action: 'organization-projects', ...selection, search: text(args.search, 300), offset: Number(args.offset || 0), limit: 20 }));
208
+ context.projects.set(environmentUid, new Set(result.results.map(p => p.uid)));
209
+ return result;
210
+ }
211
+ if (args.view === 'project') {
212
+ if (!uuid(args.projectUid) || !context.projects.get(environmentUid)?.has(args.projectUid))
213
+ throw invalid(`projectUid ${shown(args.projectUid)} is not a project of ${environmentLabel(context, environmentUid)} seen in this chat; read mainsequence_view with view "projects" first.`);
214
+ return success(await dispatch({ action: 'organization-project', ...selection, projectUid: args.projectUid }));
215
+ }
216
+ if (args.view === 'repositories' && dependencies)
217
+ return { results: await dependencies.repositories(context.backend, context.account, signal) };
218
+ if (args.view === 'preferences' && dependencies)
219
+ return { preferences: await dependencies.preferences() };
220
+ throw invalid(`view must be one of agent, assets, connections, projects, project, repositories or preferences, not ${shown(args.view)}.`);
221
+ });
222
+ }
223
+ async function connection(dispatch, args) {
224
+ return scoped(dispatch, args, async (context, environmentUid) => {
225
+ if (!['connect', 'disconnect'].includes(String(args.action)))
226
+ throw invalid(`action must be "connect" or "disconnect", not ${shown(args.action)}.`);
227
+ const releaseUid = knownRelease(context, environmentUid, args.releaseUid, 'discovered');
228
+ const result = await dispatch({ action: args.action === 'connect' ? 'mcp-add' : 'mcp-remove', backend: context.backend, expectedAccount: context.account, environmentUid, releaseUid });
229
+ if (result.status !== 'success')
230
+ throw new DiscoveryError(result.error?.code || 'API_ERROR', result.summary);
231
+ return { summary: result.summary, connected: args.action === 'connect' };
232
+ });
233
+ }
234
+ async function invoke(dispatch, args) {
235
+ return scoped(dispatch, args, async (context, environmentUid) => {
236
+ const releaseUid = knownRelease(context, environmentUid, args.releaseUid, 'saved or discovered');
237
+ if (typeof args.tool !== 'string' || !args.tool.trim() || args.tool.length > 300)
238
+ throw invalid(`tool must be a tool name from mainsequence_tools (a non-empty string of at most 300 characters), not ${shown(args.tool)}.`);
239
+ // An omitted argument object means no arguments; anything else must be the tool's input object.
240
+ const input = args.arguments === undefined ? {} : args.arguments;
241
+ if (!input || typeof input !== 'object' || Array.isArray(input))
242
+ throw invalid(`arguments must be a JSON object matching the tool's inputSchema (omit it for no arguments), not ${shown(args.arguments)}.`);
243
+ const output = await dispatch({ action: 'mcp-call', backend: context.backend, expectedAccount: context.account, environmentUid, releaseUid, tool: args.tool, arguments: input });
244
+ // Preserve remote MCP failures and uncertain effects through the native tool
245
+ // boundary. Authentication changes still invalidate the scoped panel.
246
+ if (output.error && ['AUTH_REJECTED', 'NOT_AUTHENTICATED'].includes(output.error.code))
247
+ throw new DiscoveryError(output.error.code, output.summary);
248
+ return { result: output.data, ...(output.status === 'success' ? {} : { error: output.error || { code: 'API_ERROR', message: output.summary }, effects: output.effects }) };
249
+ });
250
+ }
251
+ return { status, discover, tools, messageSelection, view, connection, invoke, scoped };
252
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.conversationIdentity = conversationIdentity;
4
+ exports.createMessaging = createMessaging;
5
+ const devtools_1 = require("../vendor/devtools/index.js");
6
+ const discovery_1 = require("./discovery.js");
7
+ /** This identity is supplied by Codex, never by a model/app tool argument. */
8
+ function conversationIdentity(meta) {
9
+ if (!meta || typeof meta !== 'object' || Array.isArray(meta))
10
+ return;
11
+ const { threadId, thread_id } = meta;
12
+ if (threadId !== undefined && thread_id !== undefined && threadId !== thread_id)
13
+ return;
14
+ const value = threadId ?? thread_id;
15
+ return typeof value === 'string' && value.trim() && value.length <= 512 ? value : undefined;
16
+ }
17
+ function createMessaging(discovery, createDispatch, options = {}) {
18
+ const delivery = new devtools_1.AgentDelivery({ ...options, createDispatch });
19
+ async function scope(dispatch, conversationId) {
20
+ if (!conversationId)
21
+ throw new discovery_1.DiscoveryError('CONVERSATION_REQUIRED', 'Open MainSequence inside a Codex chat to send or recover messages.');
22
+ const local = await dispatch({ action: 'auth-status' });
23
+ if (local.status !== 'success' || !local.data?.authenticated || !local.data.backend)
24
+ throw new discovery_1.DiscoveryError('NOT_AUTHENTICATED', 'Refresh your shared MainSequence session.');
25
+ const result = await dispatch({ action: 'auth-status', backend: local.data.backend, check: true });
26
+ if (result.status !== 'success' || !result.data?.authenticated || !result.data.username)
27
+ throw new discovery_1.DiscoveryError('NOT_AUTHENTICATED', 'The shared MainSequence account could not be verified.');
28
+ return { host: 'codex', conversationId, backend: result.data.backend, account: result.data.username };
29
+ }
30
+ async function call(name, args, dispatch, conversationId, signal) {
31
+ const target = await scope(dispatch, conversationId);
32
+ if (name === 'mainsequence_agent_prepare') {
33
+ const selected = await discovery.messageSelection(dispatch, args, target.conversationId);
34
+ return { conversation: await delivery.prepare(target, { ...selected, conversationKey: args.conversationKey }, signal) };
35
+ }
36
+ if (name === 'mainsequence_stage_message') {
37
+ const selected = await discovery.messageSelection(dispatch, args, target.conversationId);
38
+ if (selected.backend !== target.backend || selected.account !== target.account)
39
+ throw new discovery_1.DiscoveryError('CONTEXT_CHANGED', 'The selected account changed. Refresh MainSequence.');
40
+ return { request: await delivery.stage(target, { ...selected, requestId: args.requestId, message: args.message, conversationKey: args.conversationKey, taskRequestId: args.taskRequestId, responseKind: args.responseKind }, signal) };
41
+ }
42
+ if (name === 'mainsequence_agent_task')
43
+ return { request: await delivery.task(target, args.requestId, args.action, signal) };
44
+ if (name === 'mainsequence_submission')
45
+ return { request: await delivery.submission(target, args.requestId, args.outcome, signal) };
46
+ if ((name === 'mainsequence_agent_status' && !['list', 'get', 'wait'].includes(String(args.action)))
47
+ || (name === 'mainsequence_agent_request' && !['deliver', 'cancel'].includes(String(args.action))))
48
+ throw new devtools_1.ToolingError('INVALID_REQUEST', 'Use the matching delivery or read-only status tool.');
49
+ const id = args.requestId;
50
+ switch (args.action) {
51
+ case 'list':
52
+ if (id !== undefined)
53
+ throw new devtools_1.ToolingError('INVALID_REQUEST', 'List takes no request ID.');
54
+ return { requests: await delivery.list(target, signal) };
55
+ case 'get': return { request: await delivery.get(target, id, signal) };
56
+ case 'deliver': return { request: await delivery.deliver(target, id, signal) };
57
+ case 'wait': return { request: await delivery.wait(target, id, signal) };
58
+ case 'cancel': return { request: await delivery.cancel(target, id, signal) };
59
+ default: throw new devtools_1.ToolingError('INVALID_REQUEST', 'Choose list, get, deliver, wait or cancel.');
60
+ }
61
+ }
62
+ return { call };
63
+ }