@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 +26 -0
- package/src/client.ts +287 -0
- package/src/commands/accounts.ts +78 -0
- package/src/commands/activity-types.ts +59 -0
- package/src/commands/actors.ts +80 -0
- package/src/commands/assignments.ts +164 -0
- package/src/commands/auth.ts +161 -0
- package/src/commands/briefing.ts +41 -0
- package/src/commands/config.ts +25 -0
- package/src/commands/contacts.ts +83 -0
- package/src/commands/context-types.ts +56 -0
- package/src/commands/context.ts +181 -0
- package/src/commands/custom-fields.ts +63 -0
- package/src/commands/emails.ts +71 -0
- package/src/commands/events.ts +54 -0
- package/src/commands/help.ts +81 -0
- package/src/commands/hitl.ts +50 -0
- package/src/commands/init.ts +129 -0
- package/src/commands/mcp.ts +70 -0
- package/src/commands/migrate.ts +49 -0
- package/src/commands/notes.ts +88 -0
- package/src/commands/opps.ts +106 -0
- package/src/commands/pipeline.ts +26 -0
- package/src/commands/search.ts +45 -0
- package/src/commands/server.ts +32 -0
- package/src/commands/use-cases.ts +97 -0
- package/src/commands/webhooks.ts +95 -0
- package/src/commands/workflows.ts +119 -0
- package/src/config.ts +74 -0
- package/src/index.ts +80 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,164 @@
|
|
|
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 assignmentsCommand(): Command {
|
|
8
|
+
const cmd = new Command('assignments').description('Manage assignments (coordination & handoffs)');
|
|
9
|
+
|
|
10
|
+
cmd.command('list')
|
|
11
|
+
.option('--mine', 'Show assignments assigned to me')
|
|
12
|
+
.option('--delegated', 'Show assignments I created')
|
|
13
|
+
.option('--status <status>', 'Filter by status')
|
|
14
|
+
.option('--priority <priority>', 'Filter by priority')
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
const client = await getClient();
|
|
17
|
+
const input: Record<string, unknown> = { limit: 20 };
|
|
18
|
+
|
|
19
|
+
if (opts.mine) {
|
|
20
|
+
const whoami = JSON.parse(await client.call('actor_whoami', {}));
|
|
21
|
+
input.assigned_to = whoami.actor_id;
|
|
22
|
+
}
|
|
23
|
+
if (opts.delegated) {
|
|
24
|
+
const whoami = JSON.parse(await client.call('actor_whoami', {}));
|
|
25
|
+
input.assigned_by = whoami.actor_id;
|
|
26
|
+
}
|
|
27
|
+
if (opts.status) input.status = opts.status;
|
|
28
|
+
if (opts.priority) input.priority = opts.priority;
|
|
29
|
+
|
|
30
|
+
const result = await client.call('assignment_list', input);
|
|
31
|
+
const data = JSON.parse(result);
|
|
32
|
+
if (data.assignments?.length === 0) {
|
|
33
|
+
console.log('No assignments found.');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.table(data.assignments?.map((a: Record<string, unknown>) => ({
|
|
37
|
+
id: (a.id as string).slice(0, 8),
|
|
38
|
+
title: (a.title as string).slice(0, 40),
|
|
39
|
+
type: a.assignment_type,
|
|
40
|
+
status: a.status,
|
|
41
|
+
priority: a.priority,
|
|
42
|
+
subject: `${a.subject_type}:${(a.subject_id as string).slice(0, 8)}`,
|
|
43
|
+
})));
|
|
44
|
+
if (data.total > 20) console.log(`\n Showing 20 of ${data.total} assignments`);
|
|
45
|
+
await client.close();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
cmd.command('create')
|
|
49
|
+
.description('Create a new assignment')
|
|
50
|
+
.action(async () => {
|
|
51
|
+
const { default: inquirer } = await import('inquirer');
|
|
52
|
+
const answers = await inquirer.prompt([
|
|
53
|
+
{ type: 'input', name: 'title', message: 'Title:' },
|
|
54
|
+
{ type: 'input', name: 'description', message: 'Description:' },
|
|
55
|
+
{ type: 'list', name: 'assignment_type', message: 'Type:', choices: ['follow_up', 'review', 'approve', 'send', 'call', 'meet', 'research', 'draft', 'custom'] },
|
|
56
|
+
{ type: 'input', name: 'assigned_to', message: 'Assign to (actor UUID):' },
|
|
57
|
+
{ type: 'list', name: 'subject_type', message: 'Subject type:', choices: ['contact', 'account', 'opportunity', 'use_case'] },
|
|
58
|
+
{ type: 'input', name: 'subject_id', message: 'Subject ID (UUID):' },
|
|
59
|
+
{ type: 'list', name: 'priority', message: 'Priority:', choices: ['low', 'normal', 'high', 'urgent'], default: 'normal' },
|
|
60
|
+
{ type: 'input', name: 'context', message: 'Context / handoff notes:' },
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const client = await getClient();
|
|
64
|
+
const result = await client.call('assignment_create', {
|
|
65
|
+
title: answers.title,
|
|
66
|
+
description: answers.description || undefined,
|
|
67
|
+
assignment_type: answers.assignment_type,
|
|
68
|
+
assigned_to: answers.assigned_to,
|
|
69
|
+
subject_type: answers.subject_type,
|
|
70
|
+
subject_id: answers.subject_id,
|
|
71
|
+
priority: answers.priority,
|
|
72
|
+
context: answers.context || undefined,
|
|
73
|
+
});
|
|
74
|
+
const data = JSON.parse(result);
|
|
75
|
+
console.log(`\n Created assignment: ${data.assignment.id}\n`);
|
|
76
|
+
await client.close();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
cmd.command('get <id>')
|
|
80
|
+
.action(async (id) => {
|
|
81
|
+
const client = await getClient();
|
|
82
|
+
const result = await client.call('assignment_get', { id });
|
|
83
|
+
console.log(JSON.parse(result));
|
|
84
|
+
await client.close();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
cmd.command('accept <id>')
|
|
88
|
+
.description('Accept a pending assignment')
|
|
89
|
+
.action(async (id) => {
|
|
90
|
+
const client = await getClient();
|
|
91
|
+
const result = await client.call('assignment_accept', { id });
|
|
92
|
+
const data = JSON.parse(result);
|
|
93
|
+
console.log(`\n Accepted assignment: ${data.assignment.id} (status: ${data.assignment.status})\n`);
|
|
94
|
+
await client.close();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
cmd.command('complete <id>')
|
|
98
|
+
.option('--activity <activityId>', 'Link the completing activity')
|
|
99
|
+
.description('Complete an assignment')
|
|
100
|
+
.action(async (id, opts) => {
|
|
101
|
+
const client = await getClient();
|
|
102
|
+
const result = await client.call('assignment_complete', {
|
|
103
|
+
id,
|
|
104
|
+
completed_by_activity_id: opts.activity || undefined,
|
|
105
|
+
});
|
|
106
|
+
const data = JSON.parse(result);
|
|
107
|
+
console.log(`\n Completed assignment: ${data.assignment.id}\n`);
|
|
108
|
+
await client.close();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
cmd.command('decline <id>')
|
|
112
|
+
.option('-r, --reason <reason>', 'Reason for declining')
|
|
113
|
+
.description('Decline an assignment')
|
|
114
|
+
.action(async (id, opts) => {
|
|
115
|
+
const client = await getClient();
|
|
116
|
+
const result = await client.call('assignment_decline', {
|
|
117
|
+
id,
|
|
118
|
+
reason: opts.reason || undefined,
|
|
119
|
+
});
|
|
120
|
+
const data = JSON.parse(result);
|
|
121
|
+
console.log(`\n Declined assignment: ${data.assignment.id}\n`);
|
|
122
|
+
await client.close();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
cmd.command('start <id>')
|
|
126
|
+
.description('Start working on an accepted assignment')
|
|
127
|
+
.action(async (id) => {
|
|
128
|
+
const client = await getClient();
|
|
129
|
+
const result = await client.call('assignment_start', { id });
|
|
130
|
+
const data = JSON.parse(result);
|
|
131
|
+
console.log(`\n Started assignment: ${data.assignment.id} (status: ${data.assignment.status})\n`);
|
|
132
|
+
await client.close();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
cmd.command('block <id>')
|
|
136
|
+
.option('-r, --reason <reason>', 'Reason for blocking')
|
|
137
|
+
.description('Mark an assignment as blocked')
|
|
138
|
+
.action(async (id, opts) => {
|
|
139
|
+
const client = await getClient();
|
|
140
|
+
const result = await client.call('assignment_block', {
|
|
141
|
+
id,
|
|
142
|
+
reason: opts.reason || undefined,
|
|
143
|
+
});
|
|
144
|
+
const data = JSON.parse(result);
|
|
145
|
+
console.log(`\n Blocked assignment: ${data.assignment.id}\n`);
|
|
146
|
+
await client.close();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
cmd.command('cancel <id>')
|
|
150
|
+
.option('-r, --reason <reason>', 'Reason for cancelling')
|
|
151
|
+
.description('Cancel an assignment')
|
|
152
|
+
.action(async (id, opts) => {
|
|
153
|
+
const client = await getClient();
|
|
154
|
+
const result = await client.call('assignment_cancel', {
|
|
155
|
+
id,
|
|
156
|
+
reason: opts.reason || undefined,
|
|
157
|
+
});
|
|
158
|
+
const data = JSON.parse(result);
|
|
159
|
+
console.log(`\n Cancelled assignment: ${data.assignment.id}\n`);
|
|
160
|
+
await client.close();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return cmd;
|
|
164
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Copyright 2026 CRMy Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { loadAuthState, saveAuthState, clearAuthState, resolveServerUrl } from '../config.js';
|
|
6
|
+
|
|
7
|
+
export function authCommand(): Command {
|
|
8
|
+
const cmd = new Command('auth').description('Authenticate against a CRMy server');
|
|
9
|
+
|
|
10
|
+
// crmy auth setup
|
|
11
|
+
cmd.command('setup')
|
|
12
|
+
.description('Configure the CRMy server URL')
|
|
13
|
+
.argument('[url]', 'Server URL (e.g. http://localhost:3000)')
|
|
14
|
+
.action(async (urlArg?: string) => {
|
|
15
|
+
let serverUrl = urlArg;
|
|
16
|
+
|
|
17
|
+
if (!serverUrl) {
|
|
18
|
+
const { default: inquirer } = await import('inquirer');
|
|
19
|
+
const answers = await inquirer.prompt([
|
|
20
|
+
{
|
|
21
|
+
type: 'input',
|
|
22
|
+
name: 'serverUrl',
|
|
23
|
+
message: 'CRMy server URL:',
|
|
24
|
+
default: 'http://localhost:3000',
|
|
25
|
+
},
|
|
26
|
+
]);
|
|
27
|
+
serverUrl = answers.serverUrl;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Validate the URL by hitting the health endpoint
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch(`${serverUrl!.replace(/\/$/, '')}/health`);
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
console.error(`Server at ${serverUrl} returned ${res.status}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const data = await res.json() as { status: string; version: string };
|
|
38
|
+
console.log(`\n Connected to CRMy ${data.version} at ${serverUrl}\n`);
|
|
39
|
+
console.log(' Run `crmy login` to authenticate.\n');
|
|
40
|
+
|
|
41
|
+
// Save a partial auth state with just the server URL
|
|
42
|
+
const existing = loadAuthState();
|
|
43
|
+
if (existing) {
|
|
44
|
+
saveAuthState({ ...existing, serverUrl: serverUrl! });
|
|
45
|
+
} else {
|
|
46
|
+
// Store server URL for login to pick up
|
|
47
|
+
saveAuthState({
|
|
48
|
+
serverUrl: serverUrl!,
|
|
49
|
+
token: '',
|
|
50
|
+
user: { id: '', email: '', name: '', role: '', tenant_id: '' },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error(`Could not connect to ${serverUrl}: ${err instanceof Error ? err.message : err}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// crmy login
|
|
60
|
+
cmd.command('login')
|
|
61
|
+
.description('Sign in with email and password')
|
|
62
|
+
.option('-e, --email <email>', 'Email address')
|
|
63
|
+
.option('-p, --password <password>', 'Password')
|
|
64
|
+
.action(async (opts) => {
|
|
65
|
+
const serverUrl = resolveServerUrl();
|
|
66
|
+
if (!serverUrl) {
|
|
67
|
+
console.error('No server configured. Run `crmy auth setup` first.');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let email = opts.email;
|
|
72
|
+
let password = opts.password;
|
|
73
|
+
|
|
74
|
+
if (!email || !password) {
|
|
75
|
+
const { default: inquirer } = await import('inquirer');
|
|
76
|
+
const answers = await inquirer.prompt([
|
|
77
|
+
...(email ? [] : [{ type: 'input', name: 'email', message: 'Email:' }]),
|
|
78
|
+
...(password ? [] : [{ type: 'password', name: 'password', message: 'Password:', mask: '*' }]),
|
|
79
|
+
]);
|
|
80
|
+
email = email ?? answers.email;
|
|
81
|
+
password = password ?? answers.password;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch(`${serverUrl.replace(/\/$/, '')}/auth/login`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'Content-Type': 'application/json' },
|
|
88
|
+
body: JSON.stringify({ email, password }),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const body = await res.json().catch(() => ({ detail: res.statusText })) as { detail?: string };
|
|
93
|
+
console.error(`Login failed: ${body.detail ?? res.statusText}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const data = await res.json() as {
|
|
98
|
+
token: string;
|
|
99
|
+
user: { id: string; email: string; name: string; role: string; tenant_id: string };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Decode JWT to get expiration
|
|
103
|
+
const payloadB64 = data.token.split('.')[1];
|
|
104
|
+
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
|
|
105
|
+
const expiresAt = payload.exp ? new Date(payload.exp * 1000).toISOString() : undefined;
|
|
106
|
+
|
|
107
|
+
saveAuthState({
|
|
108
|
+
serverUrl,
|
|
109
|
+
token: data.token,
|
|
110
|
+
user: data.user,
|
|
111
|
+
expiresAt,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
console.log(`\n Logged in as ${data.user.name} (${data.user.email})`);
|
|
115
|
+
console.log(` Role: ${data.user.role}`);
|
|
116
|
+
if (expiresAt) {
|
|
117
|
+
console.log(` Token expires: ${new Date(expiresAt).toLocaleString()}`);
|
|
118
|
+
}
|
|
119
|
+
console.log();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error(`Login failed: ${err instanceof Error ? err.message : err}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// crmy auth status
|
|
127
|
+
cmd.command('status')
|
|
128
|
+
.description('Show current authentication status')
|
|
129
|
+
.action(() => {
|
|
130
|
+
const auth = loadAuthState();
|
|
131
|
+
if (!auth || !auth.token) {
|
|
132
|
+
console.log('\n Not authenticated. Run `crmy login` to sign in.\n');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log(`\n Server: ${auth.serverUrl}`);
|
|
137
|
+
console.log(` User: ${auth.user.name} (${auth.user.email})`);
|
|
138
|
+
console.log(` Role: ${auth.user.role}`);
|
|
139
|
+
if (auth.expiresAt) {
|
|
140
|
+
const expires = new Date(auth.expiresAt);
|
|
141
|
+
const remaining = expires.getTime() - Date.now();
|
|
142
|
+
if (remaining <= 0) {
|
|
143
|
+
console.log(' Token: Expired — run `crmy login` to re-authenticate');
|
|
144
|
+
} else {
|
|
145
|
+
const mins = Math.floor(remaining / 60000);
|
|
146
|
+
console.log(` Token: Valid (${mins}m remaining)`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
console.log();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// crmy auth logout
|
|
153
|
+
cmd.command('logout')
|
|
154
|
+
.description('Clear stored credentials')
|
|
155
|
+
.action(() => {
|
|
156
|
+
clearAuthState();
|
|
157
|
+
console.log('\n Logged out. Credentials cleared.\n');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return cmd;
|
|
161
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { getClient } from '../client.js';
|
|
5
|
+
|
|
6
|
+
export function briefingCommand(): Command {
|
|
7
|
+
const cmd = new Command('briefing')
|
|
8
|
+
.description('Get a unified briefing for any CRM object — everything you need before taking action')
|
|
9
|
+
.argument('<subject>', 'Subject as type:UUID (e.g. contact:550e8400-...)')
|
|
10
|
+
.option('--format <fmt>', 'Output format (json or text)', 'text')
|
|
11
|
+
.option('--since <duration>', 'Filter activities by duration (e.g. 7d, 24h)')
|
|
12
|
+
.option('--context-types <types>', 'Comma-separated context types to include')
|
|
13
|
+
.option('--include-stale', 'Include superseded context entries')
|
|
14
|
+
.action(async (subject, opts) => {
|
|
15
|
+
const [subjectType, subjectId] = subject.split(':');
|
|
16
|
+
if (!subjectType || !subjectId) {
|
|
17
|
+
console.error('Subject must be in format type:UUID (e.g. contact:550e8400-...)');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const client = await getClient();
|
|
22
|
+
const result = await client.call('briefing_get', {
|
|
23
|
+
subject_type: subjectType,
|
|
24
|
+
subject_id: subjectId,
|
|
25
|
+
format: opts.format,
|
|
26
|
+
since: opts.since,
|
|
27
|
+
context_types: opts.contextTypes ? opts.contextTypes.split(',') : undefined,
|
|
28
|
+
include_stale: opts.includeStale ?? false,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const data = JSON.parse(result);
|
|
32
|
+
if (opts.format === 'text' && data.briefing_text) {
|
|
33
|
+
console.log(data.briefing_text);
|
|
34
|
+
} else {
|
|
35
|
+
console.log(JSON.stringify(data, null, 2));
|
|
36
|
+
}
|
|
37
|
+
await client.close();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return cmd;
|
|
41
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Copyright 2026 CRMy Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { loadConfigFile } from '../config.js';
|
|
6
|
+
|
|
7
|
+
export function configCommand(): Command {
|
|
8
|
+
return new Command('config')
|
|
9
|
+
.description('Show configuration')
|
|
10
|
+
.command('show')
|
|
11
|
+
.action(() => {
|
|
12
|
+
const config = loadConfigFile();
|
|
13
|
+
if (Object.keys(config).length === 0) {
|
|
14
|
+
console.log('No .crmy.json found. Run `crmy init` to create one.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
// Redact sensitive values
|
|
18
|
+
const display = {
|
|
19
|
+
...config,
|
|
20
|
+
apiKey: config.apiKey ? config.apiKey.slice(0, 10) + '...' : undefined,
|
|
21
|
+
jwtSecret: config.jwtSecret ? '***' : undefined,
|
|
22
|
+
};
|
|
23
|
+
console.log(JSON.stringify(display, null, 2));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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 contactsCommand(): Command {
|
|
8
|
+
const cmd = new Command('contacts').description('Manage contacts');
|
|
9
|
+
|
|
10
|
+
cmd.command('list')
|
|
11
|
+
.option('-q, --query <query>', 'Search query')
|
|
12
|
+
.option('--stage <stage>', 'Filter by lifecycle stage')
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
const client = await getClient();
|
|
15
|
+
const result = await client.call('contact_search', {
|
|
16
|
+
query: opts.query,
|
|
17
|
+
lifecycle_stage: opts.stage,
|
|
18
|
+
limit: 20,
|
|
19
|
+
});
|
|
20
|
+
const data = JSON.parse(result);
|
|
21
|
+
if (data.contacts?.length === 0) {
|
|
22
|
+
console.log('No contacts found.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
console.table(data.contacts?.map((c: Record<string, unknown>) => ({
|
|
26
|
+
id: (c.id as string).slice(0, 8),
|
|
27
|
+
name: `${c.first_name} ${c.last_name}`,
|
|
28
|
+
email: c.email ?? '',
|
|
29
|
+
stage: c.lifecycle_stage,
|
|
30
|
+
company: c.company_name ?? '',
|
|
31
|
+
})));
|
|
32
|
+
if (data.total > 20) console.log(`\n Showing 20 of ${data.total} contacts`);
|
|
33
|
+
await client.close();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
cmd.command('get <id>')
|
|
37
|
+
.action(async (id) => {
|
|
38
|
+
const client = await getClient();
|
|
39
|
+
const result = await client.call('contact_get', { id });
|
|
40
|
+
console.log(JSON.parse(result));
|
|
41
|
+
await client.close();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
cmd.command('create')
|
|
45
|
+
.action(async () => {
|
|
46
|
+
const { default: inquirer } = await import('inquirer');
|
|
47
|
+
const answers = await inquirer.prompt([
|
|
48
|
+
{ type: 'input', name: 'first_name', message: 'First name:' },
|
|
49
|
+
{ type: 'input', name: 'last_name', message: 'Last name:' },
|
|
50
|
+
{ type: 'input', name: 'email', message: 'Email:' },
|
|
51
|
+
{ type: 'input', name: 'company_name', message: 'Company:' },
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const client = await getClient();
|
|
55
|
+
const result = await client.call('contact_create', {
|
|
56
|
+
first_name: answers.first_name,
|
|
57
|
+
last_name: answers.last_name || undefined,
|
|
58
|
+
email: answers.email || undefined,
|
|
59
|
+
company_name: answers.company_name || undefined,
|
|
60
|
+
});
|
|
61
|
+
const data = JSON.parse(result);
|
|
62
|
+
console.log(`\n Created contact: ${data.contact.id}\n`);
|
|
63
|
+
await client.close();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
cmd.command('delete <id>')
|
|
67
|
+
.description('Permanently delete a contact (admin/owner only)')
|
|
68
|
+
.action(async (id) => {
|
|
69
|
+
const { default: inquirer } = await import('inquirer');
|
|
70
|
+
const { confirm } = await inquirer.prompt([
|
|
71
|
+
{ type: 'confirm', name: 'confirm', message: `Delete contact ${id}? This cannot be undone.`, default: false },
|
|
72
|
+
]);
|
|
73
|
+
if (!confirm) { console.log(' Cancelled.'); return; }
|
|
74
|
+
|
|
75
|
+
const client = await getClient();
|
|
76
|
+
const result = await client.call('contact_delete', { id });
|
|
77
|
+
const data = JSON.parse(result);
|
|
78
|
+
if (data.deleted) console.log(` Deleted.`);
|
|
79
|
+
await client.close();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return cmd;
|
|
83
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { getClient } from '../client.js';
|
|
5
|
+
|
|
6
|
+
export function contextTypesCommand(): Command {
|
|
7
|
+
const cmd = new Command('context-types').description('Manage context type registry');
|
|
8
|
+
|
|
9
|
+
cmd.command('list')
|
|
10
|
+
.action(async () => {
|
|
11
|
+
const client = await getClient();
|
|
12
|
+
const result = await client.call('context_type_list', {});
|
|
13
|
+
const data = JSON.parse(result);
|
|
14
|
+
if (data.context_types?.length === 0) {
|
|
15
|
+
console.log('No context types found.');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
console.table(data.context_types?.map((t: Record<string, unknown>) => ({
|
|
19
|
+
type_name: t.type_name,
|
|
20
|
+
label: t.label,
|
|
21
|
+
description: ((t.description as string) ?? '').slice(0, 60),
|
|
22
|
+
default: t.is_default ? '✓' : '',
|
|
23
|
+
})));
|
|
24
|
+
await client.close();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
cmd.command('add <type_name>')
|
|
28
|
+
.requiredOption('--label <label>', 'Display label')
|
|
29
|
+
.option('--description <desc>', 'Description')
|
|
30
|
+
.action(async (typeName, opts) => {
|
|
31
|
+
const client = await getClient();
|
|
32
|
+
const result = await client.call('context_type_add', {
|
|
33
|
+
type_name: typeName,
|
|
34
|
+
label: opts.label,
|
|
35
|
+
description: opts.description,
|
|
36
|
+
});
|
|
37
|
+
const data = JSON.parse(result);
|
|
38
|
+
console.log(`\n Added context type: ${data.context_type.type_name}\n`);
|
|
39
|
+
await client.close();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
cmd.command('remove <type_name>')
|
|
43
|
+
.description('Remove a custom context type (cannot remove defaults)')
|
|
44
|
+
.action(async (typeName) => {
|
|
45
|
+
const client = await getClient();
|
|
46
|
+
try {
|
|
47
|
+
await client.call('context_type_remove', { type_name: typeName });
|
|
48
|
+
console.log(`\n Removed context type: ${typeName}\n`);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error(`\n Error: ${err instanceof Error ? err.message : err}\n`);
|
|
51
|
+
}
|
|
52
|
+
await client.close();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return cmd;
|
|
56
|
+
}
|