@crmy/cli 0.5.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.
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@crmy/cli",
3
+ "version": "0.5.1",
4
+ "description": "CRMy CLI — Local CLI + stdio MCP server",
5
+ "type": "module",
6
+ "bin": {
7
+ "crmy": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "scripts": {
11
+ "build": "tsup src/index.ts --format esm --clean",
12
+ "dev": "tsx src/index.ts"
13
+ },
14
+ "dependencies": {
15
+ "@crmy/shared": "*",
16
+ "@crmy/server": "*",
17
+ "@modelcontextprotocol/sdk": "^1.27.0",
18
+ "commander": "^12.1.0",
19
+ "inquirer": "^12.0.0",
20
+ "pg": "^8.13.0"
21
+ },
22
+ "devDependencies": {
23
+ "tsup": "^8.3.0"
24
+ },
25
+ "license": "Apache-2.0"
26
+ }
package/src/client.ts ADDED
@@ -0,0 +1,287 @@
1
+ // Copyright 2026 CRMy Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { loadConfigFile, loadAuthState } from './config.js';
5
+
6
+ export interface CliClient {
7
+ call(toolName: string, input: Record<string, unknown>): Promise<string>;
8
+ close(): Promise<void>;
9
+ }
10
+
11
+ // Map MCP tool names to REST method + path
12
+ const TOOL_REST_MAP: Record<string, { method: string; path: (input: Record<string, unknown>) => string; bodyTransform?: (input: Record<string, unknown>) => Record<string, unknown> }> = {
13
+ // Contacts
14
+ contact_create: { method: 'POST', path: () => '/api/v1/contacts' },
15
+ contact_get: { method: 'GET', path: (i) => `/api/v1/contacts/${i.id}` },
16
+ contact_search: { method: 'GET', path: (i) => `/api/v1/contacts?q=${encodeURIComponent((i.query as string) ?? '')}&limit=${i.limit ?? 20}${i.lifecycle_stage ? `&stage=${i.lifecycle_stage}` : ''}${i.cursor ? `&cursor=${i.cursor}` : ''}` },
17
+ contact_update: { method: 'PATCH', path: (i) => `/api/v1/contacts/${i.id}` },
18
+ contact_set_lifecycle: { method: 'PATCH', path: (i) => `/api/v1/contacts/${i.id}` },
19
+ contact_log_activity: { method: 'POST', path: () => '/api/v1/activities' },
20
+ contact_get_timeline: { method: 'GET', path: (i) => `/api/v1/contacts/${i.id}/timeline` },
21
+
22
+ // Accounts
23
+ account_create: { method: 'POST', path: () => '/api/v1/accounts' },
24
+ account_get: { method: 'GET', path: (i) => `/api/v1/accounts/${i.id}` },
25
+ account_search: { method: 'GET', path: (i) => `/api/v1/accounts?q=${encodeURIComponent((i.query as string) ?? '')}&limit=${i.limit ?? 20}` },
26
+ account_update: { method: 'PATCH', path: (i) => `/api/v1/accounts/${i.id}` },
27
+
28
+ // Opportunities
29
+ opportunity_create: { method: 'POST', path: () => '/api/v1/opportunities' },
30
+ opportunity_get: { method: 'GET', path: (i) => `/api/v1/opportunities/${i.id}` },
31
+ opportunity_search: { method: 'GET', path: (i) => `/api/v1/opportunities?q=${encodeURIComponent((i.query as string) ?? '')}&limit=${i.limit ?? 20}${i.stage ? `&stage=${i.stage}` : ''}` },
32
+ opportunity_advance_stage: { method: 'PATCH', path: (i) => `/api/v1/opportunities/${i.id}` },
33
+ opportunity_update: { method: 'PATCH', path: (i) => `/api/v1/opportunities/${i.id}` },
34
+
35
+ // Activities
36
+ activity_create: { method: 'POST', path: () => '/api/v1/activities' },
37
+ activity_get: { method: 'GET', path: (i) => `/api/v1/activities?limit=${i.limit ?? 20}` },
38
+ activity_search: { method: 'GET', path: (i) => `/api/v1/activities?limit=${i.limit ?? 20}` },
39
+
40
+ // Use Cases
41
+ use_case_create: { method: 'POST', path: () => '/api/v1/use-cases' },
42
+ use_case_get: { method: 'GET', path: (i) => `/api/v1/use-cases/${i.id}` },
43
+ use_case_search: { method: 'GET', path: (i) => `/api/v1/use-cases?q=${encodeURIComponent((i.query as string) ?? '')}&limit=${i.limit ?? 20}${i.stage ? `&stage=${i.stage}` : ''}${i.account_id ? `&account_id=${i.account_id}` : ''}` },
44
+ use_case_update: { method: 'PATCH', path: (i) => `/api/v1/use-cases/${i.id}` },
45
+ use_case_delete: { method: 'DELETE', path: (i) => `/api/v1/use-cases/${i.id}` },
46
+ use_case_advance_stage: { method: 'POST', path: (i) => `/api/v1/use-cases/${i.id}/stage` },
47
+ use_case_update_consumption: { method: 'POST', path: (i) => `/api/v1/use-cases/${i.id}/consumption` },
48
+ use_case_set_health: { method: 'POST', path: (i) => `/api/v1/use-cases/${i.id}/health` },
49
+ use_case_link_contact: { method: 'POST', path: (i) => `/api/v1/use-cases/${i.use_case_id ?? i.id}/contacts` },
50
+ use_case_unlink_contact: { method: 'DELETE', path: (i) => `/api/v1/use-cases/${i.use_case_id ?? i.id}/contacts/${i.contact_id}` },
51
+ use_case_list_contacts: { method: 'GET', path: (i) => `/api/v1/use-cases/${i.use_case_id ?? i.id}/contacts` },
52
+ use_case_get_timeline: { method: 'GET', path: (i) => `/api/v1/use-cases/${i.id}/timeline` },
53
+ use_case_summary: { method: 'GET', path: (i) => `/api/v1/analytics/use-cases?group_by=${i.group_by ?? 'stage'}` },
54
+
55
+ // Analytics
56
+ pipeline_summary: { method: 'GET', path: (i) => `/api/v1/analytics/pipeline${i.owner_id ? `?owner_id=${i.owner_id}` : ''}` },
57
+ pipeline_forecast: { method: 'GET', path: (i) => `/api/v1/analytics/forecast${i.period ? `?period=${i.period}` : ''}` },
58
+
59
+ // HITL
60
+ hitl_list_pending: { method: 'GET', path: () => '/api/v1/hitl' },
61
+ hitl_check_status: { method: 'GET', path: (i) => `/api/v1/hitl/${i.id}` },
62
+ hitl_submit_request: { method: 'POST', path: () => '/api/v1/hitl' },
63
+ hitl_resolve: { method: 'POST', path: (i) => `/api/v1/hitl/${i.id}/resolve` },
64
+
65
+ // Webhooks
66
+ webhook_create: { method: 'POST', path: () => '/api/v1/webhooks' },
67
+ webhook_list: { method: 'GET', path: () => '/api/v1/webhooks' },
68
+ webhook_get: { method: 'GET', path: (i) => `/api/v1/webhooks/${i.id}` },
69
+ webhook_update: { method: 'PATCH', path: (i) => `/api/v1/webhooks/${i.id}` },
70
+ webhook_delete: { method: 'DELETE', path: (i) => `/api/v1/webhooks/${i.id}` },
71
+ webhook_list_deliveries: { method: 'GET', path: (i) => `/api/v1/webhooks/${i.endpoint_id ?? i.id}/deliveries` },
72
+
73
+ // Emails
74
+ email_create: { method: 'POST', path: () => '/api/v1/emails' },
75
+ email_get: { method: 'GET', path: (i) => `/api/v1/emails/${i.id}` },
76
+ email_search: { method: 'GET', path: (i) => `/api/v1/emails?limit=${i.limit ?? 20}` },
77
+
78
+ // Custom Fields
79
+ custom_field_create: { method: 'POST', path: () => '/api/v1/custom-fields' },
80
+ custom_field_list: { method: 'GET', path: (i) => `/api/v1/custom-fields?object_type=${i.object_type}` },
81
+ custom_field_delete: { method: 'DELETE', path: (i) => `/api/v1/custom-fields/${i.id}` },
82
+
83
+ // Notes
84
+ note_create: { method: 'POST', path: () => '/api/v1/notes' },
85
+ note_get: { method: 'GET', path: (i) => `/api/v1/notes/${i.id}` },
86
+ note_list: { method: 'GET', path: (i) => `/api/v1/notes?object_type=${i.object_type}&object_id=${i.object_id}` },
87
+ note_delete: { method: 'DELETE', path: (i) => `/api/v1/notes/${i.id}` },
88
+
89
+ // Workflows
90
+ workflow_create: { method: 'POST', path: () => '/api/v1/workflows' },
91
+ workflow_get: { method: 'GET', path: (i) => `/api/v1/workflows/${i.id}` },
92
+ workflow_list: { method: 'GET', path: () => '/api/v1/workflows' },
93
+ workflow_delete: { method: 'DELETE', path: (i) => `/api/v1/workflows/${i.id}` },
94
+ workflow_run_list: { method: 'GET', path: (i) => `/api/v1/workflows/${i.workflow_id ?? i.id}/runs` },
95
+
96
+ // Events
97
+ event_search: { method: 'GET', path: (i) => `/api/v1/events?${i.object_id ? `object_id=${i.object_id}&` : ''}limit=${i.limit ?? 20}` },
98
+
99
+ // Search
100
+ search: { method: 'GET', path: (i) => `/api/v1/search?q=${encodeURIComponent((i.query as string) ?? '')}` },
101
+
102
+ // Meta
103
+ schema_get: { method: 'GET', path: () => '/health' },
104
+ tenant_get_stats: { method: 'GET', path: () => '/health' },
105
+
106
+ // Actors
107
+ actor_register: { method: 'POST', path: () => '/api/v1/actors' },
108
+ actor_get: { method: 'GET', path: (i) => `/api/v1/actors/${i.id}` },
109
+ actor_list: { method: 'GET', path: (i) => `/api/v1/actors?limit=${i.limit ?? 20}${i.actor_type ? `&actor_type=${i.actor_type}` : ''}${i.query ? `&q=${encodeURIComponent(i.query as string)}` : ''}` },
110
+ actor_update: { method: 'PATCH', path: (i) => `/api/v1/actors/${i.id}` },
111
+ actor_whoami: { method: 'GET', path: () => '/api/v1/actors/whoami' },
112
+
113
+ // Assignments
114
+ assignment_create: { method: 'POST', path: () => '/api/v1/assignments' },
115
+ assignment_get: { method: 'GET', path: (i) => `/api/v1/assignments/${i.id}` },
116
+ assignment_list: { method: 'GET', path: (i) => `/api/v1/assignments?limit=${i.limit ?? 20}${i.assigned_to ? `&assigned_to=${i.assigned_to}` : ''}${i.assigned_by ? `&assigned_by=${i.assigned_by}` : ''}${i.status ? `&status=${i.status}` : ''}` },
117
+ assignment_update: { method: 'PATCH', path: (i) => `/api/v1/assignments/${i.id}` },
118
+ assignment_accept: { method: 'POST', path: (i) => `/api/v1/assignments/${i.id}/accept` },
119
+ assignment_complete: { method: 'POST', path: (i) => `/api/v1/assignments/${i.id}/complete` },
120
+ assignment_decline: { method: 'POST', path: (i) => `/api/v1/assignments/${i.id}/decline` },
121
+
122
+ // Context Entries
123
+ context_add: { method: 'POST', path: () => '/api/v1/context' },
124
+ context_get: { method: 'GET', path: (i) => `/api/v1/context/${i.id}` },
125
+ context_list: { method: 'GET', path: (i) => `/api/v1/context?limit=${i.limit ?? 20}${i.subject_type ? `&subject_type=${i.subject_type}` : ''}${i.subject_id ? `&subject_id=${i.subject_id}` : ''}${i.context_type ? `&context_type=${i.context_type}` : ''}` },
126
+ context_supersede: { method: 'POST', path: (i) => `/api/v1/context/${i.id}/supersede` },
127
+ context_search: { method: 'GET', path: (i) => `/api/v1/context/search?q=${encodeURIComponent(i.query as string)}&limit=${i.limit ?? 20}${i.subject_type ? `&subject_type=${i.subject_type}` : ''}${i.context_type ? `&context_type=${i.context_type}` : ''}${i.tag ? `&tag=${i.tag}` : ''}${i.current_only === false ? '&current_only=false' : ''}` },
128
+ context_review: { method: 'POST', path: (i) => `/api/v1/context/${i.id}/review` },
129
+ context_stale: { method: 'GET', path: (i) => `/api/v1/context/stale?limit=${i.limit ?? 20}${i.subject_type ? `&subject_type=${i.subject_type}` : ''}${i.subject_id ? `&subject_id=${i.subject_id}` : ''}` },
130
+
131
+ // Activity Type Registry
132
+ activity_type_list: { method: 'GET', path: (i) => `/api/v1/activity-types${i.category ? `?category=${i.category}` : ''}` },
133
+ activity_type_add: { method: 'POST', path: () => '/api/v1/activity-types' },
134
+ activity_type_remove: { method: 'DELETE', path: (i) => `/api/v1/activity-types/${i.type_name}` },
135
+
136
+ // Context Type Registry
137
+ context_type_list: { method: 'GET', path: () => '/api/v1/context-types' },
138
+ context_type_add: { method: 'POST', path: () => '/api/v1/context-types' },
139
+ context_type_remove: { method: 'DELETE', path: (i) => `/api/v1/context-types/${i.type_name}` },
140
+
141
+ // Briefing
142
+ briefing_get: { method: 'GET', path: (i) => `/api/v1/briefing/${i.subject_type}/${i.subject_id}?format=${i.format ?? 'json'}${i.since ? `&since=${i.since}` : ''}${i.include_stale ? '&include_stale=true' : ''}${i.context_types ? `&context_types=${(i.context_types as string[]).join(',')}` : ''}` },
143
+
144
+ // Assignment: Start, Block, Cancel
145
+ assignment_start: { method: 'POST', path: (i) => `/api/v1/assignments/${i.id}/start` },
146
+ assignment_block: { method: 'POST', path: (i) => `/api/v1/assignments/${i.id}/block` },
147
+ assignment_cancel: { method: 'POST', path: (i) => `/api/v1/assignments/${i.id}/cancel` },
148
+
149
+ // Activity Timeline (enhanced)
150
+ activity_get_timeline: { method: 'GET', path: (i) => `/api/v1/activities?subject_type=${i.subject_type}&subject_id=${i.subject_id}&limit=${i.limit ?? 50}` },
151
+ };
152
+
153
+ /**
154
+ * Create an HTTP-based client that calls the CRMy REST API.
155
+ */
156
+ function createHttpClient(serverUrl: string, token: string): CliClient {
157
+ return {
158
+ async call(toolName: string, input: Record<string, unknown>): Promise<string> {
159
+ const mapping = TOOL_REST_MAP[toolName];
160
+ if (!mapping) {
161
+ throw new Error(`Unknown tool: ${toolName} (no REST mapping)`);
162
+ }
163
+
164
+ const { method, path } = mapping;
165
+ const url = `${serverUrl.replace(/\/$/, '')}${path(input)}`;
166
+
167
+ const headers: Record<string, string> = {
168
+ 'Authorization': `Bearer ${token}`,
169
+ 'Content-Type': 'application/json',
170
+ };
171
+
172
+ const fetchOpts: RequestInit = { method, headers };
173
+ if (method === 'POST' || method === 'PATCH' || method === 'PUT') {
174
+ // Strip path params from body
175
+ const body = { ...input };
176
+ delete body.id;
177
+ fetchOpts.body = JSON.stringify(body);
178
+ }
179
+
180
+ const res = await fetch(url, fetchOpts);
181
+
182
+ if (res.status === 401) {
183
+ throw new Error('Authentication expired. Run `crmy login` to re-authenticate.');
184
+ }
185
+
186
+ const responseBody = await res.text();
187
+ if (!res.ok) {
188
+ let detail = responseBody;
189
+ try {
190
+ detail = JSON.parse(responseBody).detail ?? responseBody;
191
+ } catch {}
192
+ throw new Error(`API error (${res.status}): ${detail}`);
193
+ }
194
+
195
+ return responseBody;
196
+ },
197
+ async close() {
198
+ // No cleanup needed for HTTP client
199
+ },
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Create a direct database client using MCP tools.
205
+ */
206
+ async function createDbClient(databaseUrl: string, apiKey?: string): Promise<CliClient> {
207
+ process.env.CRMY_IMPORTED = '1';
208
+
209
+ const { initPool, closePool, getAllTools } = await import('@crmy/server');
210
+ const db = await initPool(databaseUrl);
211
+
212
+ let actor = {
213
+ tenant_id: '',
214
+ actor_id: 'cli-user',
215
+ actor_type: 'user' as const,
216
+ role: 'owner' as const,
217
+ };
218
+
219
+ if (apiKey) {
220
+ const crypto = await import('node:crypto');
221
+ const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex');
222
+ const result = await db.query(
223
+ `SELECT ak.tenant_id, ak.user_id, u.role
224
+ FROM api_keys ak LEFT JOIN users u ON ak.user_id = u.id
225
+ WHERE ak.key_hash = $1`,
226
+ [keyHash],
227
+ );
228
+ if (result.rows.length > 0) {
229
+ actor.tenant_id = result.rows[0].tenant_id;
230
+ actor.actor_id = result.rows[0].user_id ?? 'cli-user';
231
+ actor.role = result.rows[0].role ?? 'owner';
232
+ }
233
+ }
234
+
235
+ if (!actor.tenant_id) {
236
+ const tenantResult = await db.query("SELECT id FROM tenants WHERE slug = 'default' LIMIT 1");
237
+ if (tenantResult.rows.length > 0) {
238
+ actor.tenant_id = tenantResult.rows[0].id;
239
+ }
240
+ }
241
+
242
+ const tools = getAllTools(db);
243
+
244
+ return {
245
+ async call(toolName: string, input: Record<string, unknown>): Promise<string> {
246
+ const tool = tools.find(t => t.name === toolName);
247
+ if (!tool) throw new Error(`Unknown tool: ${toolName}`);
248
+ const result = await tool.handler(input, actor);
249
+ return JSON.stringify(result, null, 2);
250
+ },
251
+ async close() {
252
+ await closePool();
253
+ },
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Get a CLI client. Priority:
259
+ * 1. Direct DB (if DATABASE_URL or .crmy.json database.url is set)
260
+ * 2. HTTP client (if authenticated via `crmy login`)
261
+ */
262
+ export async function getClient(): Promise<CliClient> {
263
+ const config = loadConfigFile();
264
+ const databaseUrl = process.env.DATABASE_URL ?? config.database?.url;
265
+
266
+ // Prefer direct DB connection when available
267
+ if (databaseUrl) {
268
+ const apiKey = process.env.CRMY_API_KEY ?? config.apiKey;
269
+ return createDbClient(databaseUrl, apiKey);
270
+ }
271
+
272
+ // Fall back to HTTP client if authenticated
273
+ const auth = loadAuthState();
274
+ if (auth) {
275
+ return createHttpClient(auth.serverUrl, auth.token);
276
+ }
277
+
278
+ // Also check for server URL + API key (headless mode)
279
+ const serverUrl = process.env.CRMY_SERVER_URL ?? config.serverUrl;
280
+ const apiKey = process.env.CRMY_API_KEY ?? config.apiKey;
281
+ if (serverUrl && apiKey) {
282
+ return createHttpClient(serverUrl, apiKey);
283
+ }
284
+
285
+ console.error('Not connected. Run `crmy auth setup` and `crmy login`, or `crmy init` for local mode.');
286
+ process.exit(1);
287
+ }
@@ -0,0 +1,78 @@
1
+ // Copyright 2026 CRMy Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { Command } from 'commander';
5
+ import { getClient } from '../client.js';
6
+
7
+ export function accountsCommand(): Command {
8
+ const cmd = new Command('accounts').description('Manage accounts');
9
+
10
+ cmd.command('list')
11
+ .option('-q, --query <query>', 'Search query')
12
+ .action(async (opts) => {
13
+ const client = await getClient();
14
+ const result = await client.call('account_search', { query: opts.query, limit: 20 });
15
+ const data = JSON.parse(result);
16
+ if (data.accounts?.length === 0) {
17
+ console.log('No accounts found.');
18
+ return;
19
+ }
20
+ console.table(data.accounts?.map((a: Record<string, unknown>) => ({
21
+ id: (a.id as string).slice(0, 8),
22
+ name: a.name,
23
+ industry: a.industry ?? '',
24
+ health: a.health_score ?? '',
25
+ })));
26
+ await client.close();
27
+ });
28
+
29
+ cmd.command('get <id>')
30
+ .description('Get account details including contacts and open opportunities')
31
+ .action(async (id) => {
32
+ const client = await getClient();
33
+ const result = await client.call('account_get', { id });
34
+ console.log(JSON.parse(result));
35
+ await client.close();
36
+ });
37
+
38
+ cmd.command('create')
39
+ .description('Create a new account')
40
+ .action(async () => {
41
+ const { default: inquirer } = await import('inquirer');
42
+ const answers = await inquirer.prompt([
43
+ { type: 'input', name: 'name', message: 'Account name:' },
44
+ { type: 'input', name: 'domain', message: 'Domain (optional):' },
45
+ { type: 'input', name: 'industry', message: 'Industry (optional):' },
46
+ { type: 'input', name: 'website', message: 'Website (optional):' },
47
+ ]);
48
+
49
+ const client = await getClient();
50
+ const result = await client.call('account_create', {
51
+ name: answers.name,
52
+ domain: answers.domain || undefined,
53
+ industry: answers.industry || undefined,
54
+ website: answers.website || undefined,
55
+ });
56
+ const data = JSON.parse(result);
57
+ console.log(`\n Created account: ${data.account.id}\n`);
58
+ await client.close();
59
+ });
60
+
61
+ cmd.command('delete <id>')
62
+ .description('Permanently delete an account (admin/owner only)')
63
+ .action(async (id) => {
64
+ const { default: inquirer } = await import('inquirer');
65
+ const { confirm } = await inquirer.prompt([
66
+ { type: 'confirm', name: 'confirm', message: `Delete account ${id}? This cannot be undone.`, default: false },
67
+ ]);
68
+ if (!confirm) { console.log(' Cancelled.'); return; }
69
+
70
+ const client = await getClient();
71
+ const result = await client.call('account_delete', { id });
72
+ const data = JSON.parse(result);
73
+ if (data.deleted) console.log(` Deleted.`);
74
+ await client.close();
75
+ });
76
+
77
+ return cmd;
78
+ }
@@ -0,0 +1,59 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ import { Command } from 'commander';
4
+ import { getClient } from '../client.js';
5
+
6
+ export function activityTypesCommand(): Command {
7
+ const cmd = new Command('activity-types').description('Manage activity type registry');
8
+
9
+ cmd.command('list')
10
+ .option('--category <cat>', 'Filter by category (outreach, meeting, proposal, contract, internal, lifecycle, handoff)')
11
+ .action(async (opts) => {
12
+ const client = await getClient();
13
+ const result = await client.call('activity_type_list', { category: opts.category });
14
+ const data = JSON.parse(result);
15
+ if (data.activity_types?.length === 0) {
16
+ console.log('No activity types found.');
17
+ return;
18
+ }
19
+ console.table(data.activity_types?.map((t: Record<string, unknown>) => ({
20
+ type_name: t.type_name,
21
+ label: t.label,
22
+ category: t.category,
23
+ default: t.is_default ? '✓' : '',
24
+ })));
25
+ await client.close();
26
+ });
27
+
28
+ cmd.command('add <type_name>')
29
+ .requiredOption('--label <label>', 'Display label')
30
+ .requiredOption('--category <category>', 'Category')
31
+ .option('--description <desc>', 'Description')
32
+ .action(async (typeName, opts) => {
33
+ const client = await getClient();
34
+ const result = await client.call('activity_type_add', {
35
+ type_name: typeName,
36
+ label: opts.label,
37
+ category: opts.category,
38
+ description: opts.description,
39
+ });
40
+ const data = JSON.parse(result);
41
+ console.log(`\n Added activity type: ${data.activity_type.type_name}\n`);
42
+ await client.close();
43
+ });
44
+
45
+ cmd.command('remove <type_name>')
46
+ .description('Remove a custom activity type (cannot remove defaults)')
47
+ .action(async (typeName) => {
48
+ const client = await getClient();
49
+ try {
50
+ await client.call('activity_type_remove', { type_name: typeName });
51
+ console.log(`\n Removed activity type: ${typeName}\n`);
52
+ } catch (err) {
53
+ console.error(`\n Error: ${err instanceof Error ? err.message : err}\n`);
54
+ }
55
+ await client.close();
56
+ });
57
+
58
+ return cmd;
59
+ }
@@ -0,0 +1,80 @@
1
+ // Copyright 2026 CRMy Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { Command } from 'commander';
5
+ import { getClient } from '../client.js';
6
+
7
+ export function actorsCommand(): Command {
8
+ const cmd = new Command('actors').description('Manage actors (humans & agents)');
9
+
10
+ cmd.command('list')
11
+ .option('--type <type>', 'Filter by actor_type (human or agent)')
12
+ .option('-q, --query <query>', 'Search query')
13
+ .action(async (opts) => {
14
+ const client = await getClient();
15
+ const result = await client.call('actor_list', {
16
+ actor_type: opts.type,
17
+ query: opts.query,
18
+ limit: 20,
19
+ });
20
+ const data = JSON.parse(result);
21
+ if (data.actors?.length === 0) {
22
+ console.log('No actors found.');
23
+ return;
24
+ }
25
+ console.table(data.actors?.map((a: Record<string, unknown>) => ({
26
+ id: (a.id as string).slice(0, 8),
27
+ type: a.actor_type,
28
+ name: a.display_name,
29
+ email: a.email ?? '',
30
+ agent_id: a.agent_identifier ?? '',
31
+ active: a.is_active ? '✓' : '✗',
32
+ })));
33
+ if (data.total > 20) console.log(`\n Showing 20 of ${data.total} actors`);
34
+ await client.close();
35
+ });
36
+
37
+ cmd.command('register')
38
+ .description('Register a new actor')
39
+ .action(async () => {
40
+ const { default: inquirer } = await import('inquirer');
41
+ const answers = await inquirer.prompt([
42
+ { type: 'list', name: 'actor_type', message: 'Actor type:', choices: ['human', 'agent'] },
43
+ { type: 'input', name: 'display_name', message: 'Display name:' },
44
+ { type: 'input', name: 'email', message: 'Email (for humans):' },
45
+ { type: 'input', name: 'agent_identifier', message: 'Agent identifier (for agents):' },
46
+ { type: 'input', name: 'agent_model', message: 'Agent model (e.g. claude-sonnet-4-20250514):' },
47
+ ]);
48
+
49
+ const client = await getClient();
50
+ const result = await client.call('actor_register', {
51
+ actor_type: answers.actor_type,
52
+ display_name: answers.display_name,
53
+ email: answers.email || undefined,
54
+ agent_identifier: answers.agent_identifier || undefined,
55
+ agent_model: answers.agent_model || undefined,
56
+ });
57
+ const data = JSON.parse(result);
58
+ console.log(`\n Registered actor: ${data.actor.id}\n`);
59
+ await client.close();
60
+ });
61
+
62
+ cmd.command('get <id>')
63
+ .action(async (id) => {
64
+ const client = await getClient();
65
+ const result = await client.call('actor_get', { id });
66
+ console.log(JSON.parse(result));
67
+ await client.close();
68
+ });
69
+
70
+ cmd.command('whoami')
71
+ .description('Show current actor identity')
72
+ .action(async () => {
73
+ const client = await getClient();
74
+ const result = await client.call('actor_whoami', {});
75
+ console.log(JSON.parse(result));
76
+ await client.close();
77
+ });
78
+
79
+ return cmd;
80
+ }