btcp-browser-agent 0.1.0

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 (117) hide show
  1. package/CLAUDE.md +230 -0
  2. package/LICENSE +21 -0
  3. package/README.md +309 -0
  4. package/SKILL.md +143 -0
  5. package/SNAPSHOT_IMPROVEMENTS.md +302 -0
  6. package/USAGE.md +146 -0
  7. package/dist/index.d.ts +34 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +35 -0
  10. package/dist/index.js.map +1 -0
  11. package/docs/browser-cli-design.md +500 -0
  12. package/examples/chrome-extension/CHANGELOG.md +210 -0
  13. package/examples/chrome-extension/DEBUG.md +231 -0
  14. package/examples/chrome-extension/ERROR_FIXED.md +147 -0
  15. package/examples/chrome-extension/QUICK_TEST.md +189 -0
  16. package/examples/chrome-extension/README.md +149 -0
  17. package/examples/chrome-extension/SESSION_ONLY_MODE.md +305 -0
  18. package/examples/chrome-extension/TEST_WITH_YOUR_TABS.md +97 -0
  19. package/examples/chrome-extension/build.js +43 -0
  20. package/examples/chrome-extension/manifest.json +37 -0
  21. package/examples/chrome-extension/package-lock.json +1063 -0
  22. package/examples/chrome-extension/package.json +21 -0
  23. package/examples/chrome-extension/popup.html +195 -0
  24. package/examples/chrome-extension/src/background.ts +12 -0
  25. package/examples/chrome-extension/src/content.ts +7 -0
  26. package/examples/chrome-extension/src/popup.ts +303 -0
  27. package/examples/chrome-extension/src/scenario-google-github.ts +389 -0
  28. package/examples/chrome-extension/test-page.html +127 -0
  29. package/examples/chrome-extension/tests/README.md +206 -0
  30. package/examples/chrome-extension/tests/scenario-google-to-github-star.ts +380 -0
  31. package/examples/chrome-extension/tsconfig.json +14 -0
  32. package/examples/snapshots/README.md +207 -0
  33. package/examples/snapshots/amazon-com-detail.html +9528 -0
  34. package/examples/snapshots/amazon-com-detail.snapshot.txt +997 -0
  35. package/examples/snapshots/convert-snapshots.ts +97 -0
  36. package/examples/snapshots/edition-cnn-com.html +13292 -0
  37. package/examples/snapshots/edition-cnn-com.snapshot.txt +562 -0
  38. package/examples/snapshots/github-com-microsoft-vscode.html +2916 -0
  39. package/examples/snapshots/github-com-microsoft-vscode.snapshot.txt +455 -0
  40. package/examples/snapshots/google-search.html +20012 -0
  41. package/examples/snapshots/google-search.snapshot.txt +195 -0
  42. package/examples/snapshots/metadata.json +86 -0
  43. package/examples/snapshots/npr-org-templates.html +2031 -0
  44. package/examples/snapshots/npr-org-templates.snapshot.txt +224 -0
  45. package/examples/snapshots/stackoverflow-com.html +5216 -0
  46. package/examples/snapshots/stackoverflow-com.snapshot.txt +2404 -0
  47. package/examples/snapshots/test-all-mode.html +46 -0
  48. package/examples/snapshots/test-all-mode.snapshot.txt +5 -0
  49. package/examples/snapshots/validate.test.ts +296 -0
  50. package/package.json +65 -0
  51. package/packages/cli/package.json +42 -0
  52. package/packages/cli/src/__tests__/cli.test.ts +434 -0
  53. package/packages/cli/src/__tests__/errors.test.ts +226 -0
  54. package/packages/cli/src/__tests__/executor.test.ts +275 -0
  55. package/packages/cli/src/__tests__/formatter.test.ts +260 -0
  56. package/packages/cli/src/__tests__/parser.test.ts +288 -0
  57. package/packages/cli/src/__tests__/suggestions.test.ts +255 -0
  58. package/packages/cli/src/commands/back.ts +22 -0
  59. package/packages/cli/src/commands/check.ts +33 -0
  60. package/packages/cli/src/commands/clear.ts +33 -0
  61. package/packages/cli/src/commands/click.ts +32 -0
  62. package/packages/cli/src/commands/closetab.ts +31 -0
  63. package/packages/cli/src/commands/eval.ts +41 -0
  64. package/packages/cli/src/commands/fill.ts +30 -0
  65. package/packages/cli/src/commands/focus.ts +33 -0
  66. package/packages/cli/src/commands/forward.ts +22 -0
  67. package/packages/cli/src/commands/goto.ts +34 -0
  68. package/packages/cli/src/commands/help.ts +162 -0
  69. package/packages/cli/src/commands/hover.ts +34 -0
  70. package/packages/cli/src/commands/index.ts +129 -0
  71. package/packages/cli/src/commands/newtab.ts +35 -0
  72. package/packages/cli/src/commands/press.ts +40 -0
  73. package/packages/cli/src/commands/reload.ts +25 -0
  74. package/packages/cli/src/commands/screenshot.ts +27 -0
  75. package/packages/cli/src/commands/scroll.ts +64 -0
  76. package/packages/cli/src/commands/select.ts +35 -0
  77. package/packages/cli/src/commands/snapshot.ts +21 -0
  78. package/packages/cli/src/commands/tab.ts +32 -0
  79. package/packages/cli/src/commands/tabs.ts +26 -0
  80. package/packages/cli/src/commands/text.ts +27 -0
  81. package/packages/cli/src/commands/title.ts +17 -0
  82. package/packages/cli/src/commands/type.ts +38 -0
  83. package/packages/cli/src/commands/uncheck.ts +33 -0
  84. package/packages/cli/src/commands/url.ts +17 -0
  85. package/packages/cli/src/commands/wait.ts +54 -0
  86. package/packages/cli/src/errors.ts +164 -0
  87. package/packages/cli/src/executor.ts +68 -0
  88. package/packages/cli/src/formatter.ts +215 -0
  89. package/packages/cli/src/index.ts +257 -0
  90. package/packages/cli/src/parser.ts +195 -0
  91. package/packages/cli/src/suggestions.ts +207 -0
  92. package/packages/cli/src/terminal/Terminal.ts +365 -0
  93. package/packages/cli/src/terminal/index.ts +5 -0
  94. package/packages/cli/src/types.ts +155 -0
  95. package/packages/cli/tsconfig.json +20 -0
  96. package/packages/core/package.json +35 -0
  97. package/packages/core/src/actions.ts +1210 -0
  98. package/packages/core/src/errors.ts +296 -0
  99. package/packages/core/src/index.test.ts +638 -0
  100. package/packages/core/src/index.ts +220 -0
  101. package/packages/core/src/ref-map.ts +107 -0
  102. package/packages/core/src/snapshot.ts +873 -0
  103. package/packages/core/src/types.ts +536 -0
  104. package/packages/core/tsconfig.json +23 -0
  105. package/packages/extension/README.md +129 -0
  106. package/packages/extension/package.json +43 -0
  107. package/packages/extension/src/background.ts +888 -0
  108. package/packages/extension/src/content.ts +172 -0
  109. package/packages/extension/src/index.ts +579 -0
  110. package/packages/extension/src/session-manager.ts +385 -0
  111. package/packages/extension/src/session-types.ts +144 -0
  112. package/packages/extension/src/types.ts +162 -0
  113. package/packages/extension/tsconfig.json +28 -0
  114. package/src/index.ts +64 -0
  115. package/tsconfig.build.json +12 -0
  116. package/tsconfig.json +26 -0
  117. package/vitest.config.ts +13 -0
