@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,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Commands
|
|
3
|
+
*
|
|
4
|
+
* CLI commands for searching across CRM entities.
|
|
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
|
+
formatCurrency,
|
|
18
|
+
formatRelativeTime,
|
|
19
|
+
truncate,
|
|
20
|
+
output,
|
|
21
|
+
} from '../utils/output.js';
|
|
22
|
+
import { Spinner } from '../utils/prompt.js';
|
|
23
|
+
import { CRMStorageClient } from '../../mcp/storage/client.js';
|
|
24
|
+
import type { Contact, Deal, Activity } from '../../core/types.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Search result type
|
|
28
|
+
*/
|
|
29
|
+
interface SearchResult {
|
|
30
|
+
type: 'contact' | 'deal' | 'activity';
|
|
31
|
+
id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
subtitle: string;
|
|
34
|
+
matchField?: string;
|
|
35
|
+
matchValue?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Storage client singleton
|
|
39
|
+
let _storage: CRMStorageClient | null = null;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get storage client (lazy initialization)
|
|
43
|
+
*/
|
|
44
|
+
async function getStorage(): Promise<CRMStorageClient> {
|
|
45
|
+
if (!_storage) {
|
|
46
|
+
_storage = new CRMStorageClient({
|
|
47
|
+
path: process.env.CRM_DB_PATH || './data/crm-cli',
|
|
48
|
+
mapSize: 256 * 1024 * 1024, // 256MB
|
|
49
|
+
maxDbs: 20,
|
|
50
|
+
});
|
|
51
|
+
await _storage.initialize();
|
|
52
|
+
}
|
|
53
|
+
return _storage;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get all contacts
|
|
58
|
+
*/
|
|
59
|
+
async function getContacts(): Promise<Contact[]> {
|
|
60
|
+
const storage = await getStorage();
|
|
61
|
+
return storage.list('contacts');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get all deals
|
|
66
|
+
*/
|
|
67
|
+
async function getDeals(): Promise<Deal[]> {
|
|
68
|
+
const storage = await getStorage();
|
|
69
|
+
return storage.list('deals');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get all activities
|
|
74
|
+
*/
|
|
75
|
+
async function getActivities(): Promise<Activity[]> {
|
|
76
|
+
const storage = await getStorage();
|
|
77
|
+
return storage.list('activities');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Register search commands
|
|
82
|
+
*/
|
|
83
|
+
export function registerSearchCommands(program: Command): void {
|
|
84
|
+
const search = program.command('search').description('Search across all entities');
|
|
85
|
+
|
|
86
|
+
// Global search
|
|
87
|
+
search
|
|
88
|
+
.command('<query>')
|
|
89
|
+
.description('Search contacts, deals, and activities')
|
|
90
|
+
.option('-t, --type <type>', 'Filter by entity type (contact, deal, activity)')
|
|
91
|
+
.option('--json', 'Output as JSON')
|
|
92
|
+
.option('--limit <number>', 'Limit results per type', '10')
|
|
93
|
+
.action(async (query, options) => {
|
|
94
|
+
if (!query || query.length < 2) {
|
|
95
|
+
printError('Search query must be at least 2 characters');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const spinner = new Spinner(`Searching for "${query}"...`).start();
|
|
100
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
101
|
+
|
|
102
|
+
const results: SearchResult[] = [];
|
|
103
|
+
const q = query.toLowerCase();
|
|
104
|
+
const limit = parseInt(options.limit);
|
|
105
|
+
|
|
106
|
+
// Search contacts
|
|
107
|
+
if (!options.type || options.type === 'contact') {
|
|
108
|
+
const contacts = await getContacts();
|
|
109
|
+
for (const contact of contacts) {
|
|
110
|
+
const matches: { field: string; value: string }[] = [];
|
|
111
|
+
|
|
112
|
+
// Check name
|
|
113
|
+
if (contact.name.toLowerCase().includes(q)) {
|
|
114
|
+
matches.push({ field: 'name', value: contact.name });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check emails
|
|
118
|
+
for (const email of contact.emails) {
|
|
119
|
+
if (email.email.toLowerCase().includes(q)) {
|
|
120
|
+
matches.push({ field: 'email', value: email.email });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check company
|
|
125
|
+
if (contact.company?.toLowerCase().includes(q)) {
|
|
126
|
+
matches.push({ field: 'company', value: contact.company });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check tags
|
|
130
|
+
for (const tag of contact.tags) {
|
|
131
|
+
if (tag.toLowerCase().includes(q)) {
|
|
132
|
+
matches.push({ field: 'tag', value: tag });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (matches.length > 0) {
|
|
137
|
+
results.push({
|
|
138
|
+
type: 'contact',
|
|
139
|
+
id: contact.id,
|
|
140
|
+
title: contact.name,
|
|
141
|
+
subtitle: contact.company || contact.emails[0]?.email || 'No details',
|
|
142
|
+
matchField: matches[0].field,
|
|
143
|
+
matchValue: matches[0].value,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (results.filter((r) => r.type === 'contact').length >= limit) break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Search deals
|
|
152
|
+
if (!options.type || options.type === 'deal') {
|
|
153
|
+
const deals = await getDeals();
|
|
154
|
+
for (const deal of deals) {
|
|
155
|
+
const matches: { field: string; value: string }[] = [];
|
|
156
|
+
|
|
157
|
+
// Check title
|
|
158
|
+
if (deal.title.toLowerCase().includes(q)) {
|
|
159
|
+
matches.push({ field: 'title', value: deal.title });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check notes
|
|
163
|
+
if (deal.notes?.toLowerCase().includes(q)) {
|
|
164
|
+
matches.push({ field: 'notes', value: truncate(deal.notes, 50) });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check tags
|
|
168
|
+
for (const tag of deal.tags) {
|
|
169
|
+
if (tag.toLowerCase().includes(q)) {
|
|
170
|
+
matches.push({ field: 'tag', value: tag });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (matches.length > 0) {
|
|
175
|
+
results.push({
|
|
176
|
+
type: 'deal',
|
|
177
|
+
id: deal.id,
|
|
178
|
+
title: deal.title,
|
|
179
|
+
subtitle: `${formatCurrency(deal.value, deal.currency)} - ${deal.stage}`,
|
|
180
|
+
matchField: matches[0].field,
|
|
181
|
+
matchValue: matches[0].value,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (results.filter((r) => r.type === 'deal').length >= limit) break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Search activities
|
|
190
|
+
if (!options.type || options.type === 'activity') {
|
|
191
|
+
const activities = await getActivities();
|
|
192
|
+
for (const activity of activities) {
|
|
193
|
+
const matches: { field: string; value: string }[] = [];
|
|
194
|
+
|
|
195
|
+
// Check title
|
|
196
|
+
if (activity.title.toLowerCase().includes(q)) {
|
|
197
|
+
matches.push({ field: 'title', value: activity.title });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check description
|
|
201
|
+
if (activity.description?.toLowerCase().includes(q)) {
|
|
202
|
+
matches.push({ field: 'description', value: truncate(activity.description, 50) });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (matches.length > 0) {
|
|
206
|
+
results.push({
|
|
207
|
+
type: 'activity',
|
|
208
|
+
id: activity.id,
|
|
209
|
+
title: activity.title,
|
|
210
|
+
subtitle: `${activity.type} - ${formatRelativeTime(activity.timestamp)}`,
|
|
211
|
+
matchField: matches[0].field,
|
|
212
|
+
matchValue: matches[0].value,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (results.filter((r) => r.type === 'activity').length >= limit) break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
spinner.succeed(`Found ${results.length} results`);
|
|
221
|
+
|
|
222
|
+
if (options.json) {
|
|
223
|
+
printJSON(results);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (results.length === 0) {
|
|
228
|
+
printWarning(`No results found for "${query}"`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Group by type
|
|
233
|
+
const byType = results.reduce(
|
|
234
|
+
(acc, r) => {
|
|
235
|
+
if (!acc[r.type]) acc[r.type] = [];
|
|
236
|
+
acc[r.type].push(r);
|
|
237
|
+
return acc;
|
|
238
|
+
},
|
|
239
|
+
{} as Record<string, SearchResult[]>
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Display contacts
|
|
243
|
+
if (byType.contact?.length) {
|
|
244
|
+
console.log();
|
|
245
|
+
console.log(output.bold(`\u{1F464} Contacts (${byType.contact.length})`));
|
|
246
|
+
printDivider('-');
|
|
247
|
+
|
|
248
|
+
printTable(byType.contact, [
|
|
249
|
+
{ key: 'id', header: 'ID', width: 8, format: (v) => truncate(String(v), 8) },
|
|
250
|
+
{ key: 'title', header: 'Name', width: 25 },
|
|
251
|
+
{ key: 'subtitle', header: 'Details', width: 30, format: (v) => truncate(String(v), 30) },
|
|
252
|
+
{
|
|
253
|
+
key: 'matchField',
|
|
254
|
+
header: 'Match',
|
|
255
|
+
format: (v, row) => `${v}: ${(row as SearchResult).matchValue}`,
|
|
256
|
+
},
|
|
257
|
+
]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Display deals
|
|
261
|
+
if (byType.deal?.length) {
|
|
262
|
+
console.log();
|
|
263
|
+
console.log(output.bold(`\u{1F4B0} Deals (${byType.deal.length})`));
|
|
264
|
+
printDivider('-');
|
|
265
|
+
|
|
266
|
+
printTable(byType.deal, [
|
|
267
|
+
{ key: 'id', header: 'ID', width: 8, format: (v) => truncate(String(v), 8) },
|
|
268
|
+
{ key: 'title', header: 'Title', width: 25, format: (v) => truncate(String(v), 25) },
|
|
269
|
+
{ key: 'subtitle', header: 'Value/Stage', width: 30, format: (v) => truncate(String(v), 30) },
|
|
270
|
+
{
|
|
271
|
+
key: 'matchField',
|
|
272
|
+
header: 'Match',
|
|
273
|
+
format: (v, row) => `${v}: ${(row as SearchResult).matchValue}`,
|
|
274
|
+
},
|
|
275
|
+
]);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Display activities
|
|
279
|
+
if (byType.activity?.length) {
|
|
280
|
+
console.log();
|
|
281
|
+
console.log(output.bold(`\u{1F4DD} Activities (${byType.activity.length})`));
|
|
282
|
+
printDivider('-');
|
|
283
|
+
|
|
284
|
+
printTable(byType.activity, [
|
|
285
|
+
{ key: 'id', header: 'ID', width: 8, format: (v) => truncate(String(v), 8) },
|
|
286
|
+
{ key: 'title', header: 'Title', width: 30, format: (v) => truncate(String(v), 30) },
|
|
287
|
+
{ key: 'subtitle', header: 'When', width: 20 },
|
|
288
|
+
{
|
|
289
|
+
key: 'matchField',
|
|
290
|
+
header: 'Match',
|
|
291
|
+
format: (v, row) => `${v}: ${(row as SearchResult).matchValue}`,
|
|
292
|
+
},
|
|
293
|
+
]);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Quick search (alias)
|
|
300
|
+
search
|
|
301
|
+
.command('quick <query>')
|
|
302
|
+
.description('Quick search with minimal output')
|
|
303
|
+
.action(async (query) => {
|
|
304
|
+
const results: SearchResult[] = [];
|
|
305
|
+
const q = query.toLowerCase();
|
|
306
|
+
|
|
307
|
+
// Quick contact search
|
|
308
|
+
const contacts = await getContacts();
|
|
309
|
+
for (const contact of contacts) {
|
|
310
|
+
if (contact.name.toLowerCase().includes(q)) {
|
|
311
|
+
results.push({
|
|
312
|
+
type: 'contact',
|
|
313
|
+
id: contact.id,
|
|
314
|
+
title: contact.name,
|
|
315
|
+
subtitle: contact.emails[0]?.email || '',
|
|
316
|
+
});
|
|
317
|
+
if (results.length >= 5) break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Quick deal search
|
|
322
|
+
const deals = await getDeals();
|
|
323
|
+
for (const deal of deals) {
|
|
324
|
+
if (deal.title.toLowerCase().includes(q)) {
|
|
325
|
+
results.push({
|
|
326
|
+
type: 'deal',
|
|
327
|
+
id: deal.id,
|
|
328
|
+
title: deal.title,
|
|
329
|
+
subtitle: formatCurrency(deal.value, deal.currency),
|
|
330
|
+
});
|
|
331
|
+
if (results.length >= 10) break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (results.length === 0) {
|
|
336
|
+
printWarning('No quick results');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
for (const r of results) {
|
|
341
|
+
const icon = r.type === 'contact' ? '\u{1F464}' : '\u{1F4B0}';
|
|
342
|
+
console.log(`${icon} ${output.bold(r.id.slice(0, 8))} ${r.title} ${output.dim(r.subtitle)}`);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Recent items
|
|
347
|
+
search
|
|
348
|
+
.command('recent')
|
|
349
|
+
.description('Show recent items')
|
|
350
|
+
.option('-t, --type <type>', 'Filter by type (contact, deal, activity)')
|
|
351
|
+
.option('--limit <number>', 'Limit results', '10')
|
|
352
|
+
.action(async (options) => {
|
|
353
|
+
const limit = parseInt(options.limit);
|
|
354
|
+
const results: SearchResult[] = [];
|
|
355
|
+
|
|
356
|
+
// Get recent contacts
|
|
357
|
+
if (!options.type || options.type === 'contact') {
|
|
358
|
+
const contacts = (await getContacts())
|
|
359
|
+
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
|
360
|
+
.slice(0, limit);
|
|
361
|
+
|
|
362
|
+
for (const c of contacts) {
|
|
363
|
+
results.push({
|
|
364
|
+
type: 'contact',
|
|
365
|
+
id: c.id,
|
|
366
|
+
title: c.name,
|
|
367
|
+
subtitle: `Updated ${formatRelativeTime(c.updatedAt)}`,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Get recent deals
|
|
373
|
+
if (!options.type || options.type === 'deal') {
|
|
374
|
+
const deals = (await getDeals())
|
|
375
|
+
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
|
376
|
+
.slice(0, limit);
|
|
377
|
+
|
|
378
|
+
for (const d of deals) {
|
|
379
|
+
results.push({
|
|
380
|
+
type: 'deal',
|
|
381
|
+
id: d.id,
|
|
382
|
+
title: d.title,
|
|
383
|
+
subtitle: `${formatStatus(d.stage)} - Updated ${formatRelativeTime(d.updatedAt)}`,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Get recent activities
|
|
389
|
+
if (!options.type || options.type === 'activity') {
|
|
390
|
+
const activities = (await getActivities())
|
|
391
|
+
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
|
392
|
+
.slice(0, limit);
|
|
393
|
+
|
|
394
|
+
for (const a of activities) {
|
|
395
|
+
results.push({
|
|
396
|
+
type: 'activity',
|
|
397
|
+
id: a.id,
|
|
398
|
+
title: a.title,
|
|
399
|
+
subtitle: `${a.type} - ${formatRelativeTime(a.timestamp)}`,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (results.length === 0) {
|
|
405
|
+
printWarning('No recent items');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
printHeader('Recent Items');
|
|
410
|
+
|
|
411
|
+
for (const r of results) {
|
|
412
|
+
const icons: Record<string, string> = {
|
|
413
|
+
contact: '\u{1F464}',
|
|
414
|
+
deal: '\u{1F4B0}',
|
|
415
|
+
activity: '\u{1F4DD}',
|
|
416
|
+
};
|
|
417
|
+
const icon = icons[r.type];
|
|
418
|
+
console.log(
|
|
419
|
+
`${icon} ${output.dim(r.type.padEnd(8))} ${output.bold(truncate(r.id, 8))} ${r.title}`
|
|
420
|
+
);
|
|
421
|
+
console.log(` ${output.dim(r.subtitle)}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.log();
|
|
425
|
+
});
|
|
426
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CRM CLI Entry Point
|
|
4
|
+
*
|
|
5
|
+
* Terminal-based CLI for CRM operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import {
|
|
10
|
+
printError,
|
|
11
|
+
printHeader,
|
|
12
|
+
output,
|
|
13
|
+
} from './utils/output.js';
|
|
14
|
+
import { loadConfig, applyEnvOverrides, initConfig } from './utils/config.js';
|
|
15
|
+
import { registerContactCommands } from './commands/contacts.js';
|
|
16
|
+
import { registerDealCommands } from './commands/deals.js';
|
|
17
|
+
import { registerActivityCommands } from './commands/activities.js';
|
|
18
|
+
import { registerMediaCommands } from './commands/media.js';
|
|
19
|
+
import { registerSearchCommands } from './commands/search.js';
|
|
20
|
+
import { registerREPLCommand } from './repl.js';
|
|
21
|
+
|
|
22
|
+
// Package version (would be read from package.json in production)
|
|
23
|
+
const VERSION = '1.0.0';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create and configure the CLI program
|
|
27
|
+
*/
|
|
28
|
+
function createProgram(): Command {
|
|
29
|
+
const program = new Command();
|
|
30
|
+
|
|
31
|
+
// Program metadata
|
|
32
|
+
program
|
|
33
|
+
.name('crm')
|
|
34
|
+
.description('Terminal-based CRM CLI')
|
|
35
|
+
.version(VERSION, '-v, --version', 'Show version')
|
|
36
|
+
.helpOption('-h, --help', 'Show help');
|
|
37
|
+
|
|
38
|
+
// Global options
|
|
39
|
+
program
|
|
40
|
+
.option('--json', 'Output as JSON')
|
|
41
|
+
.option('--no-color', 'Disable colored output')
|
|
42
|
+
.option('--api <url>', 'API endpoint URL');
|
|
43
|
+
|
|
44
|
+
// Register command modules
|
|
45
|
+
registerContactCommands(program);
|
|
46
|
+
registerDealCommands(program);
|
|
47
|
+
registerActivityCommands(program);
|
|
48
|
+
registerMediaCommands(program);
|
|
49
|
+
registerSearchCommands(program);
|
|
50
|
+
registerREPLCommand(program);
|
|
51
|
+
|
|
52
|
+
// Config command
|
|
53
|
+
program
|
|
54
|
+
.command('config')
|
|
55
|
+
.description('Manage CLI configuration')
|
|
56
|
+
.argument('[action]', 'Action (show, init, location)')
|
|
57
|
+
.action(async (action = 'show') => {
|
|
58
|
+
const { displayConfig, initConfig, getConfigLocation } = await import(
|
|
59
|
+
'./utils/config.js'
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
switch (action) {
|
|
63
|
+
case 'init':
|
|
64
|
+
await initConfig();
|
|
65
|
+
break;
|
|
66
|
+
case 'location':
|
|
67
|
+
console.log(`\nConfig file: ${getConfigLocation()}\n`);
|
|
68
|
+
break;
|
|
69
|
+
case 'show':
|
|
70
|
+
default:
|
|
71
|
+
displayConfig();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Init command (alias)
|
|
76
|
+
program
|
|
77
|
+
.command('init')
|
|
78
|
+
.description('Initialize CLI configuration')
|
|
79
|
+
.action(async () => {
|
|
80
|
+
const { initConfig } = await import('./utils/config.js');
|
|
81
|
+
await initConfig();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Status command
|
|
85
|
+
program
|
|
86
|
+
.command('status')
|
|
87
|
+
.description('Show CRM status overview')
|
|
88
|
+
.option('--json', 'Output as JSON')
|
|
89
|
+
.action(async (options) => {
|
|
90
|
+
const { printTable, printJSON, formatCurrency } = await import(
|
|
91
|
+
'./utils/output.js'
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Mock data (would come from API)
|
|
95
|
+
const status = {
|
|
96
|
+
contacts: { total: 156, leads: 45, customers: 89 },
|
|
97
|
+
deals: {
|
|
98
|
+
total: 34,
|
|
99
|
+
pipeline: 245000,
|
|
100
|
+
won: 89000,
|
|
101
|
+
weighted: 156000,
|
|
102
|
+
},
|
|
103
|
+
activities: { today: 12, thisWeek: 67 },
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (options.json) {
|
|
107
|
+
printJSON(status);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
printHeader('CRM Status');
|
|
112
|
+
|
|
113
|
+
console.log(output.bold('\u{1F464} Contacts'));
|
|
114
|
+
console.log(` Total: ${status.contacts.total}`);
|
|
115
|
+
console.log(` Leads: ${status.contacts.leads}`);
|
|
116
|
+
console.log(` Customers: ${status.contacts.customers}`);
|
|
117
|
+
console.log();
|
|
118
|
+
|
|
119
|
+
console.log(output.bold('\u{1F4B0} Deals'));
|
|
120
|
+
console.log(` Total: ${status.deals.total}`);
|
|
121
|
+
console.log(` Pipeline Value: ${formatCurrency(status.deals.pipeline)}`);
|
|
122
|
+
console.log(` Won This Month: ${formatCurrency(status.deals.won)}`);
|
|
123
|
+
console.log(` Weighted Pipeline: ${formatCurrency(status.deals.weighted)}`);
|
|
124
|
+
console.log();
|
|
125
|
+
|
|
126
|
+
console.log(output.bold('\u{1F4DD} Activities'));
|
|
127
|
+
console.log(` Today: ${status.activities.today}`);
|
|
128
|
+
console.log(` This Week: ${status.activities.thisWeek}`);
|
|
129
|
+
console.log();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Quick commands
|
|
133
|
+
program
|
|
134
|
+
.command('ls')
|
|
135
|
+
.description('List contacts (alias for contacts list)')
|
|
136
|
+
.allowUnknownOption(true)
|
|
137
|
+
.action(async () => {
|
|
138
|
+
const args = process.argv.slice(process.argv.indexOf('ls') + 1);
|
|
139
|
+
await program.parseAsync(['contacts', 'list', ...args], { from: 'user' });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
program
|
|
143
|
+
.command('new')
|
|
144
|
+
.description('Create new entity')
|
|
145
|
+
.argument('<type>', 'Entity type (contact, deal, activity)')
|
|
146
|
+
.action(async (type) => {
|
|
147
|
+
switch (type) {
|
|
148
|
+
case 'contact':
|
|
149
|
+
await program.parseAsync(['contacts', 'create'], { from: 'user' });
|
|
150
|
+
break;
|
|
151
|
+
case 'deal':
|
|
152
|
+
await program.parseAsync(['deals', 'create'], { from: 'user' });
|
|
153
|
+
break;
|
|
154
|
+
case 'activity':
|
|
155
|
+
await program.parseAsync(['activities', 'create'], { from: 'user' });
|
|
156
|
+
break;
|
|
157
|
+
default:
|
|
158
|
+
printError(`Unknown entity type: ${type}`);
|
|
159
|
+
console.log('Valid types: contact, deal, activity');
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return program;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Main entry point
|
|
168
|
+
*/
|
|
169
|
+
async function main(): Promise<void> {
|
|
170
|
+
// Load and apply configuration
|
|
171
|
+
let config = loadConfig();
|
|
172
|
+
config = applyEnvOverrides(config);
|
|
173
|
+
|
|
174
|
+
// Create program
|
|
175
|
+
const program = createProgram();
|
|
176
|
+
|
|
177
|
+
// Handle unknown commands
|
|
178
|
+
program.on('command:*', (operands) => {
|
|
179
|
+
printError(`Unknown command: ${operands[0]}`);
|
|
180
|
+
console.log(`Run ${output.info('crm --help')} for available commands.`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Parse arguments
|
|
185
|
+
try {
|
|
186
|
+
await program.parseAsync(process.argv);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error instanceof Error) {
|
|
189
|
+
printError(error.message);
|
|
190
|
+
} else {
|
|
191
|
+
printError('An unexpected error occurred');
|
|
192
|
+
}
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Run main
|
|
198
|
+
main().catch((error) => {
|
|
199
|
+
console.error('Fatal error:', error);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
export { createProgram };
|