@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.
- package/README.md +78 -0
- package/index.js +521 -0
- 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
|
+
}
|