@startanaicompany/crm 1.0.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 (3) hide show
  1. package/README.md +78 -0
  2. package/index.js +521 -0
  3. package/package.json +21 -0
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @startanaicompany/crm
2
+
3
+ AI-first CRM CLI (`saac_crm`) — manage leads, API keys, and user accounts from the terminal.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @startanaicompany/crm
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ ```bash
14
+ # Set API URL and default key
15
+ saac_crm config set --url https://yourapp.example.com --api-key crm_xxxx
16
+
17
+ # Auth priority: --api-key flag > SAAC_CRM_API_KEY env > config file
18
+ export SAAC_CRM_API_KEY=crm_xxxx
19
+ ```
20
+
21
+ ## Commands
22
+
23
+ ### API Keys
24
+
25
+ ```bash
26
+ # Create an agent key (no auth required)
27
+ saac_crm keys create --name "my-agent"
28
+
29
+ # Create an admin scope key (requires deployment password)
30
+ saac_crm keys create --name "admin-agent" --scope admin --admin-password <password>
31
+
32
+ # List all keys
33
+ saac_crm keys list
34
+
35
+ # Show current key info
36
+ saac_crm keys self
37
+
38
+ # Revoke a key
39
+ saac_crm keys revoke <id>
40
+ ```
41
+
42
+ ### Leads
43
+
44
+ ```bash
45
+ # Create a lead
46
+ saac_crm leads create --name "Jane Smith" --email "jane@example.com" --company "Acme"
47
+
48
+ # List leads (with filters)
49
+ saac_crm leads list --status new --tag vip --tag enterprise
50
+
51
+ # Get single lead
52
+ saac_crm leads get <id>
53
+
54
+ # Update lead
55
+ saac_crm leads update <id> --status contacted --notes "Called, interested"
56
+
57
+ # Delete lead (soft)
58
+ saac_crm leads delete <id>
59
+
60
+ # Status history
61
+ saac_crm leads history <id>
62
+ ```
63
+
64
+ ### Users (requires admin scope key)
65
+
66
+ ```bash
67
+ # Create a human user account
68
+ saac_crm users register --email admin@example.com --name "Admin User" --role admin
69
+
70
+ # List users
71
+ saac_crm users list
72
+
73
+ # Update user
74
+ saac_crm users update <id> --role viewer
75
+
76
+ # Deactivate user
77
+ saac_crm users deactivate <id>
78
+ ```
package/index.js ADDED
@@ -0,0 +1,521 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { Command } = require('commander');
5
+ const axios = require('axios');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+ const readline = require('readline');
10
+
11
+ // ============================================================
12
+ // CONFIG — ~/.saac_crm/config.json
13
+ // ============================================================
14
+
15
+ const CONFIG_DIR = path.join(os.homedir(), '.saac_crm');
16
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
17
+
18
+ function loadConfig() {
19
+ try {
20
+ if (fs.existsSync(CONFIG_FILE)) {
21
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
22
+ }
23
+ } catch (_) {}
24
+ return {};
25
+ }
26
+
27
+ function saveConfig(config) {
28
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
29
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
30
+ }
31
+
32
+ // Priority: --api-key flag > SAAC_CRM_API_KEY env > config file
33
+ function resolveApiKey(cliApiKey) {
34
+ if (cliApiKey) return cliApiKey;
35
+ if (process.env.SAAC_CRM_API_KEY) return process.env.SAAC_CRM_API_KEY;
36
+ const cfg = loadConfig();
37
+ return cfg.apiKey || null;
38
+ }
39
+
40
+ function resolveApiUrl(cliUrl) {
41
+ if (cliUrl) return cliUrl;
42
+ const cfg = loadConfig();
43
+ return cfg.apiUrl || null;
44
+ }
45
+
46
+ function getClient(options = {}) {
47
+ const apiKey = resolveApiKey(options.apiKey);
48
+ const apiUrl = resolveApiUrl(options.url);
49
+
50
+ if (!apiUrl) {
51
+ console.error('Error: API URL not configured. Run: saac_crm config set --url <api-url>');
52
+ process.exit(1);
53
+ }
54
+ if (!apiKey) {
55
+ console.error('Error: API key not set. Use --api-key, set SAAC_CRM_API_KEY env, or run: saac_crm config set --api-key <key>');
56
+ process.exit(1);
57
+ }
58
+
59
+ return axios.create({
60
+ baseURL: `${apiUrl.replace(/\/$/, '')}/api/v1`,
61
+ headers: {
62
+ 'Authorization': `Bearer ${apiKey}`,
63
+ 'Content-Type': 'application/json'
64
+ }
65
+ });
66
+ }
67
+
68
+ function printJSON(data) {
69
+ console.log(JSON.stringify(data, null, 2));
70
+ }
71
+
72
+ function handleError(err) {
73
+ if (err.response) {
74
+ const e = err.response.data?.error || {};
75
+ console.error(`Error ${err.response.status}: [${e.code || 'ERROR'}] ${e.message || err.response.statusText}`);
76
+ if (e.field) console.error(`Field: ${e.field}`);
77
+ } else {
78
+ console.error('Error:', err.message);
79
+ }
80
+ process.exit(1);
81
+ }
82
+
83
+ function promptSecret(question) {
84
+ return new Promise((resolve) => {
85
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
86
+ rl.question(question, (answer) => {
87
+ rl.close();
88
+ resolve(answer);
89
+ });
90
+ });
91
+ }
92
+
93
+ // ============================================================
94
+ // CLI PROGRAM
95
+ // ============================================================
96
+
97
+ const program = new Command();
98
+
99
+ program
100
+ .name('saac_crm')
101
+ .description('AI-first CRM CLI — manage leads and API keys')
102
+ .version('1.0.0')
103
+ .option('--api-key <key>', 'API key (overrides SAAC_CRM_API_KEY env and config)')
104
+ .option('--url <url>', 'API base URL (overrides config)');
105
+
106
+ // ============================================================
107
+ // CONFIG COMMANDS
108
+ // ============================================================
109
+
110
+ const configCmd = program.command('config').description('Manage CLI configuration');
111
+
112
+ configCmd
113
+ .command('set')
114
+ .description('Set configuration values')
115
+ .option('--url <url>', 'CRM API base URL (e.g. https://yourapp.example.com)')
116
+ .option('--api-key <key>', 'Default API key to use')
117
+ .action((opts) => {
118
+ const cfg = loadConfig();
119
+ if (opts.url) cfg.apiUrl = opts.url;
120
+ if (opts.apiKey) cfg.apiKey = opts.apiKey;
121
+ saveConfig(cfg);
122
+ console.log('Configuration saved to', CONFIG_FILE);
123
+ printJSON(cfg);
124
+ });
125
+
126
+ configCmd
127
+ .command('get')
128
+ .description('Show current configuration')
129
+ .action(() => {
130
+ const cfg = loadConfig();
131
+ // Mask the api key for display
132
+ if (cfg.apiKey) cfg.apiKey = cfg.apiKey.substring(0, 12) + '...';
133
+ printJSON(cfg);
134
+ });
135
+
136
+ // ============================================================
137
+ // KEYS COMMANDS
138
+ // ============================================================
139
+
140
+ const keysCmd = program.command('keys').description('Manage API keys');
141
+
142
+ keysCmd
143
+ .command('create')
144
+ .description('Create a new API key')
145
+ .requiredOption('--name <name>', 'Name/label for the key')
146
+ .option('--scope <scope>', 'Key scope: agent (default) or admin', 'agent')
147
+ .option('--workspace <slug>', 'Workspace slug (required for scope=admin, e.g. goldenrecruit101)')
148
+ .option('--admin-password <password>', 'Admin password (required for scope=admin)')
149
+ .action(async (opts) => {
150
+ const globalOpts = program.opts();
151
+ const apiUrl = resolveApiUrl(globalOpts.url);
152
+ if (!apiUrl) {
153
+ console.error('Error: API URL not configured. Run: saac_crm config set --url <api-url>');
154
+ process.exit(1);
155
+ }
156
+
157
+ const body = { name: opts.name, scope: opts.scope };
158
+
159
+ if (opts.scope === 'admin') {
160
+ if (!opts.workspace) {
161
+ console.error('Error: --workspace <slug> is required for scope=admin');
162
+ process.exit(1);
163
+ }
164
+ body.workspace = opts.workspace;
165
+
166
+ let adminPw = opts.adminPassword;
167
+ if (!adminPw) {
168
+ adminPw = await promptSecret('Admin password: ');
169
+ }
170
+ body.admin_password = adminPw;
171
+ }
172
+
173
+ try {
174
+ // key creation doesn't require auth
175
+ const res = await axios.post(
176
+ `${apiUrl.replace(/\/$/, '')}/api/v1/keys`,
177
+ body,
178
+ { headers: { 'Content-Type': 'application/json' } }
179
+ );
180
+ console.log('API key created successfully. Store the key — it will not be shown again.');
181
+ printJSON(res.data.data);
182
+ } catch (err) {
183
+ handleError(err);
184
+ }
185
+ });
186
+
187
+ // ============================================================
188
+ // REGISTER — shortcut for first admin setup
189
+ // ============================================================
190
+
191
+ program
192
+ .command('register')
193
+ .description('Register this workspace and create an admin scope API key')
194
+ .requiredOption('--workspace <slug>', 'Workspace slug (3-30 chars, a-z0-9 only, e.g. goldenrecruit101)')
195
+ .requiredOption('--name <name>', 'Name/label for this admin key')
196
+ .option('--scope <scope>', 'Key scope (must be admin)', 'admin')
197
+ .option('--admin-password <password>', 'Deployment admin password (will prompt if not provided)')
198
+ .action(async (opts) => {
199
+ const globalOpts = program.opts();
200
+ const apiUrl = resolveApiUrl(globalOpts.url);
201
+ if (!apiUrl) {
202
+ console.error('Error: API URL not configured. Run: saac_crm config set --url <api-url>');
203
+ process.exit(1);
204
+ }
205
+
206
+ let adminPw = opts.adminPassword;
207
+ if (!adminPw) {
208
+ adminPw = await promptSecret('Deployment admin password: ');
209
+ }
210
+
211
+ const body = {
212
+ name: opts.name,
213
+ scope: 'admin',
214
+ workspace: opts.workspace,
215
+ admin_password: adminPw
216
+ };
217
+
218
+ try {
219
+ const res = await axios.post(
220
+ `${apiUrl.replace(/\/$/, '')}/api/v1/keys`,
221
+ body,
222
+ { headers: { 'Content-Type': 'application/json' } }
223
+ );
224
+ console.log(`Workspace '${opts.workspace}' registered. Admin key created — store it securely.`);
225
+ printJSON(res.data.data);
226
+ } catch (err) {
227
+ handleError(err);
228
+ }
229
+ });
230
+
231
+ keysCmd
232
+ .command('list')
233
+ .description('List all API keys (metadata only)')
234
+ .action(async () => {
235
+ const globalOpts = program.opts();
236
+ const client = getClient(globalOpts);
237
+ try {
238
+ const res = await client.get('/keys');
239
+ printJSON(res.data);
240
+ } catch (err) {
241
+ handleError(err);
242
+ }
243
+ });
244
+
245
+ keysCmd
246
+ .command('self')
247
+ .description('Show current key metadata')
248
+ .action(async () => {
249
+ const globalOpts = program.opts();
250
+ const client = getClient(globalOpts);
251
+ try {
252
+ const res = await client.get('/keys/self');
253
+ printJSON(res.data.data);
254
+ } catch (err) {
255
+ handleError(err);
256
+ }
257
+ });
258
+
259
+ keysCmd
260
+ .command('revoke <id>')
261
+ .description('Revoke an API key by ID')
262
+ .action(async (id) => {
263
+ const globalOpts = program.opts();
264
+ const client = getClient(globalOpts);
265
+ try {
266
+ const res = await client.delete(`/keys/${id}`);
267
+ console.log('Key revoked.');
268
+ printJSON(res.data.data);
269
+ } catch (err) {
270
+ handleError(err);
271
+ }
272
+ });
273
+
274
+ // ============================================================
275
+ // LEADS COMMANDS
276
+ // ============================================================
277
+
278
+ const leadsCmd = program.command('leads').description('Manage leads');
279
+
280
+ leadsCmd
281
+ .command('create')
282
+ .description('Create a new lead')
283
+ .requiredOption('--name <name>', 'Lead full name')
284
+ .requiredOption('--email <email>', 'Lead email address')
285
+ .option('--phone <phone>', 'Phone number')
286
+ .option('--company <company>', 'Company name')
287
+ .option('--status <status>', 'Status: new|contacted|qualified|unresponsive|converted|lost', 'new')
288
+ .option('--source <source>', 'Source: api|import|referral', 'api')
289
+ .option('--notes <notes>', 'Notes')
290
+ .option('--assigned-to <assignedTo>', 'Assigned to')
291
+ .option('--tag <tag>', 'Tag (repeatable)', (v, prev) => prev.concat([v]), [])
292
+ .option('--external-id <externalId>', 'External ID')
293
+ .option('--idempotency-key <key>', 'Idempotency key for deduplication')
294
+ .action(async (opts) => {
295
+ const globalOpts = program.opts();
296
+ const client = getClient(globalOpts);
297
+ const headers = {};
298
+ if (opts.idempotencyKey) headers['Idempotency-Key'] = opts.idempotencyKey;
299
+ const body = {
300
+ name: opts.name,
301
+ email: opts.email,
302
+ ...(opts.phone && { phone: opts.phone }),
303
+ ...(opts.company && { company: opts.company }),
304
+ status: opts.status,
305
+ source: opts.source,
306
+ ...(opts.notes && { notes: opts.notes }),
307
+ ...(opts.assignedTo && { assigned_to: opts.assignedTo }),
308
+ ...(opts.tag.length > 0 && { tags: opts.tag }),
309
+ ...(opts.externalId && { external_id: opts.externalId })
310
+ };
311
+ try {
312
+ const res = await client.post('/leads', body, { headers });
313
+ printJSON(res.data.data);
314
+ } catch (err) {
315
+ handleError(err);
316
+ }
317
+ });
318
+
319
+ leadsCmd
320
+ .command('list')
321
+ .description('List leads with optional filters')
322
+ .option('--status <status>', 'Filter by status')
323
+ .option('--email <email>', 'Filter by email (partial match)')
324
+ .option('--company <company>', 'Filter by company (partial match)')
325
+ .option('--tag <tag>', 'Filter by tag — repeatable, AND logic', (v, prev) => prev.concat([v]), [])
326
+ .option('--page <n>', 'Page number', '1')
327
+ .option('--per-page <n>', 'Results per page', '20')
328
+ .action(async (opts) => {
329
+ const globalOpts = program.opts();
330
+ const client = getClient(globalOpts);
331
+ const params = {
332
+ ...(opts.status && { status: opts.status }),
333
+ ...(opts.email && { email: opts.email }),
334
+ ...(opts.company && { company: opts.company }),
335
+ page: opts.page,
336
+ per_page: opts.perPage
337
+ };
338
+ // Append tag as repeated query params
339
+ const tagParams = opts.tag.length > 0 ? opts.tag.map(t => `tag=${encodeURIComponent(t)}`).join('&') : '';
340
+ try {
341
+ const baseUrl = `/leads?${new URLSearchParams(params).toString()}${tagParams ? '&' + tagParams : ''}`;
342
+ const res = await client.get(baseUrl);
343
+ printJSON(res.data);
344
+ } catch (err) {
345
+ handleError(err);
346
+ }
347
+ });
348
+
349
+ leadsCmd
350
+ .command('get <id>')
351
+ .description('Get a single lead by ID')
352
+ .action(async (id) => {
353
+ const globalOpts = program.opts();
354
+ const client = getClient(globalOpts);
355
+ try {
356
+ const res = await client.get(`/leads/${id}`);
357
+ printJSON(res.data.data);
358
+ } catch (err) {
359
+ handleError(err);
360
+ }
361
+ });
362
+
363
+ leadsCmd
364
+ .command('update <id>')
365
+ .description('Update a lead by ID')
366
+ .option('--name <name>', 'New name')
367
+ .option('--email <email>', 'New email')
368
+ .option('--phone <phone>', 'New phone')
369
+ .option('--company <company>', 'New company')
370
+ .option('--status <status>', 'New status')
371
+ .option('--source <source>', 'New source')
372
+ .option('--notes <notes>', 'New notes')
373
+ .option('--assigned-to <assignedTo>', 'New assigned-to')
374
+ .option('--tag <tag>', 'Replace tags (repeatable)', (v, prev) => prev.concat([v]), [])
375
+ .option('--external-id <externalId>', 'New external ID')
376
+ .option('--version <version>', 'Optimistic lock version')
377
+ .action(async (id, opts) => {
378
+ const globalOpts = program.opts();
379
+ const client = getClient(globalOpts);
380
+ const body = {
381
+ ...(opts.name && { name: opts.name }),
382
+ ...(opts.email && { email: opts.email }),
383
+ ...(opts.phone !== undefined && { phone: opts.phone }),
384
+ ...(opts.company !== undefined && { company: opts.company }),
385
+ ...(opts.status && { status: opts.status }),
386
+ ...(opts.source && { source: opts.source }),
387
+ ...(opts.notes !== undefined && { notes: opts.notes }),
388
+ ...(opts.assignedTo !== undefined && { assigned_to: opts.assignedTo }),
389
+ ...(opts.tag.length > 0 && { tags: opts.tag }),
390
+ ...(opts.externalId !== undefined && { external_id: opts.externalId }),
391
+ ...(opts.version !== undefined && { version: parseInt(opts.version) })
392
+ };
393
+ try {
394
+ const res = await client.put(`/leads/${id}`, body);
395
+ printJSON(res.data.data);
396
+ } catch (err) {
397
+ handleError(err);
398
+ }
399
+ });
400
+
401
+ leadsCmd
402
+ .command('delete <id>')
403
+ .description('Soft-delete a lead by ID')
404
+ .action(async (id) => {
405
+ const globalOpts = program.opts();
406
+ const client = getClient(globalOpts);
407
+ try {
408
+ const res = await client.delete(`/leads/${id}`);
409
+ console.log('Lead deleted.');
410
+ printJSON(res.data.data);
411
+ } catch (err) {
412
+ handleError(err);
413
+ }
414
+ });
415
+
416
+ leadsCmd
417
+ .command('history <id>')
418
+ .description('Show status change history for a lead')
419
+ .action(async (id) => {
420
+ const globalOpts = program.opts();
421
+ const client = getClient(globalOpts);
422
+ try {
423
+ const res = await client.get(`/leads/${id}/history`);
424
+ printJSON(res.data);
425
+ } catch (err) {
426
+ handleError(err);
427
+ }
428
+ });
429
+
430
+ // ============================================================
431
+ // USERS COMMANDS (requires admin scope key)
432
+ // ============================================================
433
+
434
+ const usersCmd = program.command('users').description('Manage human user accounts (requires admin scope key)');
435
+
436
+ usersCmd
437
+ .command('register')
438
+ .description('Create a new human user account')
439
+ .requiredOption('--email <email>', 'User email address')
440
+ .requiredOption('--name <name>', 'User full name')
441
+ .option('--password <password>', 'User password (will prompt if not provided)')
442
+ .option('--role <role>', 'Role: admin or viewer', 'viewer')
443
+ .action(async (opts) => {
444
+ const globalOpts = program.opts();
445
+ const client = getClient(globalOpts);
446
+
447
+ let password = opts.password;
448
+ if (!password) {
449
+ password = await promptSecret('Password for new user: ');
450
+ }
451
+
452
+ try {
453
+ const res = await client.post('/users', {
454
+ email: opts.email,
455
+ name: opts.name,
456
+ password,
457
+ role: opts.role
458
+ });
459
+ console.log('User created successfully.');
460
+ printJSON(res.data.data);
461
+ } catch (err) {
462
+ handleError(err);
463
+ }
464
+ });
465
+
466
+ usersCmd
467
+ .command('list')
468
+ .description('List all user accounts')
469
+ .action(async () => {
470
+ const globalOpts = program.opts();
471
+ const client = getClient(globalOpts);
472
+ try {
473
+ const res = await client.get('/users');
474
+ printJSON(res.data);
475
+ } catch (err) {
476
+ handleError(err);
477
+ }
478
+ });
479
+
480
+ usersCmd
481
+ .command('update <id>')
482
+ .description('Update a user account')
483
+ .option('--name <name>', 'New name')
484
+ .option('--role <role>', 'New role: admin or viewer')
485
+ .option('--password <password>', 'New password')
486
+ .option('--activate', 'Reactivate a deactivated user')
487
+ .option('--deactivate', 'Deactivate the user')
488
+ .action(async (id, opts) => {
489
+ const globalOpts = program.opts();
490
+ const client = getClient(globalOpts);
491
+ const body = {
492
+ ...(opts.name && { name: opts.name }),
493
+ ...(opts.role && { role: opts.role }),
494
+ ...(opts.password && { password: opts.password }),
495
+ ...(opts.activate && { is_active: true }),
496
+ ...(opts.deactivate && { is_active: false })
497
+ };
498
+ try {
499
+ const res = await client.put(`/users/${id}`, body);
500
+ printJSON(res.data.data);
501
+ } catch (err) {
502
+ handleError(err);
503
+ }
504
+ });
505
+
506
+ usersCmd
507
+ .command('deactivate <id>')
508
+ .description('Deactivate (soft-delete) a user account')
509
+ .action(async (id) => {
510
+ const globalOpts = program.opts();
511
+ const client = getClient(globalOpts);
512
+ try {
513
+ const res = await client.delete(`/users/${id}`);
514
+ console.log('User deactivated.');
515
+ printJSON(res.data.data);
516
+ } catch (err) {
517
+ handleError(err);
518
+ }
519
+ });
520
+
521
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@startanaicompany/crm",
3
+ "version": "1.0.0",
4
+ "description": "AI-first CRM CLI — manage leads and API keys from the terminal",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "saac_crm": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"No tests yet\""
11
+ },
12
+ "keywords": ["crm", "ai", "leads", "api"],
13
+ "license": "MIT",
14
+ "dependencies": {
15
+ "axios": "^1.6.0",
16
+ "commander": "^11.1.0"
17
+ },
18
+ "engines": {
19
+ "node": ">=18"
20
+ }
21
+ }