@startanaicompany/crm 1.0.0 → 1.0.2
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 +90 -13
- package/index.js +104 -34
- package/package.json +15 -2
package/README.md
CHANGED
|
@@ -8,34 +8,70 @@ AI-first CRM CLI (`saac_crm`) — manage leads, API keys, and user accounts from
|
|
|
8
8
|
npm install -g @startanaicompany/crm
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 🤖 AI Agent Quick Start (zero-auth, no human needed)
|
|
14
|
+
|
|
15
|
+
Agents bootstrap themselves with a single command — **no credentials required**:
|
|
12
16
|
|
|
13
17
|
```bash
|
|
14
|
-
#
|
|
15
|
-
saac_crm config set --url https://
|
|
18
|
+
# Step 1: create your own API key (no auth needed)
|
|
19
|
+
saac_crm config set --url https://your-crm.example.com
|
|
20
|
+
saac_crm keys create --name my-agent
|
|
21
|
+
# → returns crm_xxxxxxxxxxxx — store this as your API key
|
|
22
|
+
|
|
23
|
+
# Step 2: set the key and start managing leads
|
|
24
|
+
export SAAC_CRM_API_KEY=crm_xxxxxxxxxxxx
|
|
25
|
+
saac_crm leads create --name "Jane Smith" --email "jane@example.com"
|
|
26
|
+
saac_crm leads list
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> **Auth priority**: `--api-key` flag > `SAAC_CRM_API_KEY` env var > config file (`~/.saac_crm/config.json`)
|
|
30
|
+
|
|
31
|
+
> ⚠️ **Agents: never call `saac_crm register`** — that command is for one-time operator setup only (requires deployment password). Your entry point is always `keys create --name`.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 👤 Operator Setup (one-time, human)
|
|
36
|
+
|
|
37
|
+
This is done once per deployment — **not by agents**:
|
|
16
38
|
|
|
17
|
-
|
|
18
|
-
|
|
39
|
+
```bash
|
|
40
|
+
# 1. Point at your deployment
|
|
41
|
+
saac_crm config set --url https://your-crm.example.com
|
|
42
|
+
|
|
43
|
+
# 2. Register workspace + create admin key (prompts for workspace slug and password)
|
|
44
|
+
saac_crm register --name "main-admin-key"
|
|
45
|
+
# → prompts: Workspace slug (e.g. mycompany):
|
|
46
|
+
# → prompts: Deployment admin password:
|
|
47
|
+
# → returns crm_xxx admin key — store securely
|
|
48
|
+
|
|
49
|
+
# 3. Save admin key and create first human user account
|
|
50
|
+
saac_crm config set --api-key crm_xxx_admin_key
|
|
51
|
+
saac_crm users create --email admin@example.com --name "Admin" --role admin
|
|
19
52
|
```
|
|
20
53
|
|
|
54
|
+
---
|
|
55
|
+
|
|
21
56
|
## Commands
|
|
22
57
|
|
|
23
58
|
### API Keys
|
|
24
59
|
|
|
25
60
|
```bash
|
|
26
|
-
# Create
|
|
61
|
+
# Create agent key — ZERO AUTH, no prior setup needed
|
|
27
62
|
saac_crm keys create --name "my-agent"
|
|
28
63
|
|
|
29
|
-
# Create
|
|
30
|
-
saac_crm keys create --name "admin-
|
|
64
|
+
# Create admin scope key (operator only — requires deployment password + workspace slug)
|
|
65
|
+
saac_crm keys create --name "admin-key" --scope admin \
|
|
66
|
+
--workspace mycompany --admin-password <deployment-password>
|
|
31
67
|
|
|
32
|
-
# List all keys
|
|
68
|
+
# List all keys (metadata only — full key never shown again)
|
|
33
69
|
saac_crm keys list
|
|
34
70
|
|
|
35
71
|
# Show current key info
|
|
36
72
|
saac_crm keys self
|
|
37
73
|
|
|
38
|
-
# Revoke a key
|
|
74
|
+
# Revoke a key by ID
|
|
39
75
|
saac_crm keys revoke <id>
|
|
40
76
|
```
|
|
41
77
|
|
|
@@ -54,17 +90,18 @@ saac_crm leads get <id>
|
|
|
54
90
|
# Update lead
|
|
55
91
|
saac_crm leads update <id> --status contacted --notes "Called, interested"
|
|
56
92
|
|
|
57
|
-
#
|
|
93
|
+
# Soft-delete lead
|
|
58
94
|
saac_crm leads delete <id>
|
|
59
95
|
|
|
60
|
-
# Status history
|
|
96
|
+
# Status change history
|
|
61
97
|
saac_crm leads history <id>
|
|
62
98
|
```
|
|
63
99
|
|
|
64
100
|
### Users (requires admin scope key)
|
|
65
101
|
|
|
66
102
|
```bash
|
|
67
|
-
# Create a human user account
|
|
103
|
+
# Create a human user account (both commands are equivalent)
|
|
104
|
+
saac_crm users create --email admin@example.com --name "Admin User" --role admin
|
|
68
105
|
saac_crm users register --email admin@example.com --name "Admin User" --role admin
|
|
69
106
|
|
|
70
107
|
# List users
|
|
@@ -76,3 +113,43 @@ saac_crm users update <id> --role viewer
|
|
|
76
113
|
# Deactivate user
|
|
77
114
|
saac_crm users deactivate <id>
|
|
78
115
|
```
|
|
116
|
+
|
|
117
|
+
### Human Login
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Log in as admin/viewer — saves JWT to config for dashboard access
|
|
121
|
+
saac_crm login --workspace mycompany --email admin@example.com
|
|
122
|
+
# → prompts for password, saves JWT to ~/.saac_crm/config.json
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Workspace Registration (operator only)
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# First-time setup — registers workspace slug + creates admin key
|
|
129
|
+
# Prompts for --workspace and --admin-password if not provided
|
|
130
|
+
saac_crm register --name "main-admin-key"
|
|
131
|
+
saac_crm register --name "main-admin-key" --workspace mycompany --admin-password <pw>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Configuration
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
saac_crm config set --url https://your-crm.example.com
|
|
138
|
+
saac_crm config set --api-key crm_xxxxxxxxxxxx
|
|
139
|
+
saac_crm config get
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Global Options
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
--api-key <key> Override API key for this command
|
|
146
|
+
--url <url> Override API base URL for this command
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Architecture
|
|
150
|
+
|
|
151
|
+
This is a **single-tenant** system: one workspace slug per deployment, registered once by an operator.
|
|
152
|
+
|
|
153
|
+
- **Agents** → self-service `keys create` → manage leads
|
|
154
|
+
- **Operators** → `register` once → `users create` → human accounts
|
|
155
|
+
- **Humans** → `login` → web dashboard at your deployment URL
|
package/index.js
CHANGED
|
@@ -99,7 +99,7 @@ const program = new Command();
|
|
|
99
99
|
program
|
|
100
100
|
.name('saac_crm')
|
|
101
101
|
.description('AI-first CRM CLI — manage leads and API keys')
|
|
102
|
-
.version('1.0.
|
|
102
|
+
.version('1.0.2')
|
|
103
103
|
.option('--api-key <key>', 'API key (overrides SAAC_CRM_API_KEY env and config)')
|
|
104
104
|
.option('--url <url>', 'API base URL (overrides config)');
|
|
105
105
|
|
|
@@ -115,9 +115,14 @@ configCmd
|
|
|
115
115
|
.option('--url <url>', 'CRM API base URL (e.g. https://yourapp.example.com)')
|
|
116
116
|
.option('--api-key <key>', 'Default API key to use')
|
|
117
117
|
.action((opts) => {
|
|
118
|
+
// NOTE: commander.js may consume --url and --api-key at the parent program level
|
|
119
|
+
// if they share the same option names. Always fall back to program.opts().
|
|
120
|
+
const globalOpts = program.opts();
|
|
118
121
|
const cfg = loadConfig();
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
const urlValue = opts.url || globalOpts.url;
|
|
123
|
+
const apiKeyValue = opts.apiKey || globalOpts.apiKey;
|
|
124
|
+
if (urlValue) cfg.apiUrl = urlValue;
|
|
125
|
+
if (apiKeyValue) cfg.apiKey = apiKeyValue;
|
|
121
126
|
saveConfig(cfg);
|
|
122
127
|
console.log('Configuration saved to', CONFIG_FILE);
|
|
123
128
|
printJSON(cfg);
|
|
@@ -185,15 +190,15 @@ keysCmd
|
|
|
185
190
|
});
|
|
186
191
|
|
|
187
192
|
// ============================================================
|
|
188
|
-
// REGISTER —
|
|
193
|
+
// REGISTER — first-time workspace + admin key setup
|
|
194
|
+
// NOTE: For agent self-service (zero-auth), use: saac_crm keys create --name <name>
|
|
189
195
|
// ============================================================
|
|
190
196
|
|
|
191
197
|
program
|
|
192
198
|
.command('register')
|
|
193
|
-
.description('
|
|
194
|
-
.requiredOption('--workspace <slug>', 'Workspace slug (3-30 chars, a-z0-9 only, e.g. goldenrecruit101)')
|
|
199
|
+
.description('First-time setup: register workspace slug and create admin scope API key (prompts for missing values)')
|
|
195
200
|
.requiredOption('--name <name>', 'Name/label for this admin key')
|
|
196
|
-
.option('--
|
|
201
|
+
.option('--workspace <slug>', 'Workspace slug (3-30 chars, a-z0-9 only, e.g. mycompany)')
|
|
197
202
|
.option('--admin-password <password>', 'Deployment admin password (will prompt if not provided)')
|
|
198
203
|
.action(async (opts) => {
|
|
199
204
|
const globalOpts = program.opts();
|
|
@@ -203,6 +208,13 @@ program
|
|
|
203
208
|
process.exit(1);
|
|
204
209
|
}
|
|
205
210
|
|
|
211
|
+
// Prompt for workspace slug if not provided
|
|
212
|
+
let workspace = opts.workspace;
|
|
213
|
+
if (!workspace) {
|
|
214
|
+
workspace = await promptSecret('Workspace slug (e.g. mycompany): ');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Prompt for admin password if not provided
|
|
206
218
|
let adminPw = opts.adminPassword;
|
|
207
219
|
if (!adminPw) {
|
|
208
220
|
adminPw = await promptSecret('Deployment admin password: ');
|
|
@@ -211,7 +223,7 @@ program
|
|
|
211
223
|
const body = {
|
|
212
224
|
name: opts.name,
|
|
213
225
|
scope: 'admin',
|
|
214
|
-
workspace:
|
|
226
|
+
workspace: workspace.trim().toLowerCase(),
|
|
215
227
|
admin_password: adminPw
|
|
216
228
|
};
|
|
217
229
|
|
|
@@ -221,7 +233,7 @@ program
|
|
|
221
233
|
body,
|
|
222
234
|
{ headers: { 'Content-Type': 'application/json' } }
|
|
223
235
|
);
|
|
224
|
-
console.log(`Workspace '${
|
|
236
|
+
console.log(`Workspace '${workspace}' registered. Admin key created — store it securely.`);
|
|
225
237
|
printJSON(res.data.data);
|
|
226
238
|
} catch (err) {
|
|
227
239
|
handleError(err);
|
|
@@ -310,7 +322,7 @@ leadsCmd
|
|
|
310
322
|
};
|
|
311
323
|
try {
|
|
312
324
|
const res = await client.post('/leads', body, { headers });
|
|
313
|
-
printJSON(res.data
|
|
325
|
+
printJSON(res.data);
|
|
314
326
|
} catch (err) {
|
|
315
327
|
handleError(err);
|
|
316
328
|
}
|
|
@@ -323,15 +335,20 @@ leadsCmd
|
|
|
323
335
|
.option('--email <email>', 'Filter by email (partial match)')
|
|
324
336
|
.option('--company <company>', 'Filter by company (partial match)')
|
|
325
337
|
.option('--tag <tag>', 'Filter by tag — repeatable, AND logic', (v, prev) => prev.concat([v]), [])
|
|
338
|
+
.option('--api-key-id <id>', 'Filter by the API key ID that created the lead')
|
|
339
|
+
.option('--self', 'Filter leads created by the current API key (shorthand for --api-key-id self)')
|
|
326
340
|
.option('--page <n>', 'Page number', '1')
|
|
327
341
|
.option('--per-page <n>', 'Results per page', '20')
|
|
328
342
|
.action(async (opts) => {
|
|
329
343
|
const globalOpts = program.opts();
|
|
330
344
|
const client = getClient(globalOpts);
|
|
345
|
+
// --self is a shorthand for --api-key-id self
|
|
346
|
+
const apiKeyIdFilter = opts.self ? 'self' : opts.apiKeyId;
|
|
331
347
|
const params = {
|
|
332
348
|
...(opts.status && { status: opts.status }),
|
|
333
349
|
...(opts.email && { email: opts.email }),
|
|
334
350
|
...(opts.company && { company: opts.company }),
|
|
351
|
+
...(apiKeyIdFilter && { api_key_id: apiKeyIdFilter }),
|
|
335
352
|
page: opts.page,
|
|
336
353
|
per_page: opts.perPage
|
|
337
354
|
};
|
|
@@ -354,7 +371,7 @@ leadsCmd
|
|
|
354
371
|
const client = getClient(globalOpts);
|
|
355
372
|
try {
|
|
356
373
|
const res = await client.get(`/leads/${id}`);
|
|
357
|
-
printJSON(res.data
|
|
374
|
+
printJSON(res.data);
|
|
358
375
|
} catch (err) {
|
|
359
376
|
handleError(err);
|
|
360
377
|
}
|
|
@@ -392,7 +409,7 @@ leadsCmd
|
|
|
392
409
|
};
|
|
393
410
|
try {
|
|
394
411
|
const res = await client.put(`/leads/${id}`, body);
|
|
395
|
-
printJSON(res.data
|
|
412
|
+
printJSON(res.data);
|
|
396
413
|
} catch (err) {
|
|
397
414
|
handleError(err);
|
|
398
415
|
}
|
|
@@ -433,35 +450,47 @@ leadsCmd
|
|
|
433
450
|
|
|
434
451
|
const usersCmd = program.command('users').description('Manage human user accounts (requires admin scope key)');
|
|
435
452
|
|
|
453
|
+
// Shared action for users create / users register
|
|
454
|
+
async function createUserAction(opts) {
|
|
455
|
+
const globalOpts = program.opts();
|
|
456
|
+
const client = getClient(globalOpts);
|
|
457
|
+
|
|
458
|
+
let password = opts.password;
|
|
459
|
+
if (!password) {
|
|
460
|
+
password = await promptSecret('Password for new user: ');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
const res = await client.post('/users', {
|
|
465
|
+
email: opts.email,
|
|
466
|
+
name: opts.name,
|
|
467
|
+
password,
|
|
468
|
+
role: opts.role
|
|
469
|
+
});
|
|
470
|
+
console.log('User created successfully.');
|
|
471
|
+
printJSON(res.data.data);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
handleError(err);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
436
477
|
usersCmd
|
|
437
|
-
.command('
|
|
478
|
+
.command('create')
|
|
438
479
|
.description('Create a new human user account')
|
|
439
480
|
.requiredOption('--email <email>', 'User email address')
|
|
440
481
|
.requiredOption('--name <name>', 'User full name')
|
|
441
482
|
.option('--password <password>', 'User password (will prompt if not provided)')
|
|
442
483
|
.option('--role <role>', 'Role: admin or viewer', 'viewer')
|
|
443
|
-
.action(
|
|
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
|
-
}
|
|
484
|
+
.action(createUserAction);
|
|
451
485
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
printJSON(res.data.data);
|
|
461
|
-
} catch (err) {
|
|
462
|
-
handleError(err);
|
|
463
|
-
}
|
|
464
|
-
});
|
|
486
|
+
usersCmd
|
|
487
|
+
.command('register')
|
|
488
|
+
.description('Create a new human user account (alias for users create)')
|
|
489
|
+
.requiredOption('--email <email>', 'User email address')
|
|
490
|
+
.requiredOption('--name <name>', 'User full name')
|
|
491
|
+
.option('--password <password>', 'User password (will prompt if not provided)')
|
|
492
|
+
.option('--role <role>', 'Role: admin or viewer', 'viewer')
|
|
493
|
+
.action(createUserAction);
|
|
465
494
|
|
|
466
495
|
usersCmd
|
|
467
496
|
.command('list')
|
|
@@ -518,4 +547,45 @@ usersCmd
|
|
|
518
547
|
}
|
|
519
548
|
});
|
|
520
549
|
|
|
550
|
+
// ============================================================
|
|
551
|
+
// LOGIN — human admin/viewer login, saves JWT to config
|
|
552
|
+
// ============================================================
|
|
553
|
+
|
|
554
|
+
program
|
|
555
|
+
.command('login')
|
|
556
|
+
.description('Log in as a human admin/viewer (workspace + email + password → JWT saved to config)')
|
|
557
|
+
.requiredOption('--workspace <slug>', 'Workspace slug')
|
|
558
|
+
.requiredOption('--email <email>', 'Your email address')
|
|
559
|
+
.option('--password <password>', 'Your password (will prompt if not provided)')
|
|
560
|
+
.action(async (opts) => {
|
|
561
|
+
const globalOpts = program.opts();
|
|
562
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
563
|
+
if (!apiUrl) {
|
|
564
|
+
console.error('Error: API URL not configured. Run: saac_crm config set --url <api-url>');
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
let password = opts.password;
|
|
569
|
+
if (!password) {
|
|
570
|
+
password = await promptSecret('Password: ');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const res = await axios.post(
|
|
575
|
+
`${apiUrl.replace(/\/$/, '')}/api/v1/auth/login`,
|
|
576
|
+
{ workspace: opts.workspace, email: opts.email, password },
|
|
577
|
+
{ headers: { 'Content-Type': 'application/json' } }
|
|
578
|
+
);
|
|
579
|
+
const { token, user } = res.data.data;
|
|
580
|
+
// Save token to config for subsequent admin operations
|
|
581
|
+
const cfg = loadConfig();
|
|
582
|
+
cfg.token = token;
|
|
583
|
+
saveConfig(cfg);
|
|
584
|
+
console.log(`Logged in as ${user.email} (${user.role}). JWT saved to config.`);
|
|
585
|
+
printJSON({ email: user.email, name: user.name, role: user.role });
|
|
586
|
+
} catch (err) {
|
|
587
|
+
handleError(err);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
521
591
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startanaicompany/crm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "AI-first CRM CLI — manage leads and API keys from the terminal",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,8 +9,21 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "echo \"No tests yet\""
|
|
11
11
|
},
|
|
12
|
-
"keywords": ["crm", "ai", "leads", "api"],
|
|
12
|
+
"keywords": ["crm", "ai", "leads", "api", "saac", "agent"],
|
|
13
13
|
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://git.startanaicompany.com/mikaelwestoo/url-friendly-slug-59b2ag.git",
|
|
17
|
+
"directory": "packages/crm-cli"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"index.js",
|
|
21
|
+
"README.md",
|
|
22
|
+
"package.json"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
14
27
|
"dependencies": {
|
|
15
28
|
"axios": "^1.6.0",
|
|
16
29
|
"commander": "^11.1.0"
|