@ebowwa/crm 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.
- package/README.md +174 -0
- package/dist/cli/commands/activities.d.ts +11 -0
- package/dist/cli/commands/activities.d.ts.map +1 -0
- package/dist/cli/commands/activities.js +427 -0
- package/dist/cli/commands/activities.js.map +1 -0
- package/dist/cli/commands/contacts.d.ts +11 -0
- package/dist/cli/commands/contacts.d.ts.map +1 -0
- package/dist/cli/commands/contacts.js +458 -0
- package/dist/cli/commands/contacts.js.map +1 -0
- package/dist/cli/commands/deals.d.ts +11 -0
- package/dist/cli/commands/deals.d.ts.map +1 -0
- package/dist/cli/commands/deals.js +498 -0
- package/dist/cli/commands/deals.js.map +1 -0
- package/dist/cli/commands/media.d.ts +11 -0
- package/dist/cli/commands/media.d.ts.map +1 -0
- package/dist/cli/commands/media.js +417 -0
- package/dist/cli/commands/media.js.map +1 -0
- package/dist/cli/commands/search.d.ts +11 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/search.js +346 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +173 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/repl.d.ts +15 -0
- package/dist/cli/repl.d.ts.map +1 -0
- package/dist/cli/repl.js +318 -0
- package/dist/cli/repl.js.map +1 -0
- package/dist/cli/utils/config.d.ts +91 -0
- package/dist/cli/utils/config.d.ts.map +1 -0
- package/dist/cli/utils/config.js +212 -0
- package/dist/cli/utils/config.js.map +1 -0
- package/dist/cli/utils/output.d.ts +136 -0
- package/dist/cli/utils/output.d.ts.map +1 -0
- package/dist/cli/utils/output.js +323 -0
- package/dist/cli/utils/output.js.map +1 -0
- package/dist/cli/utils/prompt.d.ts +81 -0
- package/dist/cli/utils/prompt.d.ts.map +1 -0
- package/dist/cli/utils/prompt.js +341 -0
- package/dist/cli/utils/prompt.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +32 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/schemas.d.ts +3050 -0
- package/dist/core/schemas.d.ts.map +1 -0
- package/dist/core/schemas.js +667 -0
- package/dist/core/schemas.js.map +1 -0
- package/dist/core/types.d.ts +597 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +8 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +14 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +11 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +13 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +18 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/storage/client.d.ts +109 -0
- package/dist/mcp/storage/client.d.ts.map +1 -0
- package/dist/mcp/storage/client.js +355 -0
- package/dist/mcp/storage/client.js.map +1 -0
- package/dist/mcp/storage/index.d.ts +7 -0
- package/dist/mcp/storage/index.d.ts.map +1 -0
- package/dist/mcp/storage/index.js +6 -0
- package/dist/mcp/storage/index.js.map +1 -0
- package/dist/mcp/storage/types.d.ts +44 -0
- package/dist/mcp/storage/types.d.ts.map +1 -0
- package/dist/mcp/storage/types.js +35 -0
- package/dist/mcp/storage/types.js.map +1 -0
- package/dist/mcp/tools/definitions.d.ts +16 -0
- package/dist/mcp/tools/definitions.d.ts.map +1 -0
- package/dist/mcp/tools/definitions.js +914 -0
- package/dist/mcp/tools/definitions.js.map +1 -0
- package/dist/mcp/tools/handlers.d.ts +50 -0
- package/dist/mcp/tools/handlers.d.ts.map +1 -0
- package/dist/mcp/tools/handlers.js +760 -0
- package/dist/mcp/tools/handlers.js.map +1 -0
- package/dist/mcp/tools/index.d.ts +7 -0
- package/dist/mcp/tools/index.d.ts.map +1 -0
- package/dist/mcp/tools/index.js +6 -0
- package/dist/mcp/tools/index.js.map +1 -0
- package/dist/mcp/tools/types.d.ts +314 -0
- package/dist/mcp/tools/types.d.ts.map +1 -0
- package/dist/mcp/tools/types.js +5 -0
- package/dist/mcp/tools/types.js.map +1 -0
- package/dist/mcp/transports/stdio.d.ts +27 -0
- package/dist/mcp/transports/stdio.d.ts.map +1 -0
- package/dist/mcp/transports/stdio.js +237 -0
- package/dist/mcp/transports/stdio.js.map +1 -0
- package/dist/telemetry/index.d.ts +58 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +109 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/logger.d.ts +116 -0
- package/dist/telemetry/logger.d.ts.map +1 -0
- package/dist/telemetry/logger.js +256 -0
- package/dist/telemetry/logger.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +115 -0
- package/dist/telemetry/metrics.d.ts.map +1 -0
- package/dist/telemetry/metrics.js +292 -0
- package/dist/telemetry/metrics.js.map +1 -0
- package/dist/telemetry/tracer.d.ts +227 -0
- package/dist/telemetry/tracer.d.ts.map +1 -0
- package/dist/telemetry/tracer.js +355 -0
- package/dist/telemetry/tracer.js.map +1 -0
- package/dist/web/app.d.ts +2 -0
- package/dist/web/app.d.ts.map +1 -0
- package/dist/web/app.js +115 -0
- package/dist/web/app.js.map +1 -0
- package/dist/web/components/ContactList.d.ts +3 -0
- package/dist/web/components/ContactList.d.ts.map +1 -0
- package/dist/web/components/ContactList.js +262 -0
- package/dist/web/components/ContactList.js.map +1 -0
- package/dist/web/components/Dashboard.d.ts +3 -0
- package/dist/web/components/Dashboard.d.ts.map +1 -0
- package/dist/web/components/Dashboard.js +158 -0
- package/dist/web/components/Dashboard.js.map +1 -0
- package/dist/web/components/DealPipeline.d.ts +3 -0
- package/dist/web/components/DealPipeline.d.ts.map +1 -0
- package/dist/web/components/DealPipeline.js +306 -0
- package/dist/web/components/DealPipeline.js.map +1 -0
- package/dist/web/index.d.ts +2 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +269 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/types.d.ts +75 -0
- package/dist/web/types.d.ts.map +1 -0
- package/dist/web/types.js +3 -0
- package/dist/web/types.js.map +1 -0
- package/native/index.d.ts +571 -0
- package/native/index.js +687 -0
- package/package.json +105 -0
- package/src/cli/commands/activities.ts +543 -0
- package/src/cli/commands/contacts.ts +563 -0
- package/src/cli/commands/deals.ts +637 -0
- package/src/cli/commands/media.ts +521 -0
- package/src/cli/commands/search.ts +426 -0
- package/src/cli/index.ts +203 -0
- package/src/cli/repl.ts +379 -0
- package/src/cli/utils/config.ts +299 -0
- package/src/cli/utils/output.ts +386 -0
- package/src/cli/utils/prompt.ts +444 -0
- package/src/cli.ts +11 -0
- package/src/core/index.ts +184 -0
- package/src/core/schemas.ts +770 -0
- package/src/core/types.ts +969 -0
- package/src/index.ts +8 -0
- package/src/mcp/index.ts +17 -0
- package/src/mcp/server.ts +26 -0
- package/src/mcp/storage/client.ts +408 -0
- package/src/mcp/storage/index.ts +7 -0
- package/src/mcp/storage/types.ts +72 -0
- package/src/mcp/tools/definitions.ts +961 -0
- package/src/mcp/tools/handlers.ts +805 -0
- package/src/mcp/tools/index.ts +7 -0
- package/src/mcp/tools/types.ts +390 -0
- package/src/mcp/transports/stdio.ts +225 -0
- package/src/telemetry/index.ts +131 -0
- package/src/telemetry/logger.ts +318 -0
- package/src/telemetry/metrics.ts +393 -0
- package/src/telemetry/tracer.ts +487 -0
- package/src/web/api/activities.ts +41 -0
- package/src/web/api/contacts.ts +114 -0
- package/src/web/api/deals.ts +108 -0
- package/src/web/api/media.ts +98 -0
- package/src/web/app.tsx +143 -0
- package/src/web/components/ActivityFeed.tsx +195 -0
- package/src/web/components/ContactList.tsx +340 -0
- package/src/web/components/Dashboard.tsx +214 -0
- package/src/web/components/DealPipeline.tsx +405 -0
- package/src/web/components/MediaGallery.tsx +334 -0
- package/src/web/index.html +14 -0
- package/src/web/index.ts +326 -0
- package/src/web/styles/main.css +180 -0
- package/src/web/types.ts +311 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact Commands
|
|
3
|
+
*
|
|
4
|
+
* CLI commands for managing contacts in the CRM.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import {
|
|
9
|
+
printSuccess,
|
|
10
|
+
printError,
|
|
11
|
+
printWarning,
|
|
12
|
+
printTable,
|
|
13
|
+
printJSON,
|
|
14
|
+
printHeader,
|
|
15
|
+
printDivider,
|
|
16
|
+
formatStatus,
|
|
17
|
+
formatRelativeTime,
|
|
18
|
+
truncate,
|
|
19
|
+
output,
|
|
20
|
+
} from '../utils/output.js';
|
|
21
|
+
import {
|
|
22
|
+
prompt,
|
|
23
|
+
promptRequired,
|
|
24
|
+
promptEmail,
|
|
25
|
+
promptTags,
|
|
26
|
+
confirm,
|
|
27
|
+
select,
|
|
28
|
+
multiSelect,
|
|
29
|
+
previewAndConfirm,
|
|
30
|
+
Spinner,
|
|
31
|
+
} from '../utils/prompt.js';
|
|
32
|
+
import { loadConfig, addRecentContact, getConfig } from '../utils/config.js';
|
|
33
|
+
import { CRMStorageClient } from '../../mcp/storage/client.js';
|
|
34
|
+
import type { Contact, ContactStatus, ContactSource } from '../../core/types.js';
|
|
35
|
+
|
|
36
|
+
// Contact status options
|
|
37
|
+
const CONTACT_STATUSES: ContactStatus[] = [
|
|
38
|
+
'lead',
|
|
39
|
+
'prospect',
|
|
40
|
+
'qualified',
|
|
41
|
+
'customer',
|
|
42
|
+
'churned',
|
|
43
|
+
'archived',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Contact source options
|
|
47
|
+
const CONTACT_SOURCES: ContactSource[] = [
|
|
48
|
+
'organic',
|
|
49
|
+
'referral',
|
|
50
|
+
'advertisement',
|
|
51
|
+
'social_media',
|
|
52
|
+
'email_campaign',
|
|
53
|
+
'website',
|
|
54
|
+
'event',
|
|
55
|
+
'cold_outreach',
|
|
56
|
+
'partner',
|
|
57
|
+
'other',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// Storage client singleton
|
|
61
|
+
let _storage: CRMStorageClient | null = null;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get storage client (lazy initialization)
|
|
65
|
+
*/
|
|
66
|
+
async function getStorage(): Promise<CRMStorageClient> {
|
|
67
|
+
if (!_storage) {
|
|
68
|
+
_storage = new CRMStorageClient({
|
|
69
|
+
path: process.env.CRM_DB_PATH || './data/crm-cli',
|
|
70
|
+
mapSize: 256 * 1024 * 1024, // 256MB
|
|
71
|
+
maxDbs: 20,
|
|
72
|
+
});
|
|
73
|
+
await _storage.initialize();
|
|
74
|
+
}
|
|
75
|
+
return _storage;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get all contacts
|
|
80
|
+
*/
|
|
81
|
+
async function getContacts(): Promise<Contact[]> {
|
|
82
|
+
const storage = await getStorage();
|
|
83
|
+
return storage.list('contacts');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get contact by ID
|
|
88
|
+
*/
|
|
89
|
+
async function getContact(id: string): Promise<Contact | null> {
|
|
90
|
+
const storage = await getStorage();
|
|
91
|
+
return storage.get('contacts', id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create contact
|
|
96
|
+
*/
|
|
97
|
+
async function createContact(data: Partial<Contact>): Promise<Contact> {
|
|
98
|
+
const storage = await getStorage();
|
|
99
|
+
return storage.insert('contacts', {
|
|
100
|
+
name: data.name || '',
|
|
101
|
+
firstName: data.firstName,
|
|
102
|
+
lastName: data.lastName,
|
|
103
|
+
emails: data.emails || [],
|
|
104
|
+
phones: data.phones || [],
|
|
105
|
+
addresses: data.addresses || [],
|
|
106
|
+
company: data.company,
|
|
107
|
+
title: data.title,
|
|
108
|
+
department: data.department,
|
|
109
|
+
socialProfiles: data.socialProfiles || [],
|
|
110
|
+
website: data.website,
|
|
111
|
+
tags: data.tags || [],
|
|
112
|
+
customFields: data.customFields || [],
|
|
113
|
+
source: data.source,
|
|
114
|
+
status: data.status || 'lead',
|
|
115
|
+
ownerId: data.ownerId,
|
|
116
|
+
avatar: data.avatar,
|
|
117
|
+
preferredContact: data.preferredContact,
|
|
118
|
+
language: data.language,
|
|
119
|
+
timezone: data.timezone,
|
|
120
|
+
preferences: data.preferences,
|
|
121
|
+
leadScore: data.leadScore,
|
|
122
|
+
doNotContact: data.doNotContact || false,
|
|
123
|
+
lastContactedAt: data.lastContactedAt,
|
|
124
|
+
nextFollowUpAt: data.nextFollowUpAt,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Update contact
|
|
130
|
+
*/
|
|
131
|
+
async function updateContact(id: string, data: Partial<Contact>): Promise<Contact | null> {
|
|
132
|
+
const storage = await getStorage();
|
|
133
|
+
const existing = await storage.get('contacts', id);
|
|
134
|
+
if (!existing) return null;
|
|
135
|
+
return storage.update('contacts', id, data);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Delete contact
|
|
140
|
+
*/
|
|
141
|
+
async function deleteContact(id: string): Promise<boolean> {
|
|
142
|
+
const storage = await getStorage();
|
|
143
|
+
const existing = await storage.get('contacts', id);
|
|
144
|
+
if (!existing) return false;
|
|
145
|
+
await storage.delete('contacts', id);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Search contacts
|
|
151
|
+
*/
|
|
152
|
+
async function searchContacts(query: string): Promise<Contact[]> {
|
|
153
|
+
const contacts = await getContacts();
|
|
154
|
+
const q = query.toLowerCase();
|
|
155
|
+
return contacts.filter((c) => {
|
|
156
|
+
return (
|
|
157
|
+
c.name.toLowerCase().includes(q) ||
|
|
158
|
+
c.emails.some((e) => e.email.toLowerCase().includes(q)) ||
|
|
159
|
+
c.company?.toLowerCase().includes(q) ||
|
|
160
|
+
c.tags.some((t) => t.toLowerCase().includes(q))
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Filter contacts by tag
|
|
167
|
+
*/
|
|
168
|
+
async function filterByTag(tag: string): Promise<Contact[]> {
|
|
169
|
+
const contacts = await getContacts();
|
|
170
|
+
return contacts.filter((c) =>
|
|
171
|
+
c.tags.some((t) => t.toLowerCase() === tag.toLowerCase())
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Register contact commands
|
|
177
|
+
*/
|
|
178
|
+
export function registerContactCommands(program: Command): void {
|
|
179
|
+
const contacts = program.command('contacts').description('Manage contacts');
|
|
180
|
+
|
|
181
|
+
// List contacts
|
|
182
|
+
contacts
|
|
183
|
+
.command('list')
|
|
184
|
+
.description('List all contacts')
|
|
185
|
+
.option('-s, --search <query>', 'Search contacts')
|
|
186
|
+
.option('-t, --tag <tag>', 'Filter by tag')
|
|
187
|
+
.option('--status <status>', 'Filter by status')
|
|
188
|
+
.option('--json', 'Output as JSON')
|
|
189
|
+
.option('--limit <number>', 'Limit results', '20')
|
|
190
|
+
.action(async (options) => {
|
|
191
|
+
let contacts: Contact[];
|
|
192
|
+
|
|
193
|
+
if (options.search) {
|
|
194
|
+
contacts = await searchContacts(options.search);
|
|
195
|
+
} else if (options.tag) {
|
|
196
|
+
contacts = await filterByTag(options.tag);
|
|
197
|
+
} else {
|
|
198
|
+
contacts = await getContacts();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (options.status) {
|
|
202
|
+
contacts = contacts.filter(
|
|
203
|
+
(c) => c.status.toLowerCase() === options.status.toLowerCase()
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const limit = parseInt(options.limit);
|
|
208
|
+
contacts = contacts.slice(0, limit);
|
|
209
|
+
|
|
210
|
+
if (options.json) {
|
|
211
|
+
printJSON(contacts);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (contacts.length === 0) {
|
|
216
|
+
printWarning('No contacts found');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
printHeader(`Contacts (${contacts.length})`);
|
|
221
|
+
|
|
222
|
+
printTable(contacts, [
|
|
223
|
+
{ key: 'id', header: 'ID', width: 8, format: (v) => truncate(String(v), 8) },
|
|
224
|
+
{ key: 'name', header: 'Name', width: 20 },
|
|
225
|
+
{
|
|
226
|
+
key: 'emails',
|
|
227
|
+
header: 'Email',
|
|
228
|
+
width: 25,
|
|
229
|
+
format: (v) => {
|
|
230
|
+
const emails = v as { email: string; primary: boolean }[];
|
|
231
|
+
const primary = emails.find((e) => e.primary) || emails[0];
|
|
232
|
+
return primary?.email || '';
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{ key: 'company', header: 'Company', width: 15, format: (v) => truncate(String(v || ''), 15) },
|
|
236
|
+
{ key: 'status', header: 'Status', format: (v) => formatStatus(String(v)) },
|
|
237
|
+
{
|
|
238
|
+
key: 'updatedAt',
|
|
239
|
+
header: 'Updated',
|
|
240
|
+
format: (v) => formatRelativeTime(String(v)),
|
|
241
|
+
},
|
|
242
|
+
]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Get contact details
|
|
246
|
+
contacts
|
|
247
|
+
.command('get <id>')
|
|
248
|
+
.description('Get contact details')
|
|
249
|
+
.option('--json', 'Output as JSON')
|
|
250
|
+
.action(async (id, options) => {
|
|
251
|
+
const contact = await getContact(id);
|
|
252
|
+
|
|
253
|
+
if (!contact) {
|
|
254
|
+
printError(`Contact not found: ${id}`);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
addRecentContact(id);
|
|
259
|
+
|
|
260
|
+
if (options.json) {
|
|
261
|
+
printJSON(contact);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
printHeader(`Contact: ${contact.name}`);
|
|
266
|
+
|
|
267
|
+
console.log(`${output.dim('ID:')} ${contact.id}`);
|
|
268
|
+
console.log(`${output.dim('Status:')} ${formatStatus(contact.status)}`);
|
|
269
|
+
console.log();
|
|
270
|
+
|
|
271
|
+
// Contact Info
|
|
272
|
+
console.log(output.bold('Contact Information'));
|
|
273
|
+
printDivider('-');
|
|
274
|
+
|
|
275
|
+
if (contact.emails.length > 0) {
|
|
276
|
+
contact.emails.forEach((e) => {
|
|
277
|
+
const primary = e.primary ? ' (primary)' : '';
|
|
278
|
+
console.log(`${output.dim('Email:')} ${e.email}${primary}`);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (contact.phones.length > 0) {
|
|
283
|
+
contact.phones.forEach((p) => {
|
|
284
|
+
const primary = p.primary ? ' (primary)' : '';
|
|
285
|
+
console.log(`${output.dim('Phone:')} ${p.number} [${p.type}]${primary}`);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (contact.company) {
|
|
290
|
+
console.log(`${output.dim('Company:')} ${contact.company}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (contact.title) {
|
|
294
|
+
console.log(`${output.dim('Title:')} ${contact.title}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (contact.website) {
|
|
298
|
+
console.log(`${output.dim('Website:')} ${contact.website}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.log();
|
|
302
|
+
|
|
303
|
+
// Tags
|
|
304
|
+
if (contact.tags.length > 0) {
|
|
305
|
+
console.log(output.bold('Tags'));
|
|
306
|
+
printDivider('-');
|
|
307
|
+
console.log(contact.tags.map((t) => output.info(`#${t}`)).join(' '));
|
|
308
|
+
console.log();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Metadata
|
|
312
|
+
console.log(output.bold('Metadata'));
|
|
313
|
+
printDivider('-');
|
|
314
|
+
console.log(`${output.dim('Created:')} ${new Date(contact.createdAt).toLocaleString()}`);
|
|
315
|
+
console.log(`${output.dim('Updated:')} ${new Date(contact.updatedAt).toLocaleString()}`);
|
|
316
|
+
|
|
317
|
+
if (contact.source) {
|
|
318
|
+
console.log(`${output.dim('Source:')} ${contact.source}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (contact.leadScore !== undefined) {
|
|
322
|
+
console.log(`${output.dim('Lead Score:')} ${contact.leadScore}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
console.log();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Create contact
|
|
329
|
+
contacts
|
|
330
|
+
.command('create')
|
|
331
|
+
.description('Create a new contact')
|
|
332
|
+
.option('--name <name>', 'Contact name')
|
|
333
|
+
.option('--email <email>', 'Email address')
|
|
334
|
+
.option('--phone <phone>', 'Phone number')
|
|
335
|
+
.option('--company <company>', 'Company name')
|
|
336
|
+
.option('--title <title>', 'Job title')
|
|
337
|
+
.option('--status <status>', 'Contact status')
|
|
338
|
+
.option('--tags <tags>', 'Tags (comma-separated)')
|
|
339
|
+
.option('--source <source>', 'Contact source')
|
|
340
|
+
.option('--no-prompt', 'Skip interactive prompts')
|
|
341
|
+
.action(async (options) => {
|
|
342
|
+
printHeader('Create Contact');
|
|
343
|
+
|
|
344
|
+
// Gather data
|
|
345
|
+
const name = options.name || (await promptRequired('Name'));
|
|
346
|
+
const email = options.email || (await promptEmail('Email (primary)'));
|
|
347
|
+
const phone = options.phone || (await prompt('Phone'));
|
|
348
|
+
const company = options.company || (await prompt('Company'));
|
|
349
|
+
const title = options.title || (await prompt('Title'));
|
|
350
|
+
|
|
351
|
+
let status: ContactStatus = 'lead';
|
|
352
|
+
if (options.status) {
|
|
353
|
+
status = options.status as ContactStatus;
|
|
354
|
+
} else if (!options.noPrompt) {
|
|
355
|
+
status = await select('Status', CONTACT_STATUSES, 'lead');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let tags: string[] = [];
|
|
359
|
+
if (options.tags) {
|
|
360
|
+
tags = options.tags.split(',').map((t: string) => t.trim());
|
|
361
|
+
} else if (!options.noPrompt) {
|
|
362
|
+
tags = await promptTags('Tags (comma-separated)');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let source: ContactSource | undefined;
|
|
366
|
+
if (options.source) {
|
|
367
|
+
source = options.source as ContactSource;
|
|
368
|
+
} else if (!options.noPrompt) {
|
|
369
|
+
const hasSource = await confirm('Add contact source?');
|
|
370
|
+
if (hasSource) {
|
|
371
|
+
source = await select('Source', CONTACT_SOURCES);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Build contact data
|
|
376
|
+
const contactData: Partial<Contact> = {
|
|
377
|
+
name,
|
|
378
|
+
emails: [{ email, type: 'personal', primary: true }],
|
|
379
|
+
phones: phone ? [{ number: phone, type: 'mobile', primary: true }] : [],
|
|
380
|
+
company,
|
|
381
|
+
title,
|
|
382
|
+
status,
|
|
383
|
+
tags,
|
|
384
|
+
source,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// Preview
|
|
388
|
+
console.log();
|
|
389
|
+
const confirmed = await previewAndConfirm('Contact Preview', {
|
|
390
|
+
Name: name,
|
|
391
|
+
Email: email,
|
|
392
|
+
Phone: phone || '(none)',
|
|
393
|
+
Company: company || '(none)',
|
|
394
|
+
Title: title || '(none)',
|
|
395
|
+
Status: status,
|
|
396
|
+
Tags: tags.length > 0 ? tags.join(', ') : '(none)',
|
|
397
|
+
Source: source || '(none)',
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (!confirmed) {
|
|
401
|
+
printWarning('Contact creation cancelled');
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Create contact
|
|
406
|
+
const spinner = new Spinner('Creating contact...').start();
|
|
407
|
+
|
|
408
|
+
const contact = await createContact(contactData);
|
|
409
|
+
spinner.succeed('Contact created successfully');
|
|
410
|
+
|
|
411
|
+
printSuccess(`Contact ID: ${contact.id}`);
|
|
412
|
+
console.log();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Update contact
|
|
416
|
+
contacts
|
|
417
|
+
.command('update <id>')
|
|
418
|
+
.description('Update a contact')
|
|
419
|
+
.option('--name <name>', 'Contact name')
|
|
420
|
+
.option('--email <email>', 'Email address')
|
|
421
|
+
.option('--phone <phone>', 'Phone number')
|
|
422
|
+
.option('--company <company>', 'Company name')
|
|
423
|
+
.option('--title <title>', 'Job title')
|
|
424
|
+
.option('--status <status>', 'Contact status')
|
|
425
|
+
.option('--tags <tags>', 'Tags (comma-separated)')
|
|
426
|
+
.action(async (id, options) => {
|
|
427
|
+
const contact = await getContact(id);
|
|
428
|
+
|
|
429
|
+
if (!contact) {
|
|
430
|
+
printError(`Contact not found: ${id}`);
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
printHeader(`Update Contact: ${contact.name}`);
|
|
435
|
+
|
|
436
|
+
const updates: Partial<Contact> = {};
|
|
437
|
+
|
|
438
|
+
if (options.name) {
|
|
439
|
+
updates.name = options.name;
|
|
440
|
+
} else {
|
|
441
|
+
const name = await prompt('Name', contact.name);
|
|
442
|
+
if (name !== contact.name) updates.name = name;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (options.email) {
|
|
446
|
+
updates.emails = [{ email: options.email, type: 'personal', primary: true }];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (options.phone) {
|
|
450
|
+
updates.phones = [{ number: options.phone, type: 'mobile', primary: true }];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (options.company) {
|
|
454
|
+
updates.company = options.company;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (options.title) {
|
|
458
|
+
updates.title = options.title;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (options.status) {
|
|
462
|
+
updates.status = options.status as ContactStatus;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (options.tags) {
|
|
466
|
+
updates.tags = options.tags.split(',').map((t: string) => t.trim());
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (Object.keys(updates).length === 0) {
|
|
470
|
+
printWarning('No changes to update');
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const confirmed = await confirm('Save changes?');
|
|
475
|
+
if (!confirmed) {
|
|
476
|
+
printWarning('Update cancelled');
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const spinner = new Spinner('Updating contact...').start();
|
|
481
|
+
|
|
482
|
+
const updated = await updateContact(id, updates);
|
|
483
|
+
spinner.succeed('Contact updated successfully');
|
|
484
|
+
|
|
485
|
+
if (updated) {
|
|
486
|
+
printSuccess(`Updated ${Object.keys(updates).join(', ')}`);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// Delete contact
|
|
491
|
+
contacts
|
|
492
|
+
.command('delete <id>')
|
|
493
|
+
.description('Delete a contact')
|
|
494
|
+
.option('-f, --force', 'Skip confirmation')
|
|
495
|
+
.action(async (id, options) => {
|
|
496
|
+
const contact = await getContact(id);
|
|
497
|
+
|
|
498
|
+
if (!contact) {
|
|
499
|
+
printError(`Contact not found: ${id}`);
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (!options.force) {
|
|
504
|
+
console.log(`\nContact: ${output.bold(contact.name)}`);
|
|
505
|
+
console.log(`Email: ${contact.emails[0]?.email || 'N/A'}`);
|
|
506
|
+
console.log(`Company: ${contact.company || 'N/A'}\n`);
|
|
507
|
+
|
|
508
|
+
const confirmed = await confirm(
|
|
509
|
+
'Are you sure you want to delete this contact?'
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
if (!confirmed) {
|
|
513
|
+
printWarning('Deletion cancelled');
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const spinner = new Spinner('Deleting contact...').start();
|
|
519
|
+
|
|
520
|
+
await deleteContact(id);
|
|
521
|
+
spinner.succeed('Contact deleted successfully');
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Add tags
|
|
525
|
+
contacts
|
|
526
|
+
.command('tag <id> <tags>')
|
|
527
|
+
.description('Add tags to a contact')
|
|
528
|
+
.action(async (id, tagsStr) => {
|
|
529
|
+
const contact = await getContact(id);
|
|
530
|
+
|
|
531
|
+
if (!contact) {
|
|
532
|
+
printError(`Contact not found: ${id}`);
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const newTags = tagsStr.split(',').map((t: string) => t.trim());
|
|
537
|
+
const allTags = [...new Set([...contact.tags, ...newTags])];
|
|
538
|
+
|
|
539
|
+
await updateContact(id, { tags: allTags });
|
|
540
|
+
printSuccess(`Added tags: ${newTags.join(', ')}`);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Remove tags
|
|
544
|
+
contacts
|
|
545
|
+
.command('untag <id> <tags>')
|
|
546
|
+
.description('Remove tags from a contact')
|
|
547
|
+
.action(async (id, tagsStr) => {
|
|
548
|
+
const contact = await getContact(id);
|
|
549
|
+
|
|
550
|
+
if (!contact) {
|
|
551
|
+
printError(`Contact not found: ${id}`);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const removeTags = tagsStr.split(',').map((t: string) => t.trim().toLowerCase());
|
|
556
|
+
const filteredTags = contact.tags.filter(
|
|
557
|
+
(t) => !removeTags.includes(t.toLowerCase())
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
await updateContact(id, { tags: filteredTags });
|
|
561
|
+
printSuccess(`Removed tags: ${removeTags.join(', ')}`);
|
|
562
|
+
});
|
|
563
|
+
}
|