@startanaicompany/crm 2.3.2 → 2.5.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 +130 -49
- package/index.js +264 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,43 +10,56 @@ npm install -g @startanaicompany/crm
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## 🏢 Multi-Tenant Architecture
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
This CRM is **multi-tenant**: each company gets its own isolated workspace. All data (leads, contacts, calls, contracts, etc.) is completely separated between workspaces.
|
|
16
|
+
|
|
17
|
+
- One workspace slug per company (e.g. `mygardeningcompany`, `johnscarshop`)
|
|
18
|
+
- All agents of a company share the same workspace — one key per "fleet" or one key per agent, your choice
|
|
19
|
+
- Cross-workspace data access is impossible: a key from workspace `mygardeningcompany` can never see data from `johnscarshop`
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🤖 AI Agent Quick Start (zero-auth)
|
|
24
|
+
|
|
25
|
+
Agents bootstrap themselves in two steps:
|
|
16
26
|
|
|
17
27
|
```bash
|
|
18
|
-
# Step 1:
|
|
19
|
-
saac_crm config set --url https://
|
|
20
|
-
|
|
21
|
-
#
|
|
28
|
+
# Step 1: Point at the CRM deployment
|
|
29
|
+
saac_crm config set --url https://crm.startanaicompany.com
|
|
30
|
+
|
|
31
|
+
# Step 2: Bootstrap your workspace + get an API key (no auth needed, no human required)
|
|
32
|
+
saac_crm workspace init --workspace mygardeningcompany --name my-agent
|
|
33
|
+
# → Returns crm_xxxx API key + saves key + workspace to ~/.saac_crm/config.json
|
|
22
34
|
|
|
23
|
-
# Step
|
|
24
|
-
export SAAC_CRM_API_KEY=crm_xxxxxxxxxxxx
|
|
35
|
+
# Step 3: Start working (workspace is automatically used from config)
|
|
25
36
|
saac_crm leads create --name "Jane Smith" --email "jane@example.com"
|
|
26
37
|
saac_crm leads list
|
|
38
|
+
|
|
39
|
+
# Optional: attribute actions to a named agent for audit trail
|
|
40
|
+
saac_crm leads create --name "Bob Jones" --email "bob@example.com" --from-agent-name "discovery-agent-v2"
|
|
27
41
|
```
|
|
28
42
|
|
|
29
43
|
> **Auth priority**: `--api-key` flag > `SAAC_CRM_API_KEY` env var > config file (`~/.saac_crm/config.json`)
|
|
44
|
+
> **Workspace priority**: `--workspace` flag > `SAAC_CRM_WORKSPACE` env var > config file `workspace` field
|
|
30
45
|
|
|
31
|
-
> ⚠️ **Agents:
|
|
46
|
+
> ⚠️ **Agents: `workspace init` is idempotent** — if the workspace already exists, it just creates a new key for it. Safe to call from any agent on first run.
|
|
32
47
|
|
|
33
48
|
---
|
|
34
49
|
|
|
35
50
|
## 👤 Operator Setup (one-time, human)
|
|
36
51
|
|
|
37
|
-
|
|
52
|
+
For the initial admin setup or to create admin-scope keys:
|
|
38
53
|
|
|
39
54
|
```bash
|
|
40
55
|
# 1. Point at your deployment
|
|
41
|
-
saac_crm config set --url https://
|
|
56
|
+
saac_crm config set --url https://crm.startanaicompany.com
|
|
42
57
|
|
|
43
|
-
# 2.
|
|
44
|
-
saac_crm
|
|
45
|
-
# →
|
|
46
|
-
# → prompts: Deployment admin password:
|
|
47
|
-
# → returns crm_xxx admin key — store securely
|
|
58
|
+
# 2. Create admin-scope key for your workspace (requires deployment admin password)
|
|
59
|
+
saac_crm keys create --name "admin-key" --workspace mycompany --scope admin
|
|
60
|
+
# → Prompts for admin password
|
|
48
61
|
|
|
49
|
-
# 3. Save admin key and create first human user account
|
|
62
|
+
# 3. Save admin key to config and create the first human user account
|
|
50
63
|
saac_crm config set --api-key crm_xxx_admin_key
|
|
51
64
|
saac_crm users create --email admin@example.com --name "Admin" --role admin
|
|
52
65
|
```
|
|
@@ -55,17 +68,27 @@ saac_crm users create --email admin@example.com --name "Admin" --role admin
|
|
|
55
68
|
|
|
56
69
|
## Commands
|
|
57
70
|
|
|
71
|
+
### Workspace
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Bootstrap workspace + create API key in one step (recommended for agent auto-init)
|
|
75
|
+
saac_crm workspace init --workspace mycompany --name "my-agent"
|
|
76
|
+
saac_crm workspace init --workspace mycompany --name "admin-key" --scope admin
|
|
77
|
+
|
|
78
|
+
# Via REST: POST /api/v1/workspaces/register
|
|
79
|
+
# Body: { workspace, key_name, scope?, admin_password? }
|
|
80
|
+
```
|
|
81
|
+
|
|
58
82
|
### API Keys
|
|
59
83
|
|
|
60
84
|
```bash
|
|
61
|
-
# Create agent key —
|
|
62
|
-
saac_crm keys create --name "my-agent"
|
|
85
|
+
# Create agent key — workspace required (auto-creates workspace if it doesn't exist yet)
|
|
86
|
+
saac_crm keys create --name "my-agent" --workspace mycompany
|
|
63
87
|
|
|
64
|
-
# Create admin scope key (
|
|
65
|
-
saac_crm keys create --name "admin-key" --scope admin
|
|
66
|
-
--workspace mycompany --admin-password <deployment-password>
|
|
88
|
+
# Create admin scope key (requires deployment password)
|
|
89
|
+
saac_crm keys create --name "admin-key" --workspace mycompany --scope admin
|
|
67
90
|
|
|
68
|
-
# List all keys
|
|
91
|
+
# List all keys in current workspace
|
|
69
92
|
saac_crm keys list
|
|
70
93
|
|
|
71
94
|
# Show current key info
|
|
@@ -78,11 +101,11 @@ saac_crm keys revoke <id>
|
|
|
78
101
|
### Leads
|
|
79
102
|
|
|
80
103
|
```bash
|
|
81
|
-
# Create a lead
|
|
82
|
-
saac_crm leads create --name "Jane Smith" --email "jane@example.com" --company "Acme"
|
|
104
|
+
# Create a lead (optional: --from-agent-name for audit trail)
|
|
105
|
+
saac_crm leads create --name "Jane Smith" --email "jane@example.com" --company "Acme" --from-agent-name "lead-gen-agent"
|
|
83
106
|
|
|
84
107
|
# List leads (with filters)
|
|
85
|
-
saac_crm leads list --status new --tag vip
|
|
108
|
+
saac_crm leads list --status new --tag vip
|
|
86
109
|
|
|
87
110
|
# Get single lead
|
|
88
111
|
saac_crm leads get <id>
|
|
@@ -95,22 +118,76 @@ saac_crm leads delete <id>
|
|
|
95
118
|
|
|
96
119
|
# Status change history
|
|
97
120
|
saac_crm leads history <id>
|
|
121
|
+
|
|
122
|
+
# Move lead to pipeline stage
|
|
123
|
+
saac_crm leads stage <id> --stage discovery
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Contacts
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
saac_crm contacts create --email contact@example.com --first-name "Jane" --from-agent-name "enrichment-agent"
|
|
130
|
+
saac_crm contacts list
|
|
131
|
+
saac_crm contacts get <id>
|
|
132
|
+
saac_crm contacts update <id> --company "Acme"
|
|
133
|
+
saac_crm contacts delete <id>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Calls
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
saac_crm calls create --lead-id <id> --direction outbound --outcome connected --from-agent-name "call-agent"
|
|
140
|
+
saac_crm calls list [--lead-id <id>]
|
|
141
|
+
saac_crm calls get <id>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Meetings
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
saac_crm meetings create --lead-id <id> --title "Demo call" --from-agent-name "scheduler-agent"
|
|
148
|
+
saac_crm meetings list
|
|
149
|
+
saac_crm meetings get <id>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Emails
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
saac_crm emails create --lead-id <id> --subject "Follow-up" --direction sent --from-agent-name "outreach-agent"
|
|
156
|
+
saac_crm emails list [--lead-id <id>]
|
|
157
|
+
saac_crm emails get <id>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Quotes
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
saac_crm quotes create --lead-id <id> --title "Q-2026-001" --value 5000 --from-agent-name "quote-agent"
|
|
164
|
+
saac_crm quotes list
|
|
165
|
+
saac_crm quotes get <id>
|
|
166
|
+
saac_crm quotes status <id> --status accepted
|
|
167
|
+
saac_crm quotes add-line <id> --description "License" --quantity 1 --unit-price 5000
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Contracts
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
saac_crm contracts create --lead-id <id> --title "Service Agreement" --from-agent-name "contract-agent"
|
|
174
|
+
saac_crm contracts list
|
|
175
|
+
saac_crm contracts get <id>
|
|
176
|
+
saac_crm contracts status <id> --status sent
|
|
177
|
+
saac_crm contracts signatories <id>
|
|
178
|
+
saac_crm contracts add-signatory <id> --contact-id <cid> --party customer --role signer
|
|
179
|
+
saac_crm contracts sign <id> --contact-id <cid>
|
|
180
|
+
saac_crm contracts decline-signature <id> --contact-id <cid>
|
|
181
|
+
saac_crm contracts remind-signatory <id> --contact-id <cid>
|
|
182
|
+
saac_crm contracts remind-all-pending <id>
|
|
98
183
|
```
|
|
99
184
|
|
|
100
185
|
### Users (requires admin scope key)
|
|
101
186
|
|
|
102
187
|
```bash
|
|
103
|
-
# Create a human user account (both commands are equivalent)
|
|
104
188
|
saac_crm users create --email admin@example.com --name "Admin User" --role admin
|
|
105
|
-
saac_crm users register --email admin@example.com --name "Admin User" --role admin
|
|
106
|
-
|
|
107
|
-
# List users
|
|
108
189
|
saac_crm users list
|
|
109
|
-
|
|
110
|
-
# Update user
|
|
111
190
|
saac_crm users update <id> --role viewer
|
|
112
|
-
|
|
113
|
-
# Deactivate user
|
|
114
191
|
saac_crm users deactivate <id>
|
|
115
192
|
```
|
|
116
193
|
|
|
@@ -119,23 +196,16 @@ saac_crm users deactivate <id>
|
|
|
119
196
|
```bash
|
|
120
197
|
# Log in as admin/viewer — saves JWT to config for dashboard access
|
|
121
198
|
saac_crm login --workspace mycompany --email admin@example.com
|
|
122
|
-
# →
|
|
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>
|
|
199
|
+
# → Prompts for password, saves JWT to ~/.saac_crm/config.json
|
|
132
200
|
```
|
|
133
201
|
|
|
134
202
|
### Configuration
|
|
135
203
|
|
|
136
204
|
```bash
|
|
137
|
-
saac_crm config set --url https://
|
|
205
|
+
saac_crm config set --url https://crm.startanaicompany.com
|
|
138
206
|
saac_crm config set --api-key crm_xxxxxxxxxxxx
|
|
207
|
+
saac_crm config set --workspace mycompany
|
|
208
|
+
saac_crm config set --agent-name "my-agent-v2" # default --from-agent-name for all commands
|
|
139
209
|
saac_crm config get
|
|
140
210
|
```
|
|
141
211
|
|
|
@@ -146,10 +216,21 @@ saac_crm config get
|
|
|
146
216
|
--url <url> Override API base URL for this command
|
|
147
217
|
```
|
|
148
218
|
|
|
219
|
+
## `--from-agent-name` Attribution
|
|
220
|
+
|
|
221
|
+
Pass `--from-agent-name <name>` (or set `SAAC_CRM_AGENT_NAME` env / `config.defaultAgentName`) on any create command to attribute the action to a named agent. This is for **audit trail only** — it does NOT affect authentication or authorization. The same workspace key is shared across all agents of that workspace.
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
export SAAC_CRM_AGENT_NAME=discovery-agent-v2
|
|
225
|
+
saac_crm leads create --name "Alice" --email "alice@example.com"
|
|
226
|
+
# → lead.from_agent_name = "discovery-agent-v2"
|
|
227
|
+
```
|
|
228
|
+
|
|
149
229
|
## Architecture
|
|
150
230
|
|
|
151
|
-
This is a **
|
|
231
|
+
This is a **multi-tenant** system: multiple workspace slugs per deployment, each fully isolated.
|
|
152
232
|
|
|
153
|
-
- **Agents** →
|
|
154
|
-
- **Operators** → `
|
|
155
|
-
- **Humans** → `login` → web dashboard
|
|
233
|
+
- **Agents** → `workspace init` once → `leads create` → manage leads with workspace isolation
|
|
234
|
+
- **Operators** → `keys create --scope admin` → `users create` → human accounts
|
|
235
|
+
- **Humans** → `login` → web dashboard (only shows their workspace's data)
|
|
236
|
+
- **Audit** → `--from-agent-name` for per-agent attribution within a workspace
|
package/index.js
CHANGED
|
@@ -43,7 +43,21 @@ function resolveApiUrl(cliUrl) {
|
|
|
43
43
|
return cfg.apiUrl || null;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function
|
|
46
|
+
function resolveWorkspace(cliWorkspace) {
|
|
47
|
+
if (cliWorkspace) return cliWorkspace;
|
|
48
|
+
if (process.env.SAAC_CRM_WORKSPACE) return process.env.SAAC_CRM_WORKSPACE;
|
|
49
|
+
const cfg = loadConfig();
|
|
50
|
+
return cfg.workspace || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveAgentName(cliAgentName) {
|
|
54
|
+
if (cliAgentName) return cliAgentName;
|
|
55
|
+
if (process.env.SAAC_CRM_AGENT_NAME) return process.env.SAAC_CRM_AGENT_NAME;
|
|
56
|
+
const cfg = loadConfig();
|
|
57
|
+
return cfg.defaultAgentName || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getClient(options = {}, agentName = null) {
|
|
47
61
|
const apiKey = resolveApiKey(options.apiKey);
|
|
48
62
|
const apiUrl = resolveApiUrl(options.url);
|
|
49
63
|
|
|
@@ -56,12 +70,15 @@ function getClient(options = {}) {
|
|
|
56
70
|
process.exit(1);
|
|
57
71
|
}
|
|
58
72
|
|
|
73
|
+
const headers = {
|
|
74
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
75
|
+
'Content-Type': 'application/json'
|
|
76
|
+
};
|
|
77
|
+
if (agentName) headers['X-Agent-Name'] = agentName;
|
|
78
|
+
|
|
59
79
|
return axios.create({
|
|
60
80
|
baseURL: `${apiUrl.replace(/\/$/, '')}/api/v1`,
|
|
61
|
-
headers
|
|
62
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
63
|
-
'Content-Type': 'application/json'
|
|
64
|
-
}
|
|
81
|
+
headers
|
|
65
82
|
});
|
|
66
83
|
}
|
|
67
84
|
|
|
@@ -96,10 +113,12 @@ function promptSecret(question) {
|
|
|
96
113
|
|
|
97
114
|
const program = new Command();
|
|
98
115
|
|
|
116
|
+
const { version: pkgVersion } = require('./package.json');
|
|
117
|
+
|
|
99
118
|
program
|
|
100
119
|
.name('saac_crm')
|
|
101
120
|
.description('AI-first CRM CLI — manage leads and API keys')
|
|
102
|
-
.version(
|
|
121
|
+
.version(pkgVersion)
|
|
103
122
|
.option('--api-key <key>', 'API key (overrides SAAC_CRM_API_KEY env and config)')
|
|
104
123
|
.option('--url <url>', 'API base URL (overrides config)');
|
|
105
124
|
|
|
@@ -114,6 +133,8 @@ configCmd
|
|
|
114
133
|
.description('Set configuration values')
|
|
115
134
|
.option('--url <url>', 'CRM API base URL (e.g. https://yourapp.example.com)')
|
|
116
135
|
.option('--api-key <key>', 'Default API key to use')
|
|
136
|
+
.option('--workspace <slug>', 'Default workspace slug')
|
|
137
|
+
.option('--agent-name <name>', 'Default agent name for attribution (stored as defaultAgentName)')
|
|
117
138
|
.action((opts) => {
|
|
118
139
|
// NOTE: commander.js may consume --url and --api-key at the parent program level
|
|
119
140
|
// if they share the same option names. Always fall back to program.opts().
|
|
@@ -123,6 +144,8 @@ configCmd
|
|
|
123
144
|
const apiKeyValue = opts.apiKey || globalOpts.apiKey;
|
|
124
145
|
if (urlValue) cfg.apiUrl = urlValue;
|
|
125
146
|
if (apiKeyValue) cfg.apiKey = apiKeyValue;
|
|
147
|
+
if (opts.workspace) cfg.workspace = opts.workspace;
|
|
148
|
+
if (opts.agentName) cfg.defaultAgentName = opts.agentName;
|
|
126
149
|
saveConfig(cfg);
|
|
127
150
|
console.log('Configuration saved to', CONFIG_FILE);
|
|
128
151
|
printJSON(cfg);
|
|
@@ -149,7 +172,7 @@ keysCmd
|
|
|
149
172
|
.description('Create a new API key')
|
|
150
173
|
.requiredOption('--name <name>', 'Name/label for the key')
|
|
151
174
|
.option('--scope <scope>', 'Key scope: agent (default) or admin', 'agent')
|
|
152
|
-
.option('--workspace <slug>', 'Workspace slug (
|
|
175
|
+
.option('--workspace <slug>', 'Workspace slug (falls back to config workspace)')
|
|
153
176
|
.option('--admin-password <password>', 'Admin password (required for scope=admin)')
|
|
154
177
|
.action(async (opts) => {
|
|
155
178
|
const globalOpts = program.opts();
|
|
@@ -159,15 +182,15 @@ keysCmd
|
|
|
159
182
|
process.exit(1);
|
|
160
183
|
}
|
|
161
184
|
|
|
162
|
-
const
|
|
185
|
+
const workspace = resolveWorkspace(opts.workspace);
|
|
186
|
+
if (!workspace) {
|
|
187
|
+
console.error('Error: --workspace <slug> is required (or set in config via: saac_crm config set --workspace <slug>)');
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const body = { name: opts.name, scope: opts.scope, workspace };
|
|
163
192
|
|
|
164
193
|
if (opts.scope === 'admin') {
|
|
165
|
-
if (!opts.workspace) {
|
|
166
|
-
console.error('Error: --workspace <slug> is required for scope=admin');
|
|
167
|
-
process.exit(1);
|
|
168
|
-
}
|
|
169
|
-
body.workspace = opts.workspace;
|
|
170
|
-
|
|
171
194
|
let adminPw = opts.adminPassword;
|
|
172
195
|
if (!adminPw) {
|
|
173
196
|
adminPw = await promptSecret('Admin password: ');
|
|
@@ -240,6 +263,64 @@ program
|
|
|
240
263
|
}
|
|
241
264
|
});
|
|
242
265
|
|
|
266
|
+
// ============================================================
|
|
267
|
+
// WORKSPACE COMMANDS
|
|
268
|
+
// ============================================================
|
|
269
|
+
|
|
270
|
+
const workspaceCmd = program.command('workspace').description('Manage CRM workspaces');
|
|
271
|
+
|
|
272
|
+
workspaceCmd
|
|
273
|
+
.command('init')
|
|
274
|
+
.description('Bootstrap a new workspace: register slug + create API key, save to config')
|
|
275
|
+
.requiredOption('--workspace <slug>', 'Workspace slug (3-50 chars, a-z0-9/dash/underscore)')
|
|
276
|
+
.requiredOption('--name <keyname>', 'Name/label for the initial API key')
|
|
277
|
+
.option('--scope <scope>', 'Key scope: agent (default) or admin', 'agent')
|
|
278
|
+
.option('--admin-password <password>', 'Admin password (required for scope=admin; will prompt if not provided)')
|
|
279
|
+
.option('--save-key', 'Save the returned key to CLI config (default: true)', true)
|
|
280
|
+
.action(async (opts) => {
|
|
281
|
+
const globalOpts = program.opts();
|
|
282
|
+
const apiUrl = resolveApiUrl(globalOpts.url);
|
|
283
|
+
if (!apiUrl) {
|
|
284
|
+
console.error('Error: API URL not configured. Run: saac_crm config set --url <api-url>');
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const body = {
|
|
289
|
+
workspace: opts.workspace.trim().toLowerCase(),
|
|
290
|
+
key_name: opts.name,
|
|
291
|
+
scope: opts.scope
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
if (opts.scope === 'admin') {
|
|
295
|
+
let adminPw = opts.adminPassword;
|
|
296
|
+
if (!adminPw) {
|
|
297
|
+
adminPw = await promptSecret('Admin password: ');
|
|
298
|
+
}
|
|
299
|
+
body.admin_password = adminPw;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const res = await axios.post(
|
|
304
|
+
`${apiUrl.replace(/\/$/, '')}/api/v1/workspaces/register`,
|
|
305
|
+
body,
|
|
306
|
+
{ headers: { 'Content-Type': 'application/json' } }
|
|
307
|
+
);
|
|
308
|
+
const data = res.data.data;
|
|
309
|
+
console.log(`Workspace '${data.workspace}' initialized. API key created.`);
|
|
310
|
+
console.log('Store the key — it will not be shown again.');
|
|
311
|
+
printJSON(data);
|
|
312
|
+
|
|
313
|
+
// Auto-save to config
|
|
314
|
+
const cfg = loadConfig();
|
|
315
|
+
cfg.apiKey = data.key;
|
|
316
|
+
cfg.workspace = data.workspace;
|
|
317
|
+
saveConfig(cfg);
|
|
318
|
+
console.log(`\nKey and workspace saved to config: ${CONFIG_FILE}`);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
handleError(err);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
243
324
|
keysCmd
|
|
244
325
|
.command('list')
|
|
245
326
|
.description('List all API keys (metadata only)')
|
|
@@ -305,9 +386,11 @@ leadsCmd
|
|
|
305
386
|
.option('--tag <tag>', 'Tag (repeatable)', (v, prev) => prev.concat([v]), [])
|
|
306
387
|
.option('--external-id <externalId>', 'External ID')
|
|
307
388
|
.option('--idempotency-key <key>', 'Idempotency key for deduplication')
|
|
389
|
+
.option('--from-agent-name <name>', 'Agent name for attribution (falls back to config defaultAgentName)')
|
|
308
390
|
.action(async (opts) => {
|
|
309
391
|
const globalOpts = program.opts();
|
|
310
|
-
const
|
|
392
|
+
const agentName = resolveAgentName(opts.fromAgentName);
|
|
393
|
+
const client = getClient(globalOpts, agentName);
|
|
311
394
|
const headers = {};
|
|
312
395
|
if (opts.idempotencyKey) headers['Idempotency-Key'] = opts.idempotencyKey;
|
|
313
396
|
const body = {
|
|
@@ -622,9 +705,11 @@ contactsCmd
|
|
|
622
705
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
623
706
|
.option('--do-not-contact', 'Mark as do not contact')
|
|
624
707
|
.option('--notes <notes>', 'Notes')
|
|
708
|
+
.option('--from-agent-name <name>', 'Agent name for attribution (falls back to config defaultAgentName)')
|
|
625
709
|
.action(async (opts) => {
|
|
626
710
|
const globalOpts = program.opts();
|
|
627
|
-
const
|
|
711
|
+
const agentName = resolveAgentName(opts.fromAgentName);
|
|
712
|
+
const client = getClient(globalOpts, agentName);
|
|
628
713
|
try {
|
|
629
714
|
const body = {
|
|
630
715
|
first_name: opts.firstName,
|
|
@@ -778,9 +863,11 @@ callsCmd
|
|
|
778
863
|
.option('--duration <seconds>', 'Duration in seconds')
|
|
779
864
|
.option('--notes <notes>', 'Call notes')
|
|
780
865
|
.option('--call-time <iso>', 'Call time (ISO8601, defaults to now)')
|
|
866
|
+
.option('--from-agent-name <name>', 'Agent name for attribution (falls back to config defaultAgentName)')
|
|
781
867
|
.action(async (opts) => {
|
|
782
868
|
const globalOpts = program.opts();
|
|
783
|
-
const
|
|
869
|
+
const agentName = resolveAgentName(opts.fromAgentName);
|
|
870
|
+
const client = getClient(globalOpts, agentName);
|
|
784
871
|
try {
|
|
785
872
|
const body = { direction: opts.direction };
|
|
786
873
|
if (opts.leadId) body.lead_id = opts.leadId;
|
|
@@ -946,9 +1033,11 @@ meetingsCmd
|
|
|
946
1033
|
.option('--notes <notes>', 'Meeting notes or agenda')
|
|
947
1034
|
.option('--outcome <outcome>', 'scheduled | completed | cancelled | no_show')
|
|
948
1035
|
.option('--sentiment <sentiment>', 'positive | neutral | negative | unknown')
|
|
1036
|
+
.option('--from-agent-name <name>', 'Agent name for attribution (falls back to config defaultAgentName)')
|
|
949
1037
|
.action(async (opts) => {
|
|
950
1038
|
const globalOpts = program.opts();
|
|
951
|
-
const
|
|
1039
|
+
const agentName = resolveAgentName(opts.fromAgentName);
|
|
1040
|
+
const client = getClient(globalOpts, agentName);
|
|
952
1041
|
try {
|
|
953
1042
|
const body = {
|
|
954
1043
|
title: opts.title,
|
|
@@ -1076,9 +1165,11 @@ emailsCmd
|
|
|
1076
1165
|
.option('--body-summary <summary>', 'Body summary (max 1000 chars)')
|
|
1077
1166
|
.option('--thread-id <id>', 'Thread ID for grouping related emails')
|
|
1078
1167
|
.option('--email-timestamp <iso>', 'When the email was sent/received (ISO8601, defaults to now)')
|
|
1168
|
+
.option('--from-agent-name <name>', 'Agent name for attribution (falls back to config defaultAgentName)')
|
|
1079
1169
|
.action(async (opts) => {
|
|
1080
1170
|
const globalOpts = program.opts();
|
|
1081
|
-
const
|
|
1171
|
+
const agentName = resolveAgentName(opts.fromAgentName);
|
|
1172
|
+
const client = getClient(globalOpts, agentName);
|
|
1082
1173
|
try {
|
|
1083
1174
|
const body = {
|
|
1084
1175
|
direction: opts.direction,
|
|
@@ -1169,9 +1260,11 @@ quotesCmd
|
|
|
1169
1260
|
.option('--currency <code>', 'Currency code (default: USD)')
|
|
1170
1261
|
.option('--validity-date <date>', 'Validity date (YYYY-MM-DD)')
|
|
1171
1262
|
.option('--notes <notes>', 'Notes')
|
|
1263
|
+
.option('--from-agent-name <name>', 'Agent name for attribution (falls back to config defaultAgentName)')
|
|
1172
1264
|
.action(async (opts) => {
|
|
1173
1265
|
const globalOpts = program.opts();
|
|
1174
|
-
const
|
|
1266
|
+
const agentName = resolveAgentName(opts.fromAgentName);
|
|
1267
|
+
const client = getClient(globalOpts, agentName);
|
|
1175
1268
|
try {
|
|
1176
1269
|
|
|
1177
1270
|
const res = await client.post('/quotes', {
|
|
@@ -1183,7 +1276,7 @@ quotesCmd
|
|
|
1183
1276
|
notes: opts.notes
|
|
1184
1277
|
});
|
|
1185
1278
|
printJSON(res.data);
|
|
1186
|
-
|
|
1279
|
+
|
|
1187
1280
|
} catch (err) {
|
|
1188
1281
|
handleError(err);
|
|
1189
1282
|
}
|
|
@@ -1394,9 +1487,11 @@ contractsCmd
|
|
|
1394
1487
|
.option('--end-date <date>', 'End date (YYYY-MM-DD)')
|
|
1395
1488
|
.option('--document-url <url>', 'Document URL reference')
|
|
1396
1489
|
.option('--notes <notes>', 'Notes')
|
|
1490
|
+
.option('--from-agent-name <name>', 'Agent name for attribution (falls back to config defaultAgentName)')
|
|
1397
1491
|
.action(async (opts) => {
|
|
1398
1492
|
const globalOpts = program.opts();
|
|
1399
|
-
const
|
|
1493
|
+
const agentName = resolveAgentName(opts.fromAgentName);
|
|
1494
|
+
const client = getClient(globalOpts, agentName);
|
|
1400
1495
|
try {
|
|
1401
1496
|
|
|
1402
1497
|
const res = await client.post('/contracts', {
|
|
@@ -1412,7 +1507,7 @@ contractsCmd
|
|
|
1412
1507
|
notes: opts.notes
|
|
1413
1508
|
});
|
|
1414
1509
|
printJSON(res.data);
|
|
1415
|
-
|
|
1510
|
+
|
|
1416
1511
|
} catch (err) {
|
|
1417
1512
|
handleError(err);
|
|
1418
1513
|
}
|
|
@@ -1506,16 +1601,158 @@ contractsCmd
|
|
|
1506
1601
|
|
|
1507
1602
|
contractsCmd
|
|
1508
1603
|
.command('sign <id>')
|
|
1509
|
-
.description('
|
|
1510
|
-
.option('--
|
|
1604
|
+
.description('Mark a specific signatory as signed (use --contact-id for multi-signatory; falls back to whole-contract sign if omitted)')
|
|
1605
|
+
.option('--contact-id <uuid>', 'Contact UUID of the signatory to mark as signed')
|
|
1606
|
+
.option('--method <method>', 'Signature method: electronic, wet_signature, docusign, hellosign, other')
|
|
1607
|
+
.option('--signed-at <datetime>', 'ISO8601 timestamp of signing (defaults to now)')
|
|
1608
|
+
.option('--signed-by <name>', 'Legacy: name of signatory (used when no --contact-id)')
|
|
1511
1609
|
.action(async (id, opts) => {
|
|
1512
1610
|
const globalOpts = program.opts();
|
|
1513
1611
|
const client = getClient(globalOpts);
|
|
1514
1612
|
try {
|
|
1613
|
+
if (opts.contactId) {
|
|
1614
|
+
// Sprint 8: per-signatory sign via PATCH /contracts/:id/signatories/:signatory_id
|
|
1615
|
+
// First find the signatory record for this contact
|
|
1616
|
+
const listRes = await client.get(`/contracts/${id}/signatories`);
|
|
1617
|
+
const signatories = listRes.data.data.signatories || [];
|
|
1618
|
+
const sig = signatories.find(s => s.contact_id === opts.contactId);
|
|
1619
|
+
if (!sig) {
|
|
1620
|
+
console.error(`No signatory found for contact ${opts.contactId} on contract ${id}`);
|
|
1621
|
+
process.exit(1);
|
|
1622
|
+
}
|
|
1623
|
+
const res = await client.patch(`/contracts/${id}/signatories/${sig.id}`, {
|
|
1624
|
+
status: 'signed',
|
|
1625
|
+
signature_method: opts.method || 'electronic',
|
|
1626
|
+
signed_at: opts.signedAt || new Date().toISOString(),
|
|
1627
|
+
});
|
|
1628
|
+
printJSON(res.data);
|
|
1629
|
+
} else {
|
|
1630
|
+
// Legacy whole-contract sign
|
|
1631
|
+
const res = await client.post(`/contracts/${id}/status`, { status: 'signed', signed_by: opts.signedBy });
|
|
1632
|
+
printJSON(res.data);
|
|
1633
|
+
}
|
|
1634
|
+
} catch (err) {
|
|
1635
|
+
handleError(err);
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
// Sprint 8: Multi-Signatory Commands
|
|
1640
|
+
|
|
1641
|
+
contractsCmd
|
|
1642
|
+
.command('signatories <id>')
|
|
1643
|
+
.description('List all signatories for a contract with status and timestamps')
|
|
1644
|
+
.action(async (id) => {
|
|
1645
|
+
const globalOpts = program.opts();
|
|
1646
|
+
const client = getClient(globalOpts);
|
|
1647
|
+
try {
|
|
1648
|
+
const res = await client.get(`/contracts/${id}/signatories`);
|
|
1649
|
+
const { signatories, summary } = res.data.data;
|
|
1650
|
+
console.log(`\nSignatories for contract ${id}`);
|
|
1651
|
+
console.log(`Summary: ${summary.signed}/${summary.total} signed | ${summary.pending} pending | ${summary.declined} declined`);
|
|
1652
|
+
if (summary.is_fully_executed) console.log('✅ Contract fully executed');
|
|
1653
|
+
console.log('');
|
|
1654
|
+
if (signatories.length === 0) {
|
|
1655
|
+
console.log('No signatories added yet.');
|
|
1656
|
+
} else {
|
|
1657
|
+
signatories.forEach(s => {
|
|
1658
|
+
const icon = s.status === 'signed' ? '✅' : s.status === 'declined' ? '⛔' : '⏳';
|
|
1659
|
+
const signedAt = s.signed_at ? ` | Signed: ${new Date(s.signed_at).toLocaleDateString()}` : '';
|
|
1660
|
+
const reminded = s.reminder_sent_at ? ` | Last reminded: ${new Date(s.reminder_sent_at).toLocaleDateString()}` : '';
|
|
1661
|
+
console.log(` ${icon} [${s.party}] ${s.contact_name} <${s.contact_email}> — ${s.role} | ${s.status}${signedAt}${reminded}`);
|
|
1662
|
+
if (s.notes) console.log(` Notes: ${s.notes}`);
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
} catch (err) {
|
|
1666
|
+
handleError(err);
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1515
1669
|
|
|
1516
|
-
|
|
1670
|
+
contractsCmd
|
|
1671
|
+
.command('add-signatory <id>')
|
|
1672
|
+
.description('Add a signatory to a contract')
|
|
1673
|
+
.requiredOption('--contact-id <uuid>', 'Contact UUID of the signatory')
|
|
1674
|
+
.requiredOption('--party <party>', 'Party: vendor or customer')
|
|
1675
|
+
.requiredOption('--role <role>', 'Role: signer, approver, or witness')
|
|
1676
|
+
.option('--method <method>', 'Signature method: electronic, wet_signature, docusign, hellosign, other')
|
|
1677
|
+
.option('--notes <notes>', 'Internal notes about this signatory')
|
|
1678
|
+
.action(async (id, opts) => {
|
|
1679
|
+
const globalOpts = program.opts();
|
|
1680
|
+
const client = getClient(globalOpts);
|
|
1681
|
+
try {
|
|
1682
|
+
const res = await client.post(`/contracts/${id}/signatories`, {
|
|
1683
|
+
contact_id: opts.contactId,
|
|
1684
|
+
party: opts.party,
|
|
1685
|
+
role: opts.role,
|
|
1686
|
+
signature_method: opts.method || undefined,
|
|
1687
|
+
notes: opts.notes || undefined,
|
|
1688
|
+
});
|
|
1689
|
+
printJSON(res.data);
|
|
1690
|
+
} catch (err) {
|
|
1691
|
+
handleError(err);
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
contractsCmd
|
|
1696
|
+
.command('decline-signature <id>')
|
|
1697
|
+
.description('Mark a signatory as declined (deal blocker alert)')
|
|
1698
|
+
.requiredOption('--contact-id <uuid>', 'Contact UUID of the signatory declining')
|
|
1699
|
+
.option('--notes <notes>', 'Reason for declining')
|
|
1700
|
+
.action(async (id, opts) => {
|
|
1701
|
+
const globalOpts = program.opts();
|
|
1702
|
+
const client = getClient(globalOpts);
|
|
1703
|
+
try {
|
|
1704
|
+
const listRes = await client.get(`/contracts/${id}/signatories`);
|
|
1705
|
+
const signatories = listRes.data.data.signatories || [];
|
|
1706
|
+
const sig = signatories.find(s => s.contact_id === opts.contactId);
|
|
1707
|
+
if (!sig) {
|
|
1708
|
+
console.error(`No signatory found for contact ${opts.contactId} on contract ${id}`);
|
|
1709
|
+
process.exit(1);
|
|
1710
|
+
}
|
|
1711
|
+
const res = await client.patch(`/contracts/${id}/signatories/${sig.id}`, {
|
|
1712
|
+
status: 'declined',
|
|
1713
|
+
notes: opts.notes || undefined,
|
|
1714
|
+
});
|
|
1715
|
+
console.log('⛔ Signature declined — deal blocker raised.');
|
|
1716
|
+
printJSON(res.data);
|
|
1717
|
+
} catch (err) {
|
|
1718
|
+
handleError(err);
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
contractsCmd
|
|
1723
|
+
.command('remind-signatory <id>')
|
|
1724
|
+
.description('Send a reminder to a pending signatory')
|
|
1725
|
+
.requiredOption('--contact-id <uuid>', 'Contact UUID of the signatory to remind')
|
|
1726
|
+
.action(async (id, opts) => {
|
|
1727
|
+
const globalOpts = program.opts();
|
|
1728
|
+
const client = getClient(globalOpts);
|
|
1729
|
+
try {
|
|
1730
|
+
const listRes = await client.get(`/contracts/${id}/signatories`);
|
|
1731
|
+
const signatories = listRes.data.data.signatories || [];
|
|
1732
|
+
const sig = signatories.find(s => s.contact_id === opts.contactId);
|
|
1733
|
+
if (!sig) {
|
|
1734
|
+
console.error(`No signatory found for contact ${opts.contactId} on contract ${id}`);
|
|
1735
|
+
process.exit(1);
|
|
1736
|
+
}
|
|
1737
|
+
const res = await client.post(`/contracts/${id}/signatories/${sig.id}/remind`);
|
|
1738
|
+
console.log(`📩 Reminder sent to ${sig.contact_name} (${sig.contact_email})`);
|
|
1739
|
+
printJSON(res.data);
|
|
1740
|
+
} catch (err) {
|
|
1741
|
+
handleError(err);
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
contractsCmd
|
|
1746
|
+
.command('remind-all-pending <id>')
|
|
1747
|
+
.description('Send reminders to all pending signatories on a contract')
|
|
1748
|
+
.action(async (id) => {
|
|
1749
|
+
const globalOpts = program.opts();
|
|
1750
|
+
const client = getClient(globalOpts);
|
|
1751
|
+
try {
|
|
1752
|
+
const res = await client.post(`/contracts/${id}/signatories/remind-all-pending`);
|
|
1753
|
+
const { reminded } = res.data.data;
|
|
1754
|
+
console.log(`📩 Reminders sent to ${reminded} pending signator${reminded === 1 ? 'y' : 'ies'}.`);
|
|
1517
1755
|
printJSON(res.data);
|
|
1518
|
-
|
|
1519
1756
|
} catch (err) {
|
|
1520
1757
|
handleError(err);
|
|
1521
1758
|
}
|