@agent-crm/cli 0.0.1

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 ADDED
@@ -0,0 +1,108 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/header.png" alt="agent-crm">
4
+
5
+ </div>
6
+
7
+ Claude is now running your GTM. Your lead lists live in CSVs because you want to move fast with claude code, but existing CRMs were built for humans, not agents. Their MCPs slow you down, bloat your context, and kill your usage limits.
8
+
9
+ Agent CRM gives your agent a structured backend it can query, edit, diff and validate, fast.
10
+
11
+ The source of truth is a portable `.acrm` file. UIs, CLIs, scripts, and agents all operate on it and you can send it around like any other file.
12
+
13
+ ```txt
14
+ ┌──────────────┐
15
+ │ Custom UIs │
16
+ └──────┬───────┘
17
+
18
+ ┌────────────┐ ┌──────▼──────┐ ┌───────────────┐
19
+ │ AI Agents ├─────►│ .acrm │◄─────┤ CLI / Scripts │
20
+ └────────────┘ └─────────────┘ └───────────────┘
21
+ ```
22
+
23
+ ## Quickstart
24
+
25
+ Install the CLI:
26
+
27
+ ```bash
28
+ npm install -g @agent-crm/cli
29
+ ```
30
+
31
+ Create your first `.acrm` file and let Claude rip on it:
32
+
33
+ ```bash
34
+ claude --dangerously-skip-permissions
35
+ ```
36
+
37
+ Create an .acrm file
38
+
39
+ ```bash
40
+ acrm init pipeline.acrm
41
+ ```
42
+
43
+ Then import your CSVs
44
+
45
+ ```bash
46
+ acrm import csv ./leads.csv
47
+ ```
48
+
49
+ And query the file any time with:
50
+
51
+ ```bash
52
+ acrm execute "select * from people limit 5;"
53
+ ```
54
+
55
+ ## Why Agent CRM
56
+ - **🧩 Headless:** Ships as a CLI.
57
+ - **⚒️ Skills based:** Claude writes skills against the CLI (transcript ingestion, stale-deal sweeps, weekly reports) as `.md` files.
58
+ - **🧱 Modeled:** uses Attio's data model out of the box. Typed, related, queryable with plain SQL. Fixed schema = predictable agent edits.
59
+ - **🔀 Version controlled:** every change is a checkpoint on a branch. Diff, merge, revert, time-travel.
60
+
61
+
62
+ ## Stateful skills for GTM
63
+
64
+ Skills are how Claude does the work. Bring your own, or use the ones we ship — `prep-call`, `post-call`, `follow-up`, `stale-opportunities`, `champion-left`, `new-hire-trigger`. Claude can write new ones in seconds.
65
+
66
+ **[`prep-call`](.claude/skills/prep-call.md).** Before a meeting, Claude pulls the person's full history from your `.acrm`, fetches their LinkedIn profile (cached, 14-day TTL), and hands you a one-pager with discovery questions tied to what they've actually been talking about.
67
+
68
+ **[`post-call`](.claude/skills/post-call.md).** After a meeting, Claude pulls the transcript from Granola, resolves the person in `.acrm`, extracts the problem + would-pay signal, and writes a `last_call` value plus any deal-stage update via `acrm execute`. You review the SQL output before the next sync.
69
+
70
+ **[`follow-up`](.claude/skills/follow-up.md).** Claude queries `.acrm` for leads with stale activity, reads the prior thread for each, and drafts the next message in your tone of voice. You review and send.
71
+
72
+ **[`stale-opportunities`](.claude/skills/stale-opportunities.md).** Run nightly. Claude finds deals stuck in qualified/proposal for 60+ days, re-enriches the primary contact via Apollo, scans for new ICP-matched hires and news signals at the account, and writes back a status (`actionable` / `dead` / `needs_review`) with a one-line narrative so AEs can triage in seconds.
73
+
74
+ **[`champion-left`](.claude/skills/champion-left.md).** Run biweekly. Claude scans open pipeline for primary contacts whose Apollo employment record changed in the last 14 days and DMs the assigned AE the affected deals, departure dates, and the champion's new employer — so you can pivot to a new contact before the deal goes cold.
75
+
76
+ **[`new-hire-trigger`](.claude/skills/new-hire-trigger.md).** Run monthly. Claude searches Apollo for ICP-matched executives (VP Sales, CRO, Head of RevOps, etc.) hired in the last 30 days at your ABM accounts, flags the ones with open pipeline, and surfaces a re-engagement list — a new buyer often resets a stalled deal.
77
+
78
+ Each skill is a markdown file in `.claude/skills/`. Here's what `post-call` looks like:
79
+
80
+ ```markdown
81
+ ---
82
+ description: Pull a Granola transcript, resolve the person in .acrm, and log the call via SQL — using the CLI's three commands: init, import csv, execute.
83
+ ---
84
+
85
+ ## Steps
86
+
87
+ 1. **Resolve the person** with a SQL lookup against `pipeline.acrm`:
88
+ `acrm execute "SELECT DISTINCT record_id FROM acrm_value WHERE object_slug = 'people' AND attribute_slug = 'email_addresses' AND active_until IS NULL AND normalized_key = ?" '["<email>"]' --json`
89
+
90
+ 2. **Find the Granola meeting** via `mcp__granola__list_meetings`. Filter
91
+ to meetings where the person's name appears in the title or
92
+ participants. If multiple, ask the user to pick.
93
+
94
+ 3. **Fetch the transcript** with `mcp__granola__get_meeting_transcript`.
95
+
96
+ 4. **Extract the call fields** (problem, would-pay, next steps).
97
+
98
+ 5. **Write back via `acrm execute`.** Close the previous `last_call`
99
+ value (`UPDATE acrm_value SET active_until = …`) and insert a new
100
+ one. Update the deal's `stage` the same way if it moved.
101
+
102
+ 6. **Report a short summary.** The user reviews the JSON output.
103
+ ```
104
+
105
+ Need something custom? Just ask:
106
+
107
+ > _"Write me a skill that reads my call transcripts, updates deal stages, and posts a summary to Slack."_
108
+
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { registerInit } from "../commands/init.js";
4
+ import { registerExecute } from "../commands/execute.js";
5
+ import { registerImport } from "../commands/import.js";
6
+ import { registerUi } from "../commands/ui.js";
7
+ import { fail } from "../output/json.js";
8
+ import { ERR } from "../lib/errors.js";
9
+ const program = new Command();
10
+ program
11
+ .name("acrm")
12
+ .description("Headless CRM for Claude Code. Stores people (keyed by email), companies (keyed by domain), and deals in a portable .acrm file.")
13
+ .version("0.0.1")
14
+ .option("-w, --workspace <path>", "path to .acrm file (default: walk up from cwd)")
15
+ .option("--json", "force JSON output (default when stdout is not a TTY)")
16
+ .addHelpText("after", `
17
+ Data model:
18
+ people contacts, identified by email
19
+ companies organizations, identified by domain
20
+ deals sales opportunities, created on demand
21
+
22
+ Typical flow:
23
+ acrm init <name>.acrm create a workspace
24
+ acrm import csv ./leads.csv load people + companies (and deals if columns present)
25
+ acrm ui browse the workspace in a local UI
26
+ acrm execute "SELECT ..." run SQL against the workspace
27
+
28
+ SQL engine: DataFusion (NOT SQLite/Postgres)
29
+ - Use $1, $2 placeholders. The '?' placeholder is rejected.
30
+ - No sqlite_master — use information_schema for introspection.
31
+ - For JSON columns, use lix_json_get / lix_json_get_text (NOT json_extract).
32
+ - See \`acrm execute --help\` for the full dialect reference.
33
+
34
+ Introspection (run via \`acrm execute "<sql>"\`):
35
+ SELECT table_name FROM information_schema.tables WHERE table_schema='public'
36
+ SELECT column_name, data_type FROM information_schema.columns WHERE table_name='acrm_value'
37
+ SELECT * FROM acrm_object -- registered objects
38
+ SELECT object_slug, attribute_slug, attribute_type FROM acrm_attribute
39
+ SELECT object_slug, COUNT(*) FROM acrm_record GROUP BY object_slug
40
+ `);
41
+ registerInit(program);
42
+ registerImport(program);
43
+ registerExecute(program);
44
+ registerUi(program);
45
+ program.parseAsync(process.argv).catch((err) => {
46
+ fail(err instanceof Error ? err.message : String(err), ERR.UNHANDLED);
47
+ process.exit(1);
48
+ });
49
+ //# sourceMappingURL=acrm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acrm.js","sourceRoot":"","sources":["../../src/bin/acrm.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAEvC,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,MAAM,CAAC;KACZ,WAAW,CACV,gIAAgI,CACjI;KACA,OAAO,CAAC,OAAO,CAAC;KAChB,MAAM,CAAC,wBAAwB,EAAE,gDAAgD,CAAC;KAClF,MAAM,CAAC,QAAQ,EAAE,sDAAsD,CAAC;KACxE,WAAW,CACV,OAAO,EACP;;;;;;;;;;;;;;;;;;;;;;;;CAwBH,CACE,CAAC;AAEJ,YAAY,CAAC,OAAO,CAAC,CAAC;AACtB,cAAc,CAAC,OAAO,CAAC,CAAC;AACxB,eAAe,CAAC,OAAO,CAAC,CAAC;AACzB,UAAU,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IAC7C,IAAI,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;IACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,75 @@
1
+ import { openWorkspace } from "../workspace/open.js";
2
+ import { exec } from "../db/execute.js";
3
+ import { fail, ok, setJsonMode } from "../output/json.js";
4
+ import { AcrmError, ERR } from "../lib/errors.js";
5
+ export function registerExecute(program) {
6
+ program
7
+ .command("execute <sql> [params]")
8
+ .description("run a SQL query or mutation against the .acrm file; params is a JSON array. SQL dialect is DataFusion (NOT SQLite/Postgres) — see `acrm execute --help`.")
9
+ .addHelpText("after", `
10
+ SQL dialect: DataFusion (NOT SQLite, NOT Postgres)
11
+ - Placeholders are $1, $2, ... The '?' placeholder is rejected.
12
+ - No sqlite_master — use information_schema.tables / .columns.
13
+ - Single statement per call.
14
+
15
+ JSON projection (json_extract is NOT available; use the lix UDFs instead):
16
+ lix_json_get(json, key_or_index, ...) returns a JSON value
17
+ lix_json_get_text(json, key_or_index, ...) returns text
18
+ Example:
19
+ SELECT lix_json_get_text(value_json, 'value') AS company_name
20
+ FROM acrm_value
21
+ WHERE object_slug = 'companies' AND attribute_slug = 'name'
22
+ AND active_until IS NULL;
23
+
24
+ JSON columns to be aware of:
25
+ acrm_value.value_json the typed payload for an attribute value
26
+ acrm_value.provenance_json import row index, source metadata
27
+ acrm_attribute.config_json per-attribute config (status options, ref target, ...)
28
+
29
+ Introspection (use these instead of sqlite_master):
30
+ acrm execute "SELECT table_name FROM information_schema.tables WHERE table_schema='public'"
31
+ acrm execute "SELECT column_name, data_type FROM information_schema.columns WHERE table_name='acrm_value'"
32
+ acrm execute "SELECT * FROM acrm_object" # registered objects
33
+ acrm execute "SELECT object_slug, attribute_slug, attribute_type, is_multivalued, is_unique FROM acrm_attribute ORDER BY object_slug"
34
+ acrm execute "SELECT object_slug, COUNT(*) AS n FROM acrm_record GROUP BY object_slug"
35
+
36
+ Errors carry the lix engine code + hint when applicable
37
+ (e.g. LIX_SQL_PARSE_ERROR with hint: "Use $1 instead of ?").
38
+ `)
39
+ .action(async (sql, paramsJson) => {
40
+ const root = program.opts();
41
+ setJsonMode(root.json);
42
+ try {
43
+ let params = [];
44
+ if (paramsJson) {
45
+ let parsed;
46
+ try {
47
+ parsed = JSON.parse(paramsJson);
48
+ }
49
+ catch {
50
+ throw new AcrmError(`params must be a JSON array, got: ${paramsJson}`, ERR.INVALID_INPUT);
51
+ }
52
+ if (!Array.isArray(parsed)) {
53
+ throw new AcrmError("params must be a JSON array", ERR.INVALID_INPUT);
54
+ }
55
+ params = parsed;
56
+ }
57
+ const lix = await openWorkspace({ workspace: root.workspace });
58
+ try {
59
+ const result = await exec(lix, sql, params);
60
+ ok({ rows: result.rows, rows_affected: result.rowsAffected });
61
+ }
62
+ finally {
63
+ await lix.close();
64
+ }
65
+ }
66
+ catch (e) {
67
+ if (e instanceof AcrmError)
68
+ fail(e.message, e.code, e.hint);
69
+ else
70
+ fail(e instanceof Error ? e.message : String(e), ERR.EXECUTE);
71
+ process.exit(1);
72
+ }
73
+ });
74
+ }
75
+ //# sourceMappingURL=execute.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execute.js","sourceRoot":"","sources":["../../src/commands/execute.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAElD,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,OAAO;SACJ,OAAO,CAAC,wBAAwB,CAAC;SACjC,WAAW,CACV,0JAA0J,CAC3J;SACA,WAAW,CACV,OAAO,EACP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BL,CACI;SACA,MAAM,CAAC,KAAK,EAAE,GAAW,EAAE,UAA8B,EAAE,EAAE;QAC5D,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAA4C,CAAC;QACtE,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvB,IAAI,CAAC;YACH,IAAI,MAAM,GAAsB,EAAE,CAAC;YACnC,IAAI,UAAU,EAAE,CAAC;gBACf,IAAI,MAAe,CAAC;gBACpB,IAAI,CAAC;oBACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;gBAClC,CAAC;gBAAC,MAAM,CAAC;oBACP,MAAM,IAAI,SAAS,CACjB,qCAAqC,UAAU,EAAE,EACjD,GAAG,CAAC,aAAa,CAClB,CAAC;gBACJ,CAAC;gBACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC3B,MAAM,IAAI,SAAS,CAAC,6BAA6B,EAAE,GAAG,CAAC,aAAa,CAAC,CAAC;gBACxE,CAAC;gBACD,MAAM,GAAG,MAA2B,CAAC;YACvC,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,aAAa,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;YAC/D,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;gBAC5C,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,aAAa,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC;YAChE,CAAC;oBAAS,CAAC;gBACT,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;YACpB,CAAC;QACH,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,YAAY,SAAS;gBAAE,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;;gBACvD,IAAI,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;YACnE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"}