@@ -0,0 +1,35 @@
1
+ /**
2
+ * select command - Select dropdown option
3
+ */
4
+
5
+ import type { CommandHandler } from '../types.js';
6
+ import { InvalidArgumentsError } from '../errors.js';
7
+
8
+ export const selectCommand: CommandHandler = {
9
+ name: 'select',
10
+ description: 'Select a dropdown option',
11
+ usage: 'select <selector> <value>',
12
+ examples: ['select @ref:5 "Option 1"', 'select #country US'],
13
+
14
+ async execute(client, args) {
15
+ if (args.length < 2) {
16
+ throw new InvalidArgumentsError('Selector and value required', 'select <selector> <value>');
17
+ }
18
+
19
+ const selector = args[0];
20
+ const value = args.slice(1).join(' ');
21
+
22
+ const response = await client.execute({
23
+ id: `cmd_${Date.now()}`,
24
+ action: 'select',
25
+ selector,
26
+ values: value,
27
+ } as any);
28
+
29
+ if (response.success) {
30
+ return { success: true, message: `Selected "${value}" in: ${selector}` };
31
+ }
32
+
33
+ return { success: false, error: response.error };
34
+ },
35
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * snapshot command - Get page accessibility tree
3
+ */
4
+
5
+ import type { CommandHandler } from '../types.js';
6
+ import { getFlagString, getFlagNumber } from '../parser.js';
7
+
8
+ export const snapshotCommand: CommandHandler = {
9
+ name: 'snapshot',
10
+ description: 'Get page accessibility tree',
11
+ usage: 'snapshot [--selector <css>] [--depth <n>]',
12
+ examples: ['snapshot', 'snapshot --selector main', 'snapshot --depth 5'],
13
+
14
+ async execute(client, _args, flags) {
15
+ const selector = getFlagString(flags, 'selector');
16
+ const maxDepth = getFlagNumber(flags, 'depth');
17
+
18
+ const result = await client.snapshot({ selector, maxDepth });
19
+ return { success: true, data: result };
20
+ },
21
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * tab command - Switch to a tab
3
+ */
4
+
5
+ import type { CommandHandler } from '../types.js';
6
+ import { InvalidArgumentsError } from '../errors.js';
7
+
8
+ export const tabCommand: CommandHandler = {
9
+ name: 'tab',
10
+ description: 'Switch to a tab by ID',
11
+ usage: 'tab <id>',
12
+ examples: ['tab 123'],
13
+
14
+ async execute(client, args) {
15
+ if (args.length === 0) {
16
+ throw new InvalidArgumentsError('Tab ID required', 'tab <id>');
17
+ }
18
+
19
+ const tabId = parseInt(args[0], 10);
20
+ if (isNaN(tabId)) {
21
+ throw new InvalidArgumentsError('Invalid tab ID', 'tab <id>');
22
+ }
23
+
24
+ const response = await client.tabSwitch(tabId);
25
+
26
+ if (response.success) {
27
+ return { success: true, message: `Switched to tab ${tabId}` };
28
+ }
29
+
30
+ return { success: false, error: response.error };
31
+ },
32
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * tabs command - List all tabs
3
+ */
4
+
5
+ import type { CommandHandler } from '../types.js';
6
+
7
+ export const tabsCommand: CommandHandler = {
8
+ name: 'tabs',
9
+ description: 'List all open tabs',
10
+ usage: 'tabs',
11
+ examples: ['tabs'],
12
+
13
+ async execute(client) {
14
+ const tabs = await client.tabList();
15
+
16
+ const formatted = tabs
17
+ .map((tab) => {
18
+ const marker = tab.active ? '*' : ' ';
19
+ const title = tab.title || '(untitled)';
20
+ return `${marker} [${tab.id}] ${title}\n ${tab.url || ''}`;
21
+ })
22
+ .join('\n');
23
+
24
+ return { success: true, data: formatted || 'No tabs' };
25
+ },
26
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * text command - Get text content of an element
3
+ */
4
+
5
+ import type { CommandHandler } from '../types.js';
6
+ import { InvalidArgumentsError } from '../errors.js';
7
+
8
+ export const textCommand: CommandHandler = {
9
+ name: 'text',
10
+ description: 'Get text content of an element',
11
+ usage: 'text <selector>',
12
+ examples: ['text @ref:5', 'text #heading', 'text .message'],
13
+
14
+ async execute(client, args) {
15
+ if (args.length === 0) {
16
+ throw new InvalidArgumentsError('Selector required', 'text <selector>');
17
+ }
18
+
19
+ const selector = args[0];
20
+ const text = await client.getText(selector);
21
+
22
+ return {
23
+ success: true,
24
+ data: text !== null ? text : '(element not found)',
25
+ };
26
+ },
27
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * title command - Get current page title
3
+ */
4
+
5
+ import type { CommandHandler } from '../types.js';
6
+
7
+ export const titleCommand: CommandHandler = {
8
+ name: 'title',
9
+ description: 'Get the current page title',
10
+ usage: 'title',
11
+ examples: ['title'],
12
+
13
+ async execute(client) {
14
+ const title = await client.getTitle();
15
+ return { success: true, data: title };
16
+ },
17
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * type command - Type text into an element
3
+ */
4
+
5
+ import type { CommandHandler } from '../types.js';
6
+ import { InvalidArgumentsError } from '../errors.js';
7
+ import { getFlagNumber, getFlagBool } from '../parser.js';
8
+
9
+ export const typeCommand: CommandHandler = {
10
+ name: 'type',
11
+ description: 'Type text into an element (character by character)',
12
+ usage: 'type <selector> <text> [--delay <ms>] [--clear]',
13
+ examples: [
14
+ 'type @ref:1 "hello world"',
15
+ 'type #search hello',
16
+ 'type @ref:2 "test" --delay 50',
17
+ 'type @ref:3 "new text" --clear',
18
+ ],
19
+
20
+ async execute(client, args, flags) {
21
+ if (args.length < 2) {
22
+ throw new InvalidArgumentsError('Selector and text required', 'type <selector> <text>');
23
+ }
24
+
25
+ const selector = args[0];
26
+ const text = args.slice(1).join(' ');
27
+ const delay = getFlagNumber(flags, 'delay');
28
+ const clear = getFlagBool(flags, 'clear');
29
+
30
+ const response = await client.type(selector, text, { delay, clear });
31
+
32
+ if (response.success) {
33
+ return { success: true, message: `Typed "${text}" into: ${selector}` };
34
+ }
35
+
36
+ return { success: false, error: response.error };
37
+ },
38
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * uncheck command - Uncheck a checkbox
3
+ */
4
+
5
+ import type { CommandHandler } from '../types.js';
6
+ import { InvalidArgumentsError } from '../errors.js';
7
+
8
+ export const uncheckCommand: CommandHandler = {
9
+ name: 'uncheck',
10
+ description: 'Uncheck a checkbox',
11
+ usage: 'uncheck <selector>',
12
+ examples: ['uncheck @ref:5', 'uncheck #newsletter'],
13
+
14
+ async execute(client, args) {
15
+ if (args.length === 0) {
16
+ throw new InvalidArgumentsError('Selector required', 'uncheck <selector>');
17
+ }
18
+
19
+ const selector = args[0];
20
+
21
+ const response = await client.execute({
22
+ id: `cmd_${Date.now()}`,
23
+ action: 'uncheck',
24
+ selector,
25
+ } as any);
26
+
27
+ if (response.success) {
28
+ return { success: true, message: `Unchecked: ${selector}` };
29
+ }
30
+
31
+ return { success: false, error: response.error };
32
+ },
33
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * url command - Get current page URL
3
+ */
4
+
5
+ import type { CommandHandler } from '../types.js';
6
+
7
+ export const urlCommand: CommandHandler = {
8
+ name: 'url',
9
+ description: 'Get the current page URL',
10
+ usage: 'url',
11
+ examples: ['url'],
12
+
13
+ async execute(client) {
14
+ const url = await client.getUrl();
15
+ return { success: true, data: url };
16
+ },
17
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * wait command - Wait for duration or element
3
+ */
4
+
5
+ import type { CommandHandler } from '../types.js';
6
+ import { getFlagString, getFlagNumber } from '../parser.js';
7
+
8
+ export const waitCommand: CommandHandler = {
9
+ name: 'wait',
10
+ description: 'Wait for duration (ms) or element state',
11
+ usage: 'wait <ms> | wait <selector> [--state visible|hidden]',
12
+ examples: [
13
+ 'wait 1000',
14
+ 'wait @ref:5',
15
+ 'wait @ref:5 --state visible',
16
+ 'wait #loading --state hidden',
17
+ ],
18
+
19
+ async execute(client, args, flags) {
20
+ if (args.length === 0) {
21
+ // Default wait
22
+ await new Promise((resolve) => setTimeout(resolve, 1000));
23
+ return { success: true, message: 'Waited 1000ms' };
24
+ }
25
+
26
+ const first = args[0];
27
+
28
+ // Check if it's a number (duration)
29
+ const duration = parseInt(first, 10);
30
+ if (!isNaN(duration) && String(duration) === first) {
31
+ await new Promise((resolve) => setTimeout(resolve, duration));
32
+ return { success: true, message: `Waited ${duration}ms` };
33
+ }
34
+
35
+ // It's a selector - wait for element
36
+ const selector = first;
37
+ const state = getFlagString(flags, 'state') as 'visible' | 'hidden' | undefined;
38
+ const timeout = getFlagNumber(flags, 'timeout', 30000);
39
+
40
+ const response = await client.execute({
41
+ id: `cmd_${Date.now()}`,
42
+ action: 'wait',
43
+ selector,
44
+ state: state || 'visible',
45
+ timeout,
46
+ } as any);
47
+
48
+ if (response.success) {
49
+ return { success: true, message: `Element ready: ${selector}` };
50
+ }
51
+
52
+ return { success: false, error: response.error };
53
+ },
54
+ };
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @btcp/cli - Error classes with contextual feedback
3
+ */
4
+
5
+ /**
6
+ * Base CLI error with optional suggestions
7
+ */
8
+ export class CLIError extends Error {
9
+ /** Suggestions for fixing the error */
10
+ public suggestions?: string[];
11
+ /** Usage hint */
12
+ public usage?: string;
13
+ /** Related command */
14
+ public command?: string;
15
+
16
+ constructor(
17
+ message: string,
18
+ options?: {
19
+ suggestions?: string[];
20
+ usage?: string;
21
+ command?: string;
22
+ }
23
+ ) {
24
+ super(message);
25
+ this.name = 'CLIError';
26
+ this.suggestions = options?.suggestions;
27
+ this.usage = options?.usage;
28
+ this.command = options?.command;
29
+ }
30
+
31
+ /**
32
+ * Format error with suggestions for display
33
+ */
34
+ toFormattedString(): string {
35
+ let output = this.message;
36
+
37
+ if (this.usage) {
38
+ output += `\n\nUsage: ${this.usage}`;
39
+ }
40
+
41
+ if (this.suggestions && this.suggestions.length > 0) {
42
+ output += '\n\nSuggestions:';
43
+ for (const suggestion of this.suggestions) {
44
+ output += `\n - ${suggestion}`;
45
+ }
46
+ }
47
+
48
+ return output;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Command not found error with similar command suggestions
54
+ */
55
+ export class CommandNotFoundError extends CLIError {
56
+ constructor(
57
+ public readonly commandName: string,
58
+ similarCommands?: string[]
59
+ ) {
60
+ const suggestions = similarCommands?.length
61
+ ? similarCommands.map((cmd) => `Did you mean '${cmd}'?`)
62
+ : ['Type "help" to see available commands'];
63
+
64
+ super(`Unknown command: ${commandName}`, {
65
+ suggestions,
66
+ command: commandName,
67
+ });
68
+ this.name = 'CommandNotFoundError';
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Invalid arguments error with usage hint
74
+ */
75
+ export class InvalidArgumentsError extends CLIError {
76
+ constructor(
77
+ message: string,
78
+ usage?: string,
79
+ examples?: string[]
80
+ ) {
81
+ const suggestions = examples?.map((ex) => `Example: ${ex}`);
82
+ super(message, { usage, suggestions });
83
+ this.name = 'InvalidArgumentsError';
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Parse error with syntax help
89
+ */
90
+ export class ParseError extends CLIError {
91
+ constructor(message: string, hint?: string) {
92
+ const suggestions = hint ? [hint] : [
93
+ 'Commands should be: <command> [args] [--flags]',
94
+ 'Use quotes for text with spaces: type @ref:1 "hello world"',
95
+ ];
96
+ super(message, { suggestions });
97
+ this.name = 'ParseError';
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Execution error with recovery suggestions
103
+ */
104
+ export class ExecutionError extends CLIError {
105
+ constructor(
106
+ message: string,
107
+ options?: {
108
+ suggestions?: string[];
109
+ command?: string;
110
+ }
111
+ ) {
112
+ super(message, options);
113
+ this.name = 'ExecutionError';
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Element not found error
119
+ */
120
+ export class ElementNotFoundError extends CLIError {
121
+ constructor(selector: string) {
122
+ super(`Element not found: ${selector}`, {
123
+ suggestions: [
124
+ 'Run "snapshot" to see available elements with @ref IDs',
125
+ `Wait for element: wait ${selector} --state visible`,
126
+ 'Check if the element is inside an iframe',
127
+ ],
128
+ });
129
+ this.name = 'ElementNotFoundError';
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Navigation error
135
+ */
136
+ export class NavigationError extends CLIError {
137
+ constructor(url: string, reason?: string) {
138
+ super(`Navigation failed: ${reason || url}`, {
139
+ suggestions: [
140
+ 'Ensure URL includes protocol (https://)',
141
+ 'Check if the URL is accessible',
142
+ 'Try: goto https://example.com',
143
+ ],
144
+ });
145
+ this.name = 'NavigationError';
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Timeout error
151
+ */
152
+ export class TimeoutError extends CLIError {
153
+ constructor(operation: string, selector?: string) {
154
+ const suggestions = [
155
+ 'Increase wait time: wait 5000',
156
+ 'Check if element exists: snapshot',
157
+ ];
158
+ if (selector) {
159
+ suggestions.push(`Wait for element: wait ${selector} --state visible`);
160
+ }
161
+ super(`Timeout: ${operation}`, { suggestions });
162
+ this.name = 'TimeoutError';
163
+ }
164
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @btcp/cli - Command executor
3
+ *
4
+ * Executes parsed commands using the client.
5
+ */
6
+
7
+ import type { ParsedCommand, CommandResult, CommandClient } from './types.js';
8
+ import { getCommand } from './commands/index.js';
9
+ import { CommandNotFoundError, ExecutionError, CLIError } from './errors.js';
10
+ import { findSimilarCommands, getContextualSuggestion } from './suggestions.js';
11
+
12
+ /**
13
+ * Execute a parsed command
14
+ */
15
+ export async function executeCommand(
16
+ client: CommandClient,
17
+ command: ParsedCommand
18
+ ): Promise<CommandResult> {
19
+ const handler = getCommand(command.name);
20
+
21
+ if (!handler) {
22
+ // Find similar commands for suggestion
23
+ const similar = findSimilarCommands(command.name);
24
+ throw new CommandNotFoundError(command.name, similar);
25
+ }
26
+
27
+ try {
28
+ const result = await handler.execute(client, command.args, command.flags);
29
+
30
+ // Add contextual suggestions on error
31
+ if (!result.success && result.error) {
32
+ const suggestion = getContextualSuggestion(command.name, result.error, command.args);
33
+ if (suggestion) {
34
+ return {
35
+ ...result,
36
+ error: `${result.error}\n\n${suggestion}`,
37
+ };
38
+ }
39
+ }
40
+
41
+ return result;
42
+ } catch (error) {
43
+ if (error instanceof CLIError) {
44
+ throw error;
45
+ }
46
+
47
+ // Wrap unexpected errors with contextual suggestions
48
+ const message = error instanceof Error ? error.message : String(error);
49
+ const suggestion = getContextualSuggestion(command.name, message, command.args);
50
+
51
+ throw new ExecutionError(message, {
52
+ suggestions: suggestion ? [suggestion] : undefined,
53
+ command: command.name,
54
+ });
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Execute a raw command string
60
+ */
61
+ export async function executeCommandString(
62
+ client: CommandClient,
63
+ input: string,
64
+ parse: (input: string) => ParsedCommand
65
+ ): Promise<CommandResult> {
66
+ const command = parse(input);
67
+ return executeCommand(client, command);
68
+ }