@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.
Files changed (3) hide show
  1. package/README.md +130 -49
  2. package/index.js +264 -27
  3. 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
- ## 🤖 AI Agent Quick Start (zero-auth, no human needed)
13
+ ## 🏢 Multi-Tenant Architecture
14
14
 
15
- Agents bootstrap themselves with a single command **no credentials required**:
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: 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
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 2: set the key and start managing leads
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: 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`.
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
- This is done once per deployment **not by agents**:
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://your-crm.example.com
56
+ saac_crm config set --url https://crm.startanaicompany.com
42
57
 
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
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 — ZERO AUTH, no prior setup needed
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 (operator only — requires deployment password + workspace slug)
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 (metadata only — full key never shown again)
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 --tag enterprise
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
- # → 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>
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://your-crm.example.com
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 **single-tenant** system: one workspace slug per deployment, registered once by an operator.
231
+ This is a **multi-tenant** system: multiple workspace slugs per deployment, each fully isolated.
152
232
 
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
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 getClient(options = {}) {
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('2.3.0')
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 (required for scope=admin, e.g. goldenrecruit101)')
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 body = { name: opts.name, scope: opts.scope };
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 client = getClient(globalOpts);
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 client = getClient(globalOpts);
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 client = getClient(globalOpts);
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 client = getClient(globalOpts);
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 client = getClient(globalOpts);
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 client = getClient(globalOpts);
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 client = getClient(globalOpts);
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('Sign a contract (auto-moves linked lead to closed_won)')
1510
- .option('--signed-by <name>', 'Name of signatory')
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
- const res = await client.post(`/contracts/${id}/status`, { status: 'signed', signed_by: opts.signedBy });
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/crm",
3
- "version": "2.3.2",
3
+ "version": "2.5.0",
4
4
  "description": "AI-first CRM CLI \u2014 manage leads and API keys from the terminal",
5
5
  "main": "index.js",
6
6
  "bin": {