@hsafa/cli 0.0.1

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.
@@ -0,0 +1,168 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import axios from 'axios';
4
+ import config from '../utils/config.js';
5
+ import ora from 'ora';
6
+ import http from 'http';
7
+ import open from 'open';
8
+ import { URL } from 'url';
9
+ export function registerAuthCommands(program) {
10
+ const auth = program.command('auth').description('Authentication management');
11
+ auth
12
+ .command('login')
13
+ .description('Login to HSAFA')
14
+ .option('--manual', 'Login manually with email and password')
15
+ .action(async (options) => {
16
+ if (options.manual) {
17
+ await loginManual();
18
+ }
19
+ else {
20
+ await loginBrowser();
21
+ }
22
+ });
23
+ async function loginBrowser() {
24
+ const serverUrl = config.get('serverUrl');
25
+ const clientUrl = serverUrl.replace('server.hsafa.com', 'hsafa.com');
26
+ console.log(chalk.cyan('Authentication requested.'));
27
+ console.log(`We will open your browser to log you in (via): ${clientUrl}`);
28
+ const { confirm } = await inquirer.prompt([
29
+ {
30
+ type: 'confirm',
31
+ name: 'confirm',
32
+ message: 'Press Enter to open browser...',
33
+ default: true,
34
+ },
35
+ ]);
36
+ if (!confirm) {
37
+ console.log(chalk.yellow('Login cancelled.'));
38
+ return;
39
+ }
40
+ const spinner = ora('Waiting for authentication...').start();
41
+ // Start local server to receive the token
42
+ const server = http.createServer(async (req, res) => {
43
+ const url = new URL(req.url, `http://${req.headers.host}`);
44
+ const token = url.searchParams.get('token');
45
+ if (token) {
46
+ // Save token
47
+ saveToken(token);
48
+ // Respond to browser
49
+ res.writeHead(200, { 'Content-Type': 'text/html' });
50
+ res.end(`
51
+ <html>
52
+ <body style="background:#111; color:#fff; font-family:sans-serif; text-align:center; padding-top:50px;">
53
+ <h1>Login Successful!</h1>
54
+ <p>You can close this tab and return to the CLI.</p>
55
+ <script>window.close();</script>
56
+ </body>
57
+ </html>
58
+ `);
59
+ spinner.succeed(chalk.green('Login successful!'));
60
+ server.close();
61
+ process.exit(0); // Exit successfully
62
+ }
63
+ else {
64
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
65
+ res.end('No token provided.');
66
+ spinner.fail(chalk.red('Login failed: No token received.'));
67
+ server.close();
68
+ process.exit(1);
69
+ }
70
+ });
71
+ // Listen on a random port
72
+ server.listen(0, async () => {
73
+ const address = server.address();
74
+ const port = address.port;
75
+ const callbackUrl = `http://localhost:${port}`;
76
+ // Open the browser
77
+ // Point to the client-side CLI login page (where NextAuth is configured)
78
+ // The client page logic:
79
+ // 1. Check if user is logged in (session).
80
+ // 2. If not, redirect to /api/auth/signin?callbackUrl=/cli-login?callback=callbackUrl
81
+ // 3. If logged in, get session token.
82
+ // 4. Redirect to callbackUrl?token=sessionToken
83
+ const loginUrl = `${serverUrl}/cli-login?callback=${encodeURIComponent(callbackUrl)}`;
84
+ try {
85
+ await open(loginUrl);
86
+ }
87
+ catch (err) {
88
+ spinner.fail(chalk.red('Failed to open browser. Please open this URL manually:'));
89
+ console.log(loginUrl);
90
+ }
91
+ });
92
+ }
93
+ async function loginManual() {
94
+ const answers = await inquirer.prompt([
95
+ {
96
+ type: 'input',
97
+ name: 'email',
98
+ message: 'Enter your email:',
99
+ },
100
+ {
101
+ type: 'password',
102
+ name: 'password',
103
+ message: 'Enter your password:',
104
+ },
105
+ ]);
106
+ const spinner = ora('Logging in...').start();
107
+ try {
108
+ const serverUrl = config.get('serverUrl');
109
+ const response = await axios.post(`${serverUrl}/api/auth/signin`, {
110
+ email: answers.email,
111
+ password: answers.password,
112
+ }, {
113
+ withCredentials: true,
114
+ });
115
+ // Extract cookie
116
+ const setCookie = response.headers['set-cookie'];
117
+ if (setCookie) {
118
+ const sessionTokenMatch = setCookie.find(c => c.includes('next-auth.session-token'));
119
+ if (sessionTokenMatch) {
120
+ const token = sessionTokenMatch.split(';')[0].split('=')[1];
121
+ saveToken(token);
122
+ spinner.succeed(chalk.green('Login successful!'));
123
+ }
124
+ else {
125
+ spinner.fail(chalk.red('Login failed: Could not find session token in response.'));
126
+ }
127
+ }
128
+ else {
129
+ spinner.fail(chalk.red('Login failed: No cookies returned from server.'));
130
+ }
131
+ }
132
+ catch (error) {
133
+ const errorMsg = error.response?.data?.error || error.message;
134
+ spinner.fail(chalk.red(`Login failed: ${errorMsg}`));
135
+ }
136
+ }
137
+ function saveToken(token) {
138
+ config.set('token', token);
139
+ // Save to current profile as well
140
+ const currentProfile = config.get('currentProfile');
141
+ if (currentProfile) {
142
+ const profiles = config.get('profiles') || {};
143
+ if (profiles[currentProfile]) {
144
+ profiles[currentProfile].token = token;
145
+ config.set('profiles', profiles);
146
+ }
147
+ }
148
+ }
149
+ auth
150
+ .command('logout')
151
+ .description('Logout from HSAFA')
152
+ .action(() => {
153
+ config.delete('token');
154
+ console.log(chalk.green('Logged out successfully.'));
155
+ });
156
+ auth
157
+ .command('status')
158
+ .description('Check authentication status')
159
+ .action(() => {
160
+ const token = config.get('token');
161
+ if (token) {
162
+ console.log(chalk.green('You are logged in.'));
163
+ }
164
+ else {
165
+ console.log(chalk.yellow('You are not logged in.'));
166
+ }
167
+ });
168
+ }
@@ -0,0 +1,233 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import api from '../utils/api.js';
4
+ import { graphqlRequest } from '../utils/graphql.js';
5
+ import ora from 'ora';
6
+ import inquirer from 'inquirer';
7
+ export function registerChatCommands(program) {
8
+ const chat = program.command('chat').description('Chat session management');
9
+ chat
10
+ .command('list')
11
+ .description('List your previous chat sessions')
12
+ .option('-a, --agent <id>', 'Filter by agent ID')
13
+ .action(async (options) => {
14
+ const globalOptions = program.opts();
15
+ const spinner = !globalOptions.json ? ora('Fetching chats...').start() : null;
16
+ try {
17
+ const query = `
18
+ query($agentId: ID) {
19
+ agentTestChats(agentId: $agentId) {
20
+ id
21
+ title
22
+ agentId
23
+ createdAt
24
+ updatedAt
25
+ }
26
+ }
27
+ `;
28
+ const data = await graphqlRequest(query, { agentId: options.agent });
29
+ const chats = data.agentTestChats;
30
+ if (spinner)
31
+ spinner.stop();
32
+ if (globalOptions.json) {
33
+ console.log(JSON.stringify(chats, null, 2));
34
+ return;
35
+ }
36
+ if (chats.length === 0) {
37
+ console.log(chalk.yellow('No chat sessions found.'));
38
+ return;
39
+ }
40
+ const table = new Table({
41
+ head: [chalk.cyan('ID'), chalk.cyan('Title'), chalk.cyan('Agent ID'), chalk.cyan('Updated')],
42
+ colWidths: [36, 25, 36, 25]
43
+ });
44
+ chats.forEach((c) => {
45
+ table.push([c.id, c.title || 'Untitled', c.agentId, c.updatedAt]);
46
+ });
47
+ console.log(table.toString());
48
+ }
49
+ catch (error) {
50
+ if (spinner)
51
+ spinner.fail(`Failed to fetch chats: ${error.message}`);
52
+ else
53
+ console.error(JSON.stringify({ error: error.message }));
54
+ }
55
+ });
56
+ chat
57
+ .command('resume <id>')
58
+ .description('Resume a previous chat session')
59
+ .action(async (id) => {
60
+ const spinner = ora('Fetching chat history...').start();
61
+ try {
62
+ const query = `
63
+ query($id: ID!) {
64
+ agentTestChat(id: $id) {
65
+ id
66
+ agentId
67
+ title
68
+ messages {
69
+ role: sender
70
+ content
71
+ }
72
+ }
73
+ }
74
+ `;
75
+ const data = await graphqlRequest(query, { id });
76
+ const chatData = data.agentTestChat;
77
+ if (!chatData) {
78
+ spinner.fail('Chat session not found.');
79
+ return;
80
+ }
81
+ spinner.succeed(`Resuming chat: ${chatData.title || 'Untitled'}`);
82
+ // Convert SENDER enum to role
83
+ const history = chatData.messages.map((m) => ({
84
+ role: m.role.toLowerCase() === 'agent' ? 'assistant' : 'user',
85
+ content: m.content
86
+ }));
87
+ await startChatSession(chatData.agentId, chatData.id, history);
88
+ }
89
+ catch (error) {
90
+ spinner.fail(`Failed to resume chat: ${error.message}`);
91
+ }
92
+ });
93
+ chat
94
+ .command('delete <id>')
95
+ .description('Delete a chat session')
96
+ .action(async (id) => {
97
+ const spinner = ora('Deleting chat session...').start();
98
+ try {
99
+ const mutation = `
100
+ mutation($id: ID!) {
101
+ deleteAgentTestChat(id: $id)
102
+ }
103
+ `;
104
+ await graphqlRequest(mutation, { id });
105
+ spinner.succeed(chalk.green('Chat session deleted.'));
106
+ }
107
+ catch (error) {
108
+ spinner.fail(`Failed to delete chat: ${error.message}`);
109
+ }
110
+ });
111
+ }
112
+ export async function startChatSession(agentId, existingChatId, history = []) {
113
+ let chatId = existingChatId;
114
+ const messages = [...history];
115
+ if (!chatId) {
116
+ const spinner = ora('Creating new chat session...').start();
117
+ try {
118
+ const mutation = `
119
+ mutation($input: CreateAgentTestChatInput!) {
120
+ createAgentTestChat(input: $input) {
121
+ id
122
+ }
123
+ }
124
+ `;
125
+ const data = await graphqlRequest(mutation, {
126
+ input: { agentId, title: `CLI Chat ${new Date().toLocaleString()}` }
127
+ });
128
+ chatId = data.createAgentTestChat.id;
129
+ spinner.succeed(chalk.gray(`New session created: ${chatId}`));
130
+ }
131
+ catch (error) {
132
+ spinner.fail(`Failed to create session: ${error.message}`);
133
+ return;
134
+ }
135
+ }
136
+ console.log(chalk.blue(`Chatting with agent ${agentId}`));
137
+ if (messages.length > 0) {
138
+ console.log(chalk.gray(`Loaded ${messages.length} messages from history.`));
139
+ messages.forEach(m => {
140
+ const label = m.role === 'user' ? chalk.green('You:') : chalk.cyan('Agent:');
141
+ console.log(`${label} ${m.content}`);
142
+ });
143
+ }
144
+ console.log(chalk.gray('Type "exit" or "quit" to stop.\n'));
145
+ while (true) {
146
+ const { prompt } = await inquirer.prompt([
147
+ {
148
+ type: 'input',
149
+ name: 'prompt',
150
+ message: chalk.green('You:'),
151
+ },
152
+ ]);
153
+ if (!prompt || prompt.toLowerCase() === 'exit' || prompt.toLowerCase() === 'quit') {
154
+ break;
155
+ }
156
+ const userMessage = { role: 'user', content: prompt };
157
+ messages.push(userMessage);
158
+ // Persist user message
159
+ try {
160
+ const mutation = `
161
+ mutation($input: AppendAgentTestMessageInput!) {
162
+ appendAgentTestMessage(input: $input) { id }
163
+ }
164
+ `;
165
+ await graphqlRequest(mutation, {
166
+ input: {
167
+ chatId,
168
+ sender: 'USER',
169
+ content: prompt
170
+ }
171
+ });
172
+ }
173
+ catch (e) {
174
+ console.error(chalk.yellow('Warning: Could not persist message to history.'));
175
+ }
176
+ process.stdout.write(chalk.cyan('Agent: '));
177
+ try {
178
+ const response = await api.post(`/api/run/${agentId}`, {
179
+ messages: messages.map(m => ({
180
+ role: m.role,
181
+ content: m.content
182
+ })),
183
+ }, {
184
+ responseType: 'stream',
185
+ });
186
+ const stream = response.data;
187
+ let assistantContent = '';
188
+ await new Promise((resolve, reject) => {
189
+ let buffer = '';
190
+ stream.on('data', (chunk) => {
191
+ buffer += chunk.toString();
192
+ const lines = buffer.split('\n');
193
+ buffer = lines.pop() || '';
194
+ for (const line of lines) {
195
+ if (!line.trim())
196
+ continue;
197
+ try {
198
+ const data = JSON.parse(line);
199
+ if (data.type === 'text-delta' || data.type === 'text') {
200
+ const delta = data.textDelta || data.text;
201
+ process.stdout.write(delta);
202
+ assistantContent += delta;
203
+ }
204
+ }
205
+ catch (e) { }
206
+ }
207
+ });
208
+ stream.on('end', () => {
209
+ process.stdout.write('\n\n');
210
+ const assistantMessage = { role: 'assistant', content: assistantContent };
211
+ messages.push(assistantMessage);
212
+ // Persist assistant message
213
+ graphqlRequest(`
214
+ mutation($input: AppendAgentTestMessageInput!) {
215
+ appendAgentTestMessage(input: $input) { id }
216
+ }
217
+ `, {
218
+ input: {
219
+ chatId,
220
+ sender: 'AGENT',
221
+ content: assistantContent
222
+ }
223
+ }).catch(() => { });
224
+ resolve();
225
+ });
226
+ stream.on('error', reject);
227
+ });
228
+ }
229
+ catch (error) {
230
+ console.error(chalk.red(`\nError: ${error.message}`));
231
+ }
232
+ }
233
+ }
@@ -0,0 +1,37 @@
1
+ import chalk from 'chalk';
2
+ import api from '../utils/api.js';
3
+ import ora from 'ora';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import FormData from 'form-data';
7
+ export function registerDocCommands(program) {
8
+ const doc = program.command('doc').description('Document management');
9
+ doc
10
+ .command('upload <file>')
11
+ .description('Upload a document')
12
+ .action(async (file) => {
13
+ const filePath = path.resolve(file);
14
+ if (!fs.existsSync(filePath)) {
15
+ console.error(chalk.red(`File not found: ${file}`));
16
+ return;
17
+ }
18
+ const spinner = ora('Uploading document...').start();
19
+ try {
20
+ const formData = new FormData();
21
+ formData.append('file', fs.createReadStream(filePath));
22
+ const response = await api.post('/api/uploads', formData, {
23
+ headers: {
24
+ ...formData.getHeaders(),
25
+ },
26
+ });
27
+ spinner.succeed(chalk.green('Document uploaded successfully!'));
28
+ console.log(chalk.cyan('File details:'));
29
+ console.log(`ID: ${response.data.id}`);
30
+ console.log(`URL: ${response.data.url}`);
31
+ console.log(`Storage: ${response.data.storage}`);
32
+ }
33
+ catch (error) {
34
+ spinner.fail(`Upload failed: ${error.message}`);
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,106 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import fs from 'fs';
4
+ import { graphqlRequest } from '../utils/graphql.js';
5
+ async function readStdin() {
6
+ const chunks = [];
7
+ for await (const chunk of process.stdin) {
8
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
9
+ }
10
+ return Buffer.concat(chunks).toString('utf8');
11
+ }
12
+ export function registerGqlCommands(program) {
13
+ const gql = program.command('gql').description('Run arbitrary GraphQL queries/mutations');
14
+ gql
15
+ .command('run')
16
+ .description('Execute a GraphQL operation')
17
+ .option('--query <graphql>', 'GraphQL query/mutation string')
18
+ .option('--query-file <path>', 'Read GraphQL query/mutation from file')
19
+ .option('--query-stdin', 'Read GraphQL query/mutation from stdin')
20
+ .option('--variables <json>', 'Variables JSON object')
21
+ .option('--variables-file <path>', 'Read variables JSON from file')
22
+ .option('--variables-stdin', 'Read variables JSON from stdin')
23
+ .option('--operation-name <name>', 'GraphQL operationName (optional)')
24
+ .option('--no-pretty', 'Disable pretty JSON formatting')
25
+ .action(async (options) => {
26
+ const globalOptions = program.opts();
27
+ const spinner = !globalOptions.json ? ora('Running GraphQL...').start() : null;
28
+ try {
29
+ let query = options.query;
30
+ if (!query && options.queryFile) {
31
+ query = fs.readFileSync(options.queryFile, 'utf8');
32
+ }
33
+ if (!query && options.queryStdin) {
34
+ query = await readStdin();
35
+ }
36
+ if (!query) {
37
+ throw new Error('No query provided. Use --query, --query-file, or --query-stdin');
38
+ }
39
+ let variables = {};
40
+ if (options.variables !== undefined) {
41
+ variables = JSON.parse(options.variables);
42
+ }
43
+ else if (options.variablesFile) {
44
+ variables = JSON.parse(fs.readFileSync(options.variablesFile, 'utf8'));
45
+ }
46
+ else if (options.variablesStdin) {
47
+ const text = await readStdin();
48
+ variables = JSON.parse(text);
49
+ }
50
+ const data = await graphqlRequest(query, variables, {
51
+ operationName: options.operationName,
52
+ });
53
+ if (spinner)
54
+ spinner.stop();
55
+ const pretty = options.pretty !== false;
56
+ const json = JSON.stringify(data, null, pretty ? 2 : 0);
57
+ console.log(json);
58
+ }
59
+ catch (error) {
60
+ if (spinner)
61
+ spinner.fail(`GraphQL failed: ${error.message}`);
62
+ else
63
+ console.error(JSON.stringify({ error: error.message }, null, 2));
64
+ if (!globalOptions.json && !spinner) {
65
+ console.error(chalk.red(error.message));
66
+ }
67
+ process.exitCode = 1;
68
+ }
69
+ });
70
+ gql
71
+ .command('introspect')
72
+ .description('Fetch the GraphQL schema via introspection')
73
+ .option('--no-pretty', 'Disable pretty JSON formatting')
74
+ .action(async (options) => {
75
+ const globalOptions = program.opts();
76
+ const spinner = !globalOptions.json ? ora('Fetching schema...').start() : null;
77
+ try {
78
+ const query = `
79
+ query IntrospectionQuery {
80
+ __schema {
81
+ queryType { name }
82
+ mutationType { name }
83
+ types {
84
+ kind
85
+ name
86
+ description
87
+ }
88
+ }
89
+ }
90
+ `;
91
+ const data = await graphqlRequest(query, {}, { operationName: 'IntrospectionQuery' });
92
+ if (spinner)
93
+ spinner.stop();
94
+ const pretty = options.pretty !== false;
95
+ const json = JSON.stringify(data, null, pretty ? 2 : 0);
96
+ console.log(json);
97
+ }
98
+ catch (error) {
99
+ if (spinner)
100
+ spinner.fail(`Introspection failed: ${error.message}`);
101
+ else
102
+ console.error(JSON.stringify({ error: error.message }, null, 2));
103
+ process.exitCode = 1;
104
+ }
105
+ });
106
+ }
@@ -0,0 +1,147 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { graphqlRequest } from '../utils/graphql.js';
4
+ import ora from 'ora';
5
+ export function registerInviteCommands(program) {
6
+ const invite = program.command('invite').description('Invitation management');
7
+ invite
8
+ .command('send <workspaceId> <email>')
9
+ .description('Invite a user to a workspace')
10
+ .option('-r, --role <role>', 'Role (OWNER, EDITOR, VIEWER)', 'VIEWER')
11
+ .action(async (workspaceId, email, options) => {
12
+ const spinner = ora(`Inviting ${email}...`).start();
13
+ try {
14
+ const mutation = `
15
+ mutation($workspaceId: ID!, $input: InviteToWorkspaceInput!) {
16
+ inviteToWorkspace(workspaceId: $workspaceId, input: $input) {
17
+ id
18
+ email
19
+ token
20
+ }
21
+ }
22
+ `;
23
+ const data = await graphqlRequest(mutation, {
24
+ workspaceId,
25
+ input: { email, role: options.role.toUpperCase() }
26
+ });
27
+ spinner.succeed(chalk.green(`Invitation sent to ${email}. Token: ${data.inviteToWorkspace.token}`));
28
+ }
29
+ catch (error) {
30
+ spinner.fail(`Failed to send invitation: ${error.message}`);
31
+ }
32
+ });
33
+ invite
34
+ .command('list [workspaceId]')
35
+ .description('List pending invitations')
36
+ .action(async (workspaceId) => {
37
+ const globalOptions = program.opts();
38
+ const spinner = !globalOptions.json ? ora('Fetching invitations...').start() : null;
39
+ try {
40
+ let query;
41
+ let variables = {};
42
+ if (workspaceId) {
43
+ query = `
44
+ query($workspaceId: ID!) {
45
+ workspaceInvitations(workspaceId: $workspaceId) {
46
+ id
47
+ email
48
+ role
49
+ expiresAt
50
+ createdAt
51
+ }
52
+ }
53
+ `;
54
+ variables = { workspaceId };
55
+ }
56
+ else {
57
+ query = `
58
+ query {
59
+ myInvitations {
60
+ id
61
+ email
62
+ role
63
+ expiresAt
64
+ createdAt
65
+ workspace {
66
+ name
67
+ }
68
+ }
69
+ }
70
+ `;
71
+ }
72
+ const data = await graphqlRequest(query, variables);
73
+ const invitations = workspaceId ? data.workspaceInvitations : data.myInvitations;
74
+ if (spinner)
75
+ spinner.stop();
76
+ if (globalOptions.json) {
77
+ console.log(JSON.stringify(invitations, null, 2));
78
+ return;
79
+ }
80
+ if (invitations.length === 0) {
81
+ console.log(chalk.yellow('No pending invitations found.'));
82
+ return;
83
+ }
84
+ const table = new Table({
85
+ head: [chalk.cyan('ID'), chalk.cyan('Email'), chalk.cyan('Workspace'), chalk.cyan('Role'), chalk.cyan('Expires')],
86
+ colWidths: [36, 30, 20, 10, 25]
87
+ });
88
+ invitations.forEach((inv) => {
89
+ table.push([
90
+ inv.id,
91
+ inv.email,
92
+ inv.workspace?.name || 'N/A',
93
+ inv.role,
94
+ inv.expiresAt
95
+ ]);
96
+ });
97
+ console.log(table.toString());
98
+ }
99
+ catch (error) {
100
+ if (spinner)
101
+ spinner.fail(`Failed to fetch invitations: ${error.message}`);
102
+ else
103
+ console.error(JSON.stringify({ error: error.message }));
104
+ }
105
+ });
106
+ invite
107
+ .command('accept <token>')
108
+ .description('Accept a workspace invitation')
109
+ .action(async (token) => {
110
+ const spinner = ora('Accepting invitation...').start();
111
+ try {
112
+ const mutation = `
113
+ mutation($input: AcceptInvitationInput!) {
114
+ acceptWorkspaceInvitation(input: $input) {
115
+ id
116
+ workspace {
117
+ name
118
+ }
119
+ }
120
+ }
121
+ `;
122
+ const data = await graphqlRequest(mutation, { input: { token } });
123
+ spinner.succeed(chalk.green(`Successfully joined workspace: ${data.acceptWorkspaceInvitation.workspace.name}`));
124
+ }
125
+ catch (error) {
126
+ spinner.fail(`Failed to accept invitation: ${error.message}`);
127
+ }
128
+ });
129
+ invite
130
+ .command('cancel <id>')
131
+ .description('Cancel a pending invitation')
132
+ .action(async (id) => {
133
+ const spinner = ora('Cancelling invitation...').start();
134
+ try {
135
+ const mutation = `
136
+ mutation($id: ID!) {
137
+ cancelWorkspaceInvitation(id: $id)
138
+ }
139
+ `;
140
+ await graphqlRequest(mutation, { id });
141
+ spinner.succeed(chalk.green('Invitation cancelled.'));
142
+ }
143
+ catch (error) {
144
+ spinner.fail(`Failed to cancel invitation: ${error.message}`);
145
+ }
146
+ });
147
+ }