@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.
Files changed (187) hide show
  1. package/README.md +174 -0
  2. package/dist/cli/commands/activities.d.ts +11 -0
  3. package/dist/cli/commands/activities.d.ts.map +1 -0
  4. package/dist/cli/commands/activities.js +427 -0
  5. package/dist/cli/commands/activities.js.map +1 -0
  6. package/dist/cli/commands/contacts.d.ts +11 -0
  7. package/dist/cli/commands/contacts.d.ts.map +1 -0
  8. package/dist/cli/commands/contacts.js +458 -0
  9. package/dist/cli/commands/contacts.js.map +1 -0
  10. package/dist/cli/commands/deals.d.ts +11 -0
  11. package/dist/cli/commands/deals.d.ts.map +1 -0
  12. package/dist/cli/commands/deals.js +498 -0
  13. package/dist/cli/commands/deals.js.map +1 -0
  14. package/dist/cli/commands/media.d.ts +11 -0
  15. package/dist/cli/commands/media.d.ts.map +1 -0
  16. package/dist/cli/commands/media.js +417 -0
  17. package/dist/cli/commands/media.js.map +1 -0
  18. package/dist/cli/commands/search.d.ts +11 -0
  19. package/dist/cli/commands/search.d.ts.map +1 -0
  20. package/dist/cli/commands/search.js +346 -0
  21. package/dist/cli/commands/search.js.map +1 -0
  22. package/dist/cli/index.d.ts +13 -0
  23. package/dist/cli/index.d.ts.map +1 -0
  24. package/dist/cli/index.js +173 -0
  25. package/dist/cli/index.js.map +1 -0
  26. package/dist/cli/repl.d.ts +15 -0
  27. package/dist/cli/repl.d.ts.map +1 -0
  28. package/dist/cli/repl.js +318 -0
  29. package/dist/cli/repl.js.map +1 -0
  30. package/dist/cli/utils/config.d.ts +91 -0
  31. package/dist/cli/utils/config.d.ts.map +1 -0
  32. package/dist/cli/utils/config.js +212 -0
  33. package/dist/cli/utils/config.js.map +1 -0
  34. package/dist/cli/utils/output.d.ts +136 -0
  35. package/dist/cli/utils/output.d.ts.map +1 -0
  36. package/dist/cli/utils/output.js +323 -0
  37. package/dist/cli/utils/output.js.map +1 -0
  38. package/dist/cli/utils/prompt.d.ts +81 -0
  39. package/dist/cli/utils/prompt.d.ts.map +1 -0
  40. package/dist/cli/utils/prompt.js +341 -0
  41. package/dist/cli/utils/prompt.js.map +1 -0
  42. package/dist/cli.d.ts +3 -0
  43. package/dist/cli.d.ts.map +1 -0
  44. package/dist/cli.js +8 -0
  45. package/dist/cli.js.map +1 -0
  46. package/dist/core/index.d.ts +6 -0
  47. package/dist/core/index.d.ts.map +1 -0
  48. package/dist/core/index.js +32 -0
  49. package/dist/core/index.js.map +1 -0
  50. package/dist/core/schemas.d.ts +3050 -0
  51. package/dist/core/schemas.d.ts.map +1 -0
  52. package/dist/core/schemas.js +667 -0
  53. package/dist/core/schemas.js.map +1 -0
  54. package/dist/core/types.d.ts +597 -0
  55. package/dist/core/types.d.ts.map +1 -0
  56. package/dist/core/types.js +8 -0
  57. package/dist/core/types.js.map +1 -0
  58. package/dist/index.d.ts +7 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +8 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/mcp/index.d.ts +14 -0
  63. package/dist/mcp/index.d.ts.map +1 -0
  64. package/dist/mcp/index.js +11 -0
  65. package/dist/mcp/index.js.map +1 -0
  66. package/dist/mcp/server.d.ts +13 -0
  67. package/dist/mcp/server.d.ts.map +1 -0
  68. package/dist/mcp/server.js +18 -0
  69. package/dist/mcp/server.js.map +1 -0
  70. package/dist/mcp/storage/client.d.ts +109 -0
  71. package/dist/mcp/storage/client.d.ts.map +1 -0
  72. package/dist/mcp/storage/client.js +355 -0
  73. package/dist/mcp/storage/client.js.map +1 -0
  74. package/dist/mcp/storage/index.d.ts +7 -0
  75. package/dist/mcp/storage/index.d.ts.map +1 -0
  76. package/dist/mcp/storage/index.js +6 -0
  77. package/dist/mcp/storage/index.js.map +1 -0
  78. package/dist/mcp/storage/types.d.ts +44 -0
  79. package/dist/mcp/storage/types.d.ts.map +1 -0
  80. package/dist/mcp/storage/types.js +35 -0
  81. package/dist/mcp/storage/types.js.map +1 -0
  82. package/dist/mcp/tools/definitions.d.ts +16 -0
  83. package/dist/mcp/tools/definitions.d.ts.map +1 -0
  84. package/dist/mcp/tools/definitions.js +914 -0
  85. package/dist/mcp/tools/definitions.js.map +1 -0
  86. package/dist/mcp/tools/handlers.d.ts +50 -0
  87. package/dist/mcp/tools/handlers.d.ts.map +1 -0
  88. package/dist/mcp/tools/handlers.js +760 -0
  89. package/dist/mcp/tools/handlers.js.map +1 -0
  90. package/dist/mcp/tools/index.d.ts +7 -0
  91. package/dist/mcp/tools/index.d.ts.map +1 -0
  92. package/dist/mcp/tools/index.js +6 -0
  93. package/dist/mcp/tools/index.js.map +1 -0
  94. package/dist/mcp/tools/types.d.ts +314 -0
  95. package/dist/mcp/tools/types.d.ts.map +1 -0
  96. package/dist/mcp/tools/types.js +5 -0
  97. package/dist/mcp/tools/types.js.map +1 -0
  98. package/dist/mcp/transports/stdio.d.ts +27 -0
  99. package/dist/mcp/transports/stdio.d.ts.map +1 -0
  100. package/dist/mcp/transports/stdio.js +237 -0
  101. package/dist/mcp/transports/stdio.js.map +1 -0
  102. package/dist/telemetry/index.d.ts +58 -0
  103. package/dist/telemetry/index.d.ts.map +1 -0
  104. package/dist/telemetry/index.js +109 -0
  105. package/dist/telemetry/index.js.map +1 -0
  106. package/dist/telemetry/logger.d.ts +116 -0
  107. package/dist/telemetry/logger.d.ts.map +1 -0
  108. package/dist/telemetry/logger.js +256 -0
  109. package/dist/telemetry/logger.js.map +1 -0
  110. package/dist/telemetry/metrics.d.ts +115 -0
  111. package/dist/telemetry/metrics.d.ts.map +1 -0
  112. package/dist/telemetry/metrics.js +292 -0
  113. package/dist/telemetry/metrics.js.map +1 -0
  114. package/dist/telemetry/tracer.d.ts +227 -0
  115. package/dist/telemetry/tracer.d.ts.map +1 -0
  116. package/dist/telemetry/tracer.js +355 -0
  117. package/dist/telemetry/tracer.js.map +1 -0
  118. package/dist/web/app.d.ts +2 -0
  119. package/dist/web/app.d.ts.map +1 -0
  120. package/dist/web/app.js +115 -0
  121. package/dist/web/app.js.map +1 -0
  122. package/dist/web/components/ContactList.d.ts +3 -0
  123. package/dist/web/components/ContactList.d.ts.map +1 -0
  124. package/dist/web/components/ContactList.js +262 -0
  125. package/dist/web/components/ContactList.js.map +1 -0
  126. package/dist/web/components/Dashboard.d.ts +3 -0
  127. package/dist/web/components/Dashboard.d.ts.map +1 -0
  128. package/dist/web/components/Dashboard.js +158 -0
  129. package/dist/web/components/Dashboard.js.map +1 -0
  130. package/dist/web/components/DealPipeline.d.ts +3 -0
  131. package/dist/web/components/DealPipeline.d.ts.map +1 -0
  132. package/dist/web/components/DealPipeline.js +306 -0
  133. package/dist/web/components/DealPipeline.js.map +1 -0
  134. package/dist/web/index.d.ts +2 -0
  135. package/dist/web/index.d.ts.map +1 -0
  136. package/dist/web/index.js +269 -0
  137. package/dist/web/index.js.map +1 -0
  138. package/dist/web/types.d.ts +75 -0
  139. package/dist/web/types.d.ts.map +1 -0
  140. package/dist/web/types.js +3 -0
  141. package/dist/web/types.js.map +1 -0
  142. package/native/index.d.ts +571 -0
  143. package/native/index.js +687 -0
  144. package/package.json +105 -0
  145. package/src/cli/commands/activities.ts +543 -0
  146. package/src/cli/commands/contacts.ts +563 -0
  147. package/src/cli/commands/deals.ts +637 -0
  148. package/src/cli/commands/media.ts +521 -0
  149. package/src/cli/commands/search.ts +426 -0
  150. package/src/cli/index.ts +203 -0
  151. package/src/cli/repl.ts +379 -0
  152. package/src/cli/utils/config.ts +299 -0
  153. package/src/cli/utils/output.ts +386 -0
  154. package/src/cli/utils/prompt.ts +444 -0
  155. package/src/cli.ts +11 -0
  156. package/src/core/index.ts +184 -0
  157. package/src/core/schemas.ts +770 -0
  158. package/src/core/types.ts +969 -0
  159. package/src/index.ts +8 -0
  160. package/src/mcp/index.ts +17 -0
  161. package/src/mcp/server.ts +26 -0
  162. package/src/mcp/storage/client.ts +408 -0
  163. package/src/mcp/storage/index.ts +7 -0
  164. package/src/mcp/storage/types.ts +72 -0
  165. package/src/mcp/tools/definitions.ts +961 -0
  166. package/src/mcp/tools/handlers.ts +805 -0
  167. package/src/mcp/tools/index.ts +7 -0
  168. package/src/mcp/tools/types.ts +390 -0
  169. package/src/mcp/transports/stdio.ts +225 -0
  170. package/src/telemetry/index.ts +131 -0
  171. package/src/telemetry/logger.ts +318 -0
  172. package/src/telemetry/metrics.ts +393 -0
  173. package/src/telemetry/tracer.ts +487 -0
  174. package/src/web/api/activities.ts +41 -0
  175. package/src/web/api/contacts.ts +114 -0
  176. package/src/web/api/deals.ts +108 -0
  177. package/src/web/api/media.ts +98 -0
  178. package/src/web/app.tsx +143 -0
  179. package/src/web/components/ActivityFeed.tsx +195 -0
  180. package/src/web/components/ContactList.tsx +340 -0
  181. package/src/web/components/Dashboard.tsx +214 -0
  182. package/src/web/components/DealPipeline.tsx +405 -0
  183. package/src/web/components/MediaGallery.tsx +334 -0
  184. package/src/web/index.html +14 -0
  185. package/src/web/index.ts +326 -0
  186. package/src/web/styles/main.css +180 -0
  187. 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
+ }