@dzhng/crm.cli 0.3.3 → 0.3.5
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/LICENSE +21 -0
- package/README.md +30 -17
- package/dist/cli.js +53 -6
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Zhang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
**A headless, CLI-first CRM for
|
|
5
|
+
**A headless, CLI-first CRM for AI native companies.** Contacts, deals, and pipeline in a single SQLite file — queryable from your terminal, composable with Unix tools, and mountable as a virtual filesystem so any tool that reads files (Claude Code, Codex, grep, jq, vim) has full CRM access without any integration.
|
|
6
6
|
|
|
7
7
|
No server. No Docker. No accounts. No GUI. Just `npm install -g @dzhng/crm.cli` and go.
|
|
8
8
|
|
|
9
|
-
> **
|
|
9
|
+
> **Created by [Duet](https://duet.so)** — a cloud agent workspace with persistent AI. Set up crm.cli in your own private cloud computer and run it with Claude Code or Codex — no local setup required. [Try Duet →](https://duet.so)
|
|
10
10
|
|
|
11
11
|
## Why crm.cli
|
|
12
12
|
|
|
@@ -207,12 +207,12 @@ Accepts ID, any email, any phone number, or any social handle (LinkedIn, X, Blue
|
|
|
207
207
|
#### `crm contact edit <id-or-email-or-phone-or-handle>`
|
|
208
208
|
|
|
209
209
|
```bash
|
|
210
|
-
crm contact edit jane@acme.com --name "Jane Smith"
|
|
210
|
+
crm contact edit jane@acme.com --name "Jane Smith" # by email
|
|
211
|
+
crm contact edit "+1-212-555-1234" --add-tag vip # by phone
|
|
212
|
+
crm contact edit janedoe --set title=CEO # by social handle (LinkedIn, X, etc.)
|
|
211
213
|
crm contact edit ct_01J8Z... --add-email jane.personal@gmail.com --rm-email old@acme.com
|
|
212
214
|
crm contact edit ct_01J8Z... --add-phone "+44-20-7946-0958" --rm-phone "+1-310-555-9876"
|
|
213
215
|
crm contact edit ct_01J8Z... --add-company "Acme Ventures" --rm-company "Old Corp"
|
|
214
|
-
crm contact edit ct_01J8Z... --set title=CEO --set source=referral
|
|
215
|
-
crm contact edit jane@acme.com --add-tag vip --rm-tag cold
|
|
216
216
|
```
|
|
217
217
|
|
|
218
218
|
| Flag | Description |
|
|
@@ -236,16 +236,20 @@ crm contact edit jane@acme.com --add-tag vip --rm-tag cold
|
|
|
236
236
|
#### `crm contact rm <id-or-email-or-phone-or-handle>`
|
|
237
237
|
|
|
238
238
|
```bash
|
|
239
|
-
crm contact rm jane@acme.com
|
|
240
|
-
crm contact rm "+1-212-555-1234" --force
|
|
239
|
+
crm contact rm jane@acme.com # by email
|
|
240
|
+
crm contact rm "+1-212-555-1234" --force # by phone (skip confirmation)
|
|
241
|
+
crm contact rm janedoe # by social handle (LinkedIn, X, etc.)
|
|
241
242
|
```
|
|
242
243
|
|
|
243
244
|
Prompts for confirmation unless `--force` is passed. Removes the contact and unlinks from deals/companies (does not delete linked entities).
|
|
244
245
|
|
|
245
|
-
#### `crm contact merge <
|
|
246
|
+
#### `crm contact merge <ref> <ref>`
|
|
246
247
|
|
|
247
248
|
```bash
|
|
248
|
-
crm contact merge ct_01J8Z... ct_02K9A...
|
|
249
|
+
crm contact merge ct_01J8Z... ct_02K9A... # by ID
|
|
250
|
+
crm contact merge jane@acme.com jane.doe@gmail.com # by email
|
|
251
|
+
crm contact merge "+1-212-555-1234" "+44-20-7946-0958" # by phone
|
|
252
|
+
crm contact merge janedoe janetdoe # by social handle
|
|
249
253
|
```
|
|
250
254
|
|
|
251
255
|
Merges two contacts. Keeps the first, absorbs data from the second. Emails, phones, companies, tags, custom fields, and activity history are combined. Deals linked to the second contact are relinked to the first. The first contact's name and social handles take priority. Prints the surviving contact ID.
|
|
@@ -291,9 +295,9 @@ crm company list --tag enterprise --sort name
|
|
|
291
295
|
#### `crm company show <id-or-website-or-phone>`
|
|
292
296
|
|
|
293
297
|
```bash
|
|
294
|
-
crm company show acme.com
|
|
295
|
-
crm company show co_01J8Z...
|
|
296
|
-
crm company show "+1-212-555-1234"
|
|
298
|
+
crm company show acme.com # by website
|
|
299
|
+
crm company show co_01J8Z... # by ID
|
|
300
|
+
crm company show "+1-212-555-1234" # by phone
|
|
297
301
|
```
|
|
298
302
|
|
|
299
303
|
Accepts ID, any stored website, or any phone number. Shows company details plus all linked contacts and deals.
|
|
@@ -301,7 +305,8 @@ Accepts ID, any stored website, or any phone number. Shows company details plus
|
|
|
301
305
|
#### `crm company edit <id-or-website-or-phone>`
|
|
302
306
|
|
|
303
307
|
```bash
|
|
304
|
-
crm company edit acme.com --name "Acme Inc" --set industry=Fintech
|
|
308
|
+
crm company edit acme.com --name "Acme Inc" --set industry=Fintech # by website
|
|
309
|
+
crm company edit "+1-212-555-1234" --add-tag enterprise # by phone
|
|
305
310
|
crm company edit co_01J8Z... --add-website acme.co.uk --add-phone "+44-20-7946-0958"
|
|
306
311
|
crm company edit acme.com --rm-website old-acme.com --rm-phone "+1-415-555-0000"
|
|
307
312
|
```
|
|
@@ -320,12 +325,20 @@ crm company edit acme.com --rm-website old-acme.com --rm-phone "+1-415-555-0000"
|
|
|
320
325
|
|
|
321
326
|
#### `crm company rm <id-or-website-or-phone>`
|
|
322
327
|
|
|
328
|
+
```bash
|
|
329
|
+
crm company rm acme.com # by website
|
|
330
|
+
crm company rm "+1-212-555-1234" --force # by phone (skip confirmation)
|
|
331
|
+
crm company rm co_01J8Z... # by ID
|
|
332
|
+
```
|
|
333
|
+
|
|
323
334
|
Prompts for confirmation unless `--force` is passed. Unlinks contacts and deals but does not delete them.
|
|
324
335
|
|
|
325
|
-
#### `crm company merge <
|
|
336
|
+
#### `crm company merge <ref> <ref>`
|
|
326
337
|
|
|
327
338
|
```bash
|
|
328
|
-
crm company merge co_01J8Z... co_02K9A...
|
|
339
|
+
crm company merge co_01J8Z... co_02K9A... # by ID
|
|
340
|
+
crm company merge acme.com acme.co.uk # by website
|
|
341
|
+
crm company merge "+1-212-555-1234" "+44-20-7946-0958" # by phone
|
|
329
342
|
```
|
|
330
343
|
|
|
331
344
|
Merges two companies. Keeps the first, absorbs data from the second. Websites, phones, tags, and custom fields are combined. All contacts and deals linked to the second company are relinked to the first. The first company's name takes priority. Prints the surviving company ID.
|
|
@@ -1299,9 +1312,9 @@ GitHub Actions pipeline:
|
|
|
1299
1312
|
|
|
1300
1313
|
---
|
|
1301
1314
|
|
|
1302
|
-
##
|
|
1315
|
+
## Created by Duet
|
|
1303
1316
|
|
|
1304
|
-
crm.cli is
|
|
1317
|
+
crm.cli is created by **[Duet](https://duet.so)** — a cloud agent workspace where every user gets a private cloud computer with a persistent, always-on AI agent. Set up crm.cli in your own Duet workspace and run it with Claude Code or Codex in the cloud — no local setup required.
|
|
1305
1318
|
|
|
1306
1319
|
[Try Duet →](https://duet.so)
|
|
1307
1320
|
|
package/dist/cli.js
CHANGED
|
@@ -1146,6 +1146,29 @@ function levenshtein(a, b) {
|
|
|
1146
1146
|
}
|
|
1147
1147
|
return dp[la][lb];
|
|
1148
1148
|
}
|
|
1149
|
+
function diceCoefficient(a, b) {
|
|
1150
|
+
if (a === b) {
|
|
1151
|
+
return 1;
|
|
1152
|
+
}
|
|
1153
|
+
if (a.length < 2 || b.length < 2) {
|
|
1154
|
+
return 0;
|
|
1155
|
+
}
|
|
1156
|
+
const bigrams = (s) => {
|
|
1157
|
+
const m = new Map;
|
|
1158
|
+
for (let i = 0;i < s.length - 1; i++) {
|
|
1159
|
+
const bg = s.slice(i, i + 2);
|
|
1160
|
+
m.set(bg, (m.get(bg) || 0) + 1);
|
|
1161
|
+
}
|
|
1162
|
+
return m;
|
|
1163
|
+
};
|
|
1164
|
+
const aBi = bigrams(a);
|
|
1165
|
+
const bBi = bigrams(b);
|
|
1166
|
+
let overlap = 0;
|
|
1167
|
+
for (const [bg, count] of aBi) {
|
|
1168
|
+
overlap += Math.min(count, bBi.get(bg) || 0);
|
|
1169
|
+
}
|
|
1170
|
+
return 2 * overlap / (a.length - 1 + b.length - 1);
|
|
1171
|
+
}
|
|
1149
1172
|
|
|
1150
1173
|
// src/db.ts
|
|
1151
1174
|
var SCHEMA_SQL = `
|
|
@@ -2505,8 +2528,9 @@ function contactDupeReasons(a, b) {
|
|
|
2505
2528
|
const bName = (b.name || "").toLowerCase();
|
|
2506
2529
|
const nameDistance = levenshtein(aName, bName);
|
|
2507
2530
|
const maxLen = Math.max(aName.length, bName.length);
|
|
2508
|
-
const
|
|
2509
|
-
|
|
2531
|
+
const levSimilarity = maxLen > 0 ? 1 - nameDistance / maxLen : 0;
|
|
2532
|
+
const nameSimilarity = Math.max(levSimilarity, diceCoefficient(aName, bName));
|
|
2533
|
+
if (nameSimilarity >= 0.6) {
|
|
2510
2534
|
reasons.push("similar name");
|
|
2511
2535
|
}
|
|
2512
2536
|
const aEmails = safeJSON(a.emails);
|
|
@@ -2574,8 +2598,9 @@ function companyDupeReasons(a, b) {
|
|
|
2574
2598
|
const bName = (b.name || "").toLowerCase();
|
|
2575
2599
|
const nameDistance = levenshtein(aName, bName);
|
|
2576
2600
|
const maxLen = Math.max(aName.length, bName.length);
|
|
2577
|
-
const
|
|
2578
|
-
|
|
2601
|
+
const levSimilarity = maxLen > 0 ? 1 - nameDistance / maxLen : 0;
|
|
2602
|
+
const nameSimilarity = Math.max(levSimilarity, diceCoefficient(aName, bName));
|
|
2603
|
+
if (nameSimilarity >= 0.6) {
|
|
2579
2604
|
reasons.push("similar name");
|
|
2580
2605
|
}
|
|
2581
2606
|
const aWebsites = safeJSON(a.websites);
|
|
@@ -3261,7 +3286,12 @@ function getDaemonArgs() {
|
|
|
3261
3286
|
if (!script) {
|
|
3262
3287
|
return [];
|
|
3263
3288
|
}
|
|
3264
|
-
|
|
3289
|
+
let resolved;
|
|
3290
|
+
try {
|
|
3291
|
+
resolved = realpathSync(script);
|
|
3292
|
+
} catch {
|
|
3293
|
+
return [];
|
|
3294
|
+
}
|
|
3265
3295
|
if (/\.[tj]s$/.test(resolved)) {
|
|
3266
3296
|
return [resolved];
|
|
3267
3297
|
}
|
|
@@ -3497,6 +3527,23 @@ function registerFuseCommands(program) {
|
|
|
3497
3527
|
program.command("mount").description("Mount CRM as virtual filesystem").argument("[mountpoint]", "Mount point directory").option("--readonly", "Mount read-only").action(async (mountpoint, opts) => {
|
|
3498
3528
|
const { config } = await getCtx();
|
|
3499
3529
|
const mp = mountpoint || config.mount.default_path;
|
|
3530
|
+
const pidFile = join3(tmpdir(), `crm-mount-${slugify(mp)}.pid`);
|
|
3531
|
+
if (existsSync3(pidFile)) {
|
|
3532
|
+
const pids = readFileSync2(pidFile, "utf-8").trim().split(`
|
|
3533
|
+
`);
|
|
3534
|
+
const alive = pids.some((pid) => {
|
|
3535
|
+
try {
|
|
3536
|
+
process.kill(Number(pid), 0);
|
|
3537
|
+
return true;
|
|
3538
|
+
} catch {
|
|
3539
|
+
return false;
|
|
3540
|
+
}
|
|
3541
|
+
});
|
|
3542
|
+
if (alive) {
|
|
3543
|
+
die(`Error: ${mp} is already mounted. Run \`crm unmount ${mp}\` first.`);
|
|
3544
|
+
}
|
|
3545
|
+
unlinkSync(pidFile);
|
|
3546
|
+
}
|
|
3500
3547
|
if (!existsSync3(mp)) {
|
|
3501
3548
|
mkdirSync4(mp, { recursive: true });
|
|
3502
3549
|
}
|
|
@@ -5425,7 +5472,7 @@ if (process.argv[1]?.endsWith("fuse-daemon.ts")) {
|
|
|
5425
5472
|
|
|
5426
5473
|
// src/cli.ts
|
|
5427
5474
|
var program = new Command;
|
|
5428
|
-
program.name("crm").description("Headless CLI-first CRM").version("0.3.
|
|
5475
|
+
program.name("crm").description("Headless CLI-first CRM").version("0.3.4");
|
|
5429
5476
|
program.exitOverride();
|
|
5430
5477
|
registerContactCommands(program);
|
|
5431
5478
|
registerCompanyCommands(program);
|