{"_id":"@bunkernineai/crm.sdk","_rev":"3-42d4ef6d0ea4d5da348a358d5690a980","name":"@bunkernineai/crm.sdk","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@bunkernineai/crm.sdk","version":"1.0.0","author":{"name":"Sheikh Sifat"},"license":"MIT","_id":"@bunkernineai/crm.sdk@1.0.0","maintainers":[{"name":"bunkernineai","email":"diversity.hello@gmail.com"}],"homepage":"https://github.com/bunkernine/crm.sdk#readme","bugs":{"url":"https://github.com/bunkernine/crm.sdk/issues"},"dist":{"shasum":"a221b435e0900207b0ed1e3d27f2027079ced903","tarball":"https://registry.npmjs.org/@bunkernineai/crm.sdk/-/crm.sdk-1.0.0.tgz","fileCount":55,"integrity":"sha512-W/zBMZe2e7LTiXdELzgktoN+bciRjDrcAue3RCY1kj/eWFG574oglIdxr1ANAd/OXZLt0zViQ1XObF5HwybdfA==","signatures":[{"sig":"MEQCIFpvl/g+9ol+mvbrDCUfiOKW03GdkXnqFPawUEEh6yAPAiAjHutTHGKtUrRPBiOnyMKkPTIu3YbOPVwN1/LspLkmRg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":211069},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"bdc38873b09f3ee1b2c4dfae8e15b25e17f6ea40","scripts":{"lint":"ultracite fix","test":"bun test --timeout 30000 --max-concurrency 1","build":"tsc -p tsconfig.build.json","check-types":"tsc --noEmit","prepublishOnly":"bun run build"},"_npmUser":{"name":"bunkernineai","email":"diversity.hello@gmail.com"},"repository":{"url":"git+https://github.com/bunkernine/crm.sdk.git","type":"git"},"_npmVersion":"11.16.0","description":"Typed CRM SDK for AI agents","directories":{},"lint-staged":{"*.{js,jsx,ts,tsx,json,jsonc,css,scss,md,mdx}":["npx ultracite fix"]},"_nodeVersion":"24.18.0","dependencies":{"ulid":"^2.3.0","postgres":"^3.4.9","drizzle-orm":"^0.45.1","normalize-url":"^9.0.0","libphonenumber-js":"^1.12.0"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","ultracite":"^7.2.3","@types/bun":"^1.2.10","typescript":"~5.9.3","drizzle-kit":"^0.31.9","lint-staged":"^16.4.0","@biomejs/biome":"^2.4.4"},"_npmOperationalInternal":{"tmp":"tmp/crm.sdk_1.0.0_1789150857739_0.044643890557997334","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@bunkernineai/crm.sdk","version":"1.0.1","author":{"name":"Sheikh Sifat"},"license":"MIT","_id":"@bunkernineai/crm.sdk@1.0.1","maintainers":[{"name":"bunkernineai","email":"diversity.hello@gmail.com"}],"homepage":"https://github.com/bunkernine/crm.sdk#readme","bugs":{"url":"https://github.com/bunkernine/crm.sdk/issues"},"dist":{"shasum":"83a4342ac9d811146f50a3ded761275dc0e8b406","tarball":"https://registry.npmjs.org/@bunkernineai/crm.sdk/-/crm.sdk-1.0.1.tgz","fileCount":55,"integrity":"sha512-Fh5xaH+JyxIWzHiHoEVHIzHpuaPOsKpwc3Yj9nMkthRzFYv1RTppJS9R14MIHklQevqQcMTH+wNCoiQNs3pr6Q==","signatures":[{"sig":"MEUCIQCHHRUMC1WDv3Z+zAl1GmCXu2KattEptEaCB3qdwA8AkQIgcG8VBSpGMfzF9xQFDaU/E5hBgm0M1h8/bEMU5MT1rak=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQDssMGLNxt09Va12c0gaIgcz4pRMWBdPdZNSNVUc0LPcAIhAIXsO+22pVrvU0IK7TItxr/Bu5+01dmoMDKcr+0l7i8V","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":211198},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"a9f0f88ed3700869b4457dfec09463ccde3d2b75","scripts":{"lint":"ultracite fix","test":"bun test --timeout 30000 --max-concurrency 1","build":"tsc -p tsconfig.build.json","check-types":"tsc --noEmit","prepublishOnly":"bun run build"},"_npmUser":{"name":"bunkernineai","email":"diversity.hello@gmail.com"},"repository":{"url":"git+https://github.com/bunkernine/crm.sdk.git","type":"git"},"_npmVersion":"11.16.0","description":"Typed CRM SDK for AI agents","directories":{},"lint-staged":{"*.{js,jsx,ts,tsx,json,jsonc,css,scss,md,mdx}":["npx ultracite fix"]},"_nodeVersion":"24.18.0","dependencies":{"ulid":"^2.3.0","postgres":"^3.4.9","drizzle-orm":"^0.45.1","normalize-url":"^9.0.0","libphonenumber-js":"^1.12.0"},"_hasShrinkwrap":false,"devDependencies":{"husky":"^9.1.7","ultracite":"^7.2.3","@types/bun":"^1.2.10","typescript":"~5.9.3","drizzle-kit":"^0.31.9","lint-staged":"^16.4.0","@biomejs/biome":"^2.4.4"},"_npmOperationalInternal":{"tmp":"tmp/crm.sdk_1.0.1_1789151769849_0.40170944698883715","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-09-11T18:20:57.532Z","modified":"2026-09-16T14:08:06.456Z","1.0.0":"2026-09-11T18:20:57.882Z","1.0.1":"2026-09-11T18:36:09.935Z"},"bugs":{"url":"https://github.com/bunkernine/crm.sdk/issues"},"author":{"name":"Sheikh Sifat"},"license":"MIT","homepage":"https://github.com/bunkernine/crm.sdk#readme","repository":{"url":"git+https://github.com/bunkernine/crm.sdk.git","type":"git"},"description":"Typed CRM SDK for AI agents","maintainers":[{"email":"diversity.hello@gmail.com","name":"sheikh_sifat"}],"readme":"# crm.sdk\r\n\r\n**A typed CRM library for AI agents.** Contacts, companies, deals, activities, search, and reports — same domain as the old `crm.cli` product, exposed as `createCrm()` instead of a filesystem mount or CLI.\r\n\r\nAgents import this package (or the host app wraps these methods). There is no MCP, REST, or FUSE layer.\r\n\r\n```ts\r\nimport { createCrm } from '@bunkernineai/crm.sdk'\r\n\r\nconst crm = await createCrm({\r\n  connectionString: process.env.DATABASE_URL!,\r\n  phone: { default_country: 'US', display: 'national' },\r\n})\r\n\r\nconst id = await crm.contact.add({\r\n  name: 'Jane Doe',\r\n  email: ['jane@acme.com'],\r\n  phone: ['+1-212-555-1234'],\r\n  linkedin: 'linkedin.com/in/janedoe',\r\n  company: ['Acme'],\r\n})\r\nawait crm.report.stale({ days: 30 })\r\nawait crm.close()\r\n```\r\n\r\n> Original product by [Duet](https://duet.so) / David Zhang. Published as `@bunkernineai/crm.sdk`. MIT.\r\n\r\n## Install\r\n\r\n```bash\r\nnpm install @bunkernineai/crm.sdk\r\n# or: bun add @bunkernineai/crm.sdk\r\n```\r\n\r\nThe skill does **not** replace the package. Agents still `import { createCrm } from '@bunkernineai/crm.sdk'`. Postgres must be reachable. On first `createCrm`, the SDK runs `CREATE SCHEMA IF NOT EXISTS crm` and creates the four entity tables plus `search_index`.\r\n\r\n### Install as an agent skill\r\n\r\nThe contract lives in [`skills/SKILL.md`](./skills/SKILL.md) (`name: crm-sdk`). Copy it into a Cursor skills directory so the agent can load it:\r\n\r\n```bash\r\n# After npm install @bunkernineai/crm.sdk (project skill)\r\nmkdir -p .cursor/skills/crm-sdk\r\ncp node_modules/@bunkernineai/crm.sdk/skills/SKILL.md .cursor/skills/crm-sdk/SKILL.md\r\n\r\n# From this repo\r\nmkdir -p .cursor/skills/crm-sdk\r\ncp skills/SKILL.md .cursor/skills/crm-sdk/SKILL.md\r\n\r\n# Personal (all projects)\r\nmkdir -p ~/.cursor/skills/crm-sdk\r\ncp skills/SKILL.md ~/.cursor/skills/crm-sdk/SKILL.md\r\n```\r\n\r\nClaude Code:\r\n\r\n```bash\r\nclaude skills add https://github.com/bunkernine/crm.sdk/tree/main/skills\r\n```\r\n\r\n## Why this package\r\n\r\nExisting CRMs are GUI-first. This one is **typed methods in, records or thrown errors out** so an agent can `show` a contact by email, move a deal, and answer “who is stale?” without inventing SQL.\r\n\r\n**Deep normalization.** E.164 phones, website URLs, social handles (paste a LinkedIn URL, it stores the handle), entity merge with reference relinking, fuzzy duplicate detection.\r\n\r\n**Lookup by ref.** IDs (`ct_` / `co_` / `dl_` / `ac_`), email, phone (any common format), social URL/handle, or website — same as the old CLI.\r\n\r\n**Failures throw** `CrmError` with the same messages the CLI printed on stderr (`invalid phone`, `duplicate email`, `contact not found`, …). Agents can match and retry.\r\n\r\n## createCrm\r\n\r\n```ts\r\nawait createCrm({\r\n  connectionString: string\r\n  pipeline?: {\r\n    stages?: string[]\r\n    won_stage?: string\r\n    lost_stage?: string\r\n  }\r\n  phone?: {\r\n    default_country?: string // ISO 3166-1 alpha-2, e.g. \"US\"\r\n    display?: 'international' | 'national' | 'e164'\r\n  }\r\n  hooks?: Partial<Record<HookName, (data) => boolean | Promise<boolean>>>\r\n  search_limit?: number // default 20\r\n})\r\n```\r\n\r\nDefaults: stages `lead` → `qualified` → `proposal` → `negotiation` → `closed-won` → `closed-lost`; phone display `international`; `search_limit` 20.\r\n\r\nDo not walk `crm.toml` from disk. Pass options.\r\n\r\n## Methods (CLI → SDK)\r\n\r\n| CLI | SDK |\r\n|---|---|\r\n| `crm contact add/list/show/edit/rm/merge` | `crm.contact.add/list/show/edit/rm/merge` |\r\n| `crm company *` | `crm.company.*` |\r\n| `crm deal add/list/show/edit/move/rm` | `crm.deal.*` |\r\n| `crm pipeline` | `crm.pipeline()` |\r\n| `crm log` / `crm activity list` | `crm.log` / `crm.activity.list` |\r\n| `crm tag` / `untag` / `tag list` | `crm.tag` / `crm.untag` / `crm.tag.list` |\r\n| `crm search` / `find` / `index status\\|rebuild` | `crm.search` / `crm.find` / `crm.index.status` / `crm.index.rebuild` |\r\n| `crm dupes` | `crm.dupes` |\r\n| `crm report *` | `crm.report.pipeline/activity/stale/conversion/velocity/forecast/won/lost` |\r\n| `crm import contacts\\|companies\\|deals` | `crm.import.contacts/companies/deals` (string CSV/JSON or records) |\r\n| `crm export *` | `crm.export.contacts/companies/deals/all` |\r\n\r\nFlag names stay as camelCase: `--add-email` → `addEmail`, `--dry-run` → `dryRun`, `--expected-close` → `expectedClose`. Repeatable flags are `string[]`.\r\n\r\n`rm` requires `{ force: true }` (non-interactive). Without it, throws.\r\n\r\nList/show return entity objects (`emails: string[]`, `custom_fields: Record<…>`), not stringified JSON columns.\r\n\r\n### Contacts\r\n\r\n```ts\r\nawait crm.contact.add({\r\n  name: 'Jane Doe',\r\n  email: ['jane@acme.com'],\r\n  phone: ['+1-212-555-1234'],\r\n  company: ['Acme Corp'],\r\n  tag: ['hot-lead'],\r\n  linkedin: 'janedoe',\r\n  x: 'janedoe',\r\n  set: ['title=CTO', 'source=conference'],\r\n})\r\n\r\nawait crm.contact.list({ tag: 'hot-lead', filter: 'name ~= Jane', limit: 20 })\r\nawait crm.contact.show('jane@acme.com')\r\nawait crm.contact.edit(id, { addEmail: ['jane.doe@gmail.com'], addTag: ['vip'] })\r\nawait crm.contact.merge(keepId, dropId)\r\nawait crm.contact.rm(id, { force: true })\r\n```\r\n\r\n### Companies and deals\r\n\r\n```ts\r\nawait crm.company.add({ name: 'Acme', website: ['acme.com'] })\r\nawait crm.deal.add({\r\n  title: 'Acme — platform',\r\n  value: 50_000,\r\n  stage: 'qualified',\r\n  contact: ['jane@acme.com'],\r\n  company: 'Acme',\r\n})\r\nawait crm.deal.move(dealId, { stage: 'proposal', note: 'Sent deck' })\r\nawait crm.pipeline()\r\n```\r\n\r\n### Activity, tags, search\r\n\r\n```ts\r\nawait crm.log({ type: 'note', body: 'Great call', contact: [janeId] })\r\nawait crm.activity.list({ contact: janeId })\r\nawait crm.tag(janeId, ['vip'])\r\nawait crm.search('acme')\r\nawait crm.find('fintech CTO') // word-overlap on search_index.content, not embeddings\r\nawait crm.dupes({ threshold: 0.5 })\r\n```\r\n\r\n`search` uses Postgres `plainto_tsquery` / `to_tsvector('simple', content)`, then `ILIKE` if that path throws. `find` scores word overlap on the same index.\r\n\r\n### Reports and import/export\r\n\r\n```ts\r\nawait crm.report.stale({ days: 14, type: 'contact' })\r\nawait crm.report.forecast()\r\nawait crm.import.contacts(csvString, { dryRun: true, skipErrors: true })\r\nawait crm.export.contacts('csv')\r\nawait crm.export.all()\r\n```\r\n\r\n### Filters\r\n\r\n`list` `filter` expressions: `=`, `!=`, `~=`, `>`, `<`, combined with `AND` / `OR`. Custom fields: `custom_fields.role = CTO`.\r\n\r\n### Hooks\r\n\r\nSame names as the CLI (`pre-contact-add`, `post-deal-stage-change`, …). Callbacks receive a JSON-able payload. Return `false` (or a rejected promise) to abort.\r\n\r\n```ts\r\nawait createCrm({\r\n  connectionString,\r\n  hooks: {\r\n    'pre-contact-add': (data) => data.name !== 'spam',\r\n  },\r\n})\r\n```\r\n\r\n### IDs\r\n\r\nStored ids are `` `ct_${string}` ``, `` `co_${string}` ``, `` `dl_${string}` ``, `` `ac_${string}` `` (ULID). Lookup arguments that accept email/phone/handle stay `string`.\r\n\r\n## Storage\r\n\r\nPostgres schema **`crm`**. JSON arrays/objects are `jsonb` (`emails`, `phones`, `tags`, `custom_fields`, …). Deal `company` is a FK `ON DELETE SET NULL`. Unique partial indexes on contact social handles.\r\n\r\nThe caller owns the connection (`connectionString` or future client). No SQLite file.\r\n\r\n## Types\r\n\r\nThe package exports `CrmClient`, `Contact`, `Company`, `Deal`, `Activity`, `ContactAddInput`, `DealMove` via `DealEditInput` / move args, `CrmError`, hook names, and the rest of the model. `strict` TypeScript; `.d.ts` ships with the package.\r\n\r\n## Tests\r\n\r\nFunctional tests call `createCrm({ connectionString })` against Postgres (schema `crm`, truncated per context). Set `DATABASE_URL` (CI provides a Postgres 16 service). Run serially:\r\n\r\n```bash\r\nexport DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres\r\nbun test --timeout 30000 --max-concurrency 1\r\n```\r\n\r\n## License\r\n\r\nMIT. Original authorship retained.\r\n","readmeFilename":"README.md"